
Site Architecture
- 83 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Site Architecture is a Claude skill for designing SEO-focused website information architecture: URL hierarchy, navigation, silo structure, internal linking and crawl-equity management.
About
Site Architecture & Internal Linking is a framework for website information architecture aimed at SEO. It covers URL hierarchy design, navigation architecture, silo structure and topic clusters, internal linking strategy and crawl-equity management. It provides depth guidelines, URL construction rules, per-site-type URL patterns and a navigation-zone model. It runs in four modes: auditing an existing site, planning a new architecture, optimizing internal linking, and URL restructuring with redirect maps. Developers and SEOs use it to structure sites so crawl equity flows to the pages that matter.
- Designs URL hierarchy, navigation, silo structure and internal linking for SEO
- Includes depth guidelines, URL construction rules and site-type URL patterns
- Four operating modes: audit, new-site planning, internal-linking optimization and URL restructuring
Site Architecture by the numbers
- 83 all-time installs (skills.sh)
- Ranked #1,174 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
site-architecture capabilities & compatibility
Free; a guidance framework, no API keys.
- Capabilities
- seo audit
- Use cases
- seo · web design
- Pricing
- Free
What site-architecture says it does
Information architecture, URL hierarchy, internal linking strategy, navigation design, and silo structure for websites.
Production-grade website architecture framework covering URL hierarchy design, internal linking strategy, navigation optimization, silo structure, and crawl equity management.
Flat is better than empty hierarchy.
npx skills add https://github.com/borghei/claude-skills --skill site-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 83 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Design URL hierarchy, navigation, silos and internal linking so crawl equity flows to key pages.
Who is it for?
Auditing site structure, planning a new site's architecture, improving internal linking, or restructuring URLs.
When should I use this skill?
You are designing or restructuring a site's URL hierarchy, navigation, silos or internal linking for SEO.
What you get
A URL hierarchy, navigation and internal-linking plan (with redirect maps when restructuring) that concentrates crawl equity on rankable pages.
- URL hierarchy design
- Navigation architecture
- Silo/topic-cluster map
By the numbers
- 4 operating modes
- 6 navigation zones
- primary nav capped at 5-8 items
Files
Site Architecture & Internal Linking
Production-grade website architecture framework covering URL hierarchy design, internal linking strategy, navigation optimization, silo structure, and crawl equity management. Handles architecture audits, new site planning, and restructuring existing sites.
---
Table of Contents
- Operating Modes
- URL Structure Design
- Navigation Architecture
- Silo Structure and Topic Clusters
- Internal Linking Strategy
- Crawl Equity Management
- Architecture Audit Framework
- Restructuring Playbook
- Architecture Patterns by Site Type
- Common Mistakes
- Output Artifacts
- Related Skills
---
Operating Modes
Mode 1: Audit Current Architecture
Existing site needs structural assessment. Analyze depth distribution, orphan pages, link equity flow, and navigation effectiveness.
Mode 2: Plan New Architecture
Building from scratch or full redesign. Map business goals to site sections, design URL hierarchy, plan navigation, and define content silos.
Mode 3: Internal Linking Optimization
Structure is fine but link equity flow needs improvement. Identify hub pages, map spoke content, fix orphans, and optimize anchor text.
Mode 4: URL Restructuring
Changing URLs on an existing site. Plan the new structure, build redirect maps, and manage the migration.
---
URL Structure Design
Depth Guidelines
| Depth | Example | Use When | SEO Impact |
|---|---|---|---|
| 1 level (flat) | /cold-email-tips | Blog posts, standalone pages | Best crawl equity per page |
| 2 levels | /email-marketing/cold-email-tips | Category is a rankable page itself | Good, category page accumulates authority |
| 3 levels | /solutions/marketing/email-automation | Product families, nested services | Acceptable if each level has content |
| 4+ levels | /a/b/c/d/page | Never | Diluted equity, poor UX, crawl issues |
Decision rule: If the intermediate directory URL (/email-marketing/) will NOT be a real page with its own content and ranking target, do not create the directory. Flat is better than empty hierarchy.
URL Construction Rules
| Rule | Good | Bad | Why |
|---|---|---|---|
| Use hyphens | /seo-audit | /seo_audit | Underscores are not word separators for Google |
| Be descriptive | /pricing | /pricing-page | Redundant suffixes add nothing |
| Keep short | /guides/technical-seo | /guides/technical-seo-audit-checklist-complete-guide | Readability matters |
| Include keyword | /guides/seo-audit | /guides/article?id=4827 | Descriptive URLs rank better |
| Be consistent with trailing slashes | Pick one: /about or /about/ | Mix of both | Inconsistency creates duplicate content |
| Use lowercase | /about-us | /About-Us | Case sensitivity varies by server |
URL Patterns by Site Type
| Site Type | Pattern | Example |
|---|---|---|
| SaaS | /features/[feature], /solutions/[use-case], /integrations/[partner] | /features/analytics, /solutions/marketing, /integrations/slack |
| E-commerce | /[category]/[subcategory]/[product] | /mens/shoes/running-shoes-pro |
| Blog/Content | /blog/[slug] or /blog/[category]/[slug] | /blog/seo-audit-guide |
| Local business | /[service]/[location] | /plumbing/austin-tx |
| Documentation | /docs/[section]/[page] | /docs/api/authentication |
| Marketplace | /[category]/[listing] | /designers/john-smith |
---
Navigation Architecture
Navigation Zones
Every site has 6 navigation zones. Each serves a different purpose and carries different SEO weight.
| Zone | Items | SEO Weight | Design Rule |
|---|---|---|---|
| Primary nav | 5-8 items max | High (sitewide equity) | Only pages you want to rank for. Never "Resources" without a landing page. |
| Secondary nav | 3-7 per section | Medium | Sub-navigation within a section/silo |
| Breadcrumbs | Dynamic | High (upward equity) | Every non-homepage page. Each segment must be a real link. |
| Footer nav | 8-15 items max | Low-Medium | Key pages only. Not a dumping ground for every page. |
| Contextual (in-content) | 3-5 per page | Highest | Most powerful signal. Natural editorial links within body content. |
| Sidebar | 5-10 items | Low-Medium | Related content, category listing |
Primary Navigation Design
Rules:
- 5-8 items maximum. Cognitive overload starts at 9+ items.
- Every nav item links to a page you actively want to rank.
- Dropdown menus are fine, but the parent item must be a clickable, crawlable link (not just a hover trigger).
- Do not put utility pages (Contact, Privacy, Terms) in primary nav -- they belong in the footer.
- Mobile nav must expose the same structure as desktop nav (no hidden critical pages).
Breadcrumb Implementation
Add breadcrumbs to every non-homepage page. They serve three functions:
1. UX: Show users their location in the hierarchy 2. SEO: Create sitewide upward internal links to hub/category pages 3. Rich results: Enable BreadcrumbList schema for SERP breadcrumbs
Format: Home > Category > Subcategory > Current Page
Rules:
- Every breadcrumb segment must be a real, crawlable link
- Never use breadcrumbs as styled text without links
- Add BreadcrumbList schema markup alongside visible breadcrumbs
- Breadcrumb hierarchy should match URL hierarchy
---
Silo Structure and Topic Clusters
Hub-and-Spoke Model
A silo is a self-contained cluster of content about one topic, where all pages link to each other and to a central hub page.
┌──────────────────┐
│ HUB PAGE │
│ /seo/ │
│ (Pillar content)│
└────────┬─────────┘
┌────────────────┼────────────────┐
┌────┴────┐ ┌───┴────┐ ┌────┴────┐
│ SPOKE 1 │ │ SPOKE 2│ │ SPOKE 3 │
│technical│ │on-page │ │ link │
│ seo │ │ seo │ │building │
└────┬────┘ └───┬────┘ └────┬────┘
│ │ │
Cross-links Cross-links Cross-links
between between between
related related related
spokes spokes spokesBuilding Topic Clusters
Step 1: Identify 3-7 core topics for your site (these become your silos)
Step 2: For each topic, create one pillar page (the hub) that covers the topic broadly
Step 3: Create spoke content for each major sub-question or sub-topic
Step 4: Implement linking:
- Hub links DOWN to every spoke
- Every spoke links UP to the hub (with keyword-rich anchor text)
- Spokes link ACROSS to related spokes within the same silo
- Cross-silo links are fine when contextually genuine
Step 5: Build the content before building the links. Links to non-existent content are useless.
Silo Depth Guidelines
| Silo Size | Recommended Depth | Structure |
|---|---|---|
| 3-5 spokes | Flat | Hub + spokes, all at same level |
| 6-15 spokes | Shallow nested | Hub > Sub-hubs > Spokes |
| 16-50 spokes | Two-level | Hub > Category sub-hubs > Individual spokes |
| 50+ spokes | Paginated | Hub > Category sub-hubs > Paginated spoke listings |
---
Internal Linking Strategy
Link Equity Power Stack
Not all internal links carry equal weight. From most to least powerful:
| Rank | Link Type | Weight | When to Use |
|---|---|---|---|
| 1 | In-content contextual link | Highest | Natural editorial links within body copy |
| 2 | Hub page link | High | Pillar page linking to all its spokes |
| 3 | Navigation link | Medium | Sitewide, consistent, but diluted by ubiquity |
| 4 | Breadcrumb link | Medium | Upward equity flow, consistent |
| 5 | Footer link | Low | Sitewide, Google discounts these |
| 6 | Sidebar link | Low | Often not in main content flow |
Anchor Text Strategy
| Type | Example | Usage | Signal Strength |
|---|---|---|---|
| Partial match | "effective cold email strategies" | Primary approach (60-70% of links) | Strong |
| Exact match | "cold email templates" | Use sparingly (10-15% of links) | Strong but risky if overused |
| Branded | "our email guide" | Natural variation (10-15%) | Moderate |
| Descriptive | "this comprehensive guide" | Natural variation (5-10%) | Weak but natural |
| Generic | "click here", "learn more" | Avoid (< 5%) | None -- wasted signal |
| Naked URL | https://example.com/guide | Never for internal links | None |
Orphan Page Detection and Resolution
An orphan page is indexed in Google but has zero inbound internal links. It is invisible to the site's link graph.
Detection method: 1. Export all indexed URLs (GSC Coverage or site: query) 2. Export all internal link targets (crawl tool or link extraction) 3. Pages in set A but not set B are orphans
Resolution actions:
| Orphan Type | Action |
|---|---|
| Valuable content, belongs in a silo | Add contextual links from 3+ related pages |
| Old content, still relevant | Link from hub page and 1-2 spokes |
| Outdated content, no value | Redirect to relevant page or noindex |
| Utility page (shouldn't be indexed) | Add noindex |
Internal Linking Audit Checklist
| Check | Pass Criteria |
|---|---|
| Every content page has 3-5 outbound internal links | No page links only to itself or navigation |
| Every target page has 3+ inbound internal links | No orphans in the indexed set |
| Hub pages link to all their spokes | Complete coverage |
| Anchor text is descriptive and varied | No > 30% exact match for any keyword |
| No broken internal links | 0 broken links |
| Link depth from homepage < 4 clicks | 95%+ of pages within 3 clicks |
---
Crawl Equity Management
Homepage Equity Distribution
The homepage is your highest-equity page. Use it wisely.
| Homepage Link Target | Priority |
|---|---|
| Hub/pillar pages (top of each silo) | Highest -- link from homepage content area |
| Key product/service pages | High -- link from homepage or primary nav |
| Top-performing content | Medium -- link if contextually relevant |
| Utility pages (contact, about) | Low -- footer links only |
| Blog index | Medium -- primary or secondary nav |
Crawl Budget Allocation
| Page Category | % of Crawl Budget | Optimization |
|---|---|---|
| Money pages (product, pricing, features) | 20-30% | Internal link from homepage + nav |
| Pillar content (hub pages) | 20-30% | Extensive internal linking |
| Spoke content (blog, guides) | 30-40% | Linked from hubs + contextual |
| Utility pages (about, contact, legal) | 5-10% | Footer links only, minimal crawl |
| Tag/archive/pagination | < 5% | Noindex if thin, limit crawling |
---
Architecture Audit Framework
Audit Scorecard
| Dimension | Weight | Check |
|---|---|---|
| Crawl depth | 20% | 95%+ pages within 3 clicks of homepage |
| Orphan pages | 20% | 0 orphan pages in indexed set |
| URL cleanliness | 15% | Clean, descriptive, consistent URLs |
| Navigation completeness | 15% | All key pages accessible via primary/secondary nav |
| Internal link coverage | 15% | Every page has 3+ inbound internal links |
| Silo coherence | 10% | Topic clusters are well-defined with clear hub-spoke relationships |
| Breadcrumb implementation | 5% | Present on all non-homepage pages with schema |
Red Flag Indicators
| Signal | Severity | Action |
|---|---|---|
| Pages > 3 clicks from homepage | High | Create shortcuts via nav or hub page links |
| Hub page has no content (just links) | High | Add pillar content to all hub pages |
| Generic anchor text dominant (> 30%) | Medium | Rewrite anchor text to be descriptive |
| No breadcrumbs on deep pages | Medium | Implement breadcrumbs with BreadcrumbList schema |
| Sitemap includes noindex pages | Medium | Filter sitemap to only indexable pages |
| Primary nav links to utility pages | Low | Move Contact/Privacy to footer |
| Footer contains 50+ links | Low | Reduce to 8-15 key pages |
---
Restructuring Playbook
When to Restructure
| Signal | Restructure? | Alternative |
|---|---|---|
| Complete URL overhaul needed | Yes | -- |
| Adding 3 new sections | Partial -- new sections only | -- |
| Fixing orphan pages | No | Add internal links, no URL changes needed |
| Improving nav | No | Update navigation, keep URLs |
| Moving to new CMS | Yes (usually) | Keep URL structure if CMS supports it |
Restructuring Steps
1. Crawl current site -- Export all URLs, internal links, and rankings 2. Map current to new -- Create 1:1 URL redirect mapping 3. Build redirect rules -- 301 redirects for every changed URL 4. Chain existing redirects -- Update old redirects to point to final destination 5. Update internal links -- Point to new URLs (do not rely on redirects for internal links) 6. Update sitemap -- Reflect new URL structure 7. Monitor for 60 days -- Watch for crawl errors, ranking changes, traffic impact
---
Architecture Patterns by Site Type
SaaS Website
/ Homepage
├── /features/ Features hub
│ ├── /features/analytics Analytics feature
│ └── /features/reporting Reporting feature
├── /solutions/ Solutions hub (by use case)
│ ├── /solutions/marketing Marketing use case
│ └── /solutions/sales Sales use case
├── /integrations/ Integrations hub
│ ├── /integrations/slack Slack integration
│ └── /integrations/salesforce Salesforce integration
├── /pricing Pricing page
├── /blog/ Blog index
│ ├── /blog/seo-guide Blog post
│ └── /blog/growth-tips Blog post
├── /docs/ Documentation
└── /about About pageE-commerce Site
/ Homepage
├── /[category]/ Category page (with content)
│ ├── /[category]/[subcategory]/ Subcategory (with content)
│ │ └── /[category]/[subcategory]/[product] Product page
│ └── /[category]/[product] Product (if no subcategory)
├── /brands/ Brand hub
│ └── /brands/[brand] Brand page
├── /blog/ Content hub
└── /sale/ Promotions hubContent/Media Site
/ Homepage
├── /[topic]/ Topic hub (pillar content)
│ ├── /[topic]/[subtopic] Subtopic article
│ └── /[topic]/[guide] In-depth guide
├── /authors/ Author hub
│ └── /authors/[name] Author page
├── /tools/ Free tools hub
│ └── /tools/[tool] Individual tool
└── /newsletter Newsletter signup---
Common Mistakes
| Mistake | Why It Hurts | Fix |
|---|---|---|
| Orphan pages | No equity flows in, Google deprioritizes | Add contextual internal links from related content |
| URL changes without redirects | Lost link equity and broken backlinks | Always 301 redirect old URLs |
| Deep nesting (4+ levels) | Diluted crawl equity, confusing UX | Flatten structure |
| Empty category pages | Thin pages do not rank | Add pillar content to all category/hub pages |
| Homepage linking to nothing | Wastes highest-equity page | Link from home to all hub pages |
| Footer with 100+ links | Dilutes equity across too many targets | Limit footer to 8-15 key pages |
| Navigation not matching user mental model | Users leave, engagement drops | Run card-sort testing with real users |
| Dynamic parameter URLs | Creates duplicate content | Canonicalize or block with robots.txt |
| Sitewide sidebar links to every post | Diluted equity, adds noise | Remove or limit to "popular" posts |
---
Output Artifacts
| Artifact | Format | Description |
|---|---|---|
| Architecture Audit Scorecard | Weighted score table | Per-dimension scores with red flag indicators |
| Site Tree Diagram | Text-based hierarchy | Visual URL structure with annotations |
| URL Specification Table | Table | URL pattern, title template, parent page, schema type per section |
| Internal Linking Plan | Hub-spoke map | Topic cluster map with anchor text guidelines and orphan fix list |
| Redirect Map | Before/after URL table | 1:1 mapping for URL restructuring with 301 implementation |
| Navigation Spec | Zone-by-zone design | Primary, secondary, breadcrumb, footer, and contextual nav plans |
---
Related Skills
- seo-audit -- For comprehensive SEO audits where architecture is one of several problem areas. Use seo-audit for the full picture, site-architecture for deep structural work.
- schema-markup -- For adding BreadcrumbList and other structured data after architecture decisions are finalized.
- programmatic-seo -- For hub-and-spoke structures at scale when generating hundreds of template-based pages.
- content-creator -- For creating the pillar content that hub pages need to rank effectively.
---
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| Orphan pages appearing in Google index | Pages exist in sitemap or have external links but no internal links pointing to them | Add contextual internal links from 3+ related pages; update hub pages to include them |
| Crawl depth exceeds 4+ clicks for key pages | Flat navigation missing or hub pages not linking to spokes | Create shortcut links via primary nav, hub page content blocks, or featured sections |
| Category/hub pages ranking poorly | Hub pages have no substantive content — just link lists | Add 800+ words of pillar content to every hub page; they must rank on their own merit |
| Redirect chains after site migration | Old redirects not updated to point to final destination; chains accumulate over multiple migrations | Audit all existing redirects; collapse chains so every redirect points directly to the final URL |
| Internal links using generic anchor text | "Click here" and "learn more" anchor text dominating internal links | Rewrite anchors to be descriptive with partial-match keywords; aim for 60-70% partial match |
| New sections not getting crawled | No internal links from existing high-authority pages to new section | Add contextual links from homepage and related hub pages; submit new section sitemap |
---
Success Criteria
- Crawl depth: 95%+ of indexable pages reachable within 3 clicks from homepage
- Zero orphan pages: No indexed pages without at least 3 inbound internal links
- URL cleanliness: 100% of URLs follow the established pattern — lowercase, hyphenated, descriptive, consistent trailing slash policy
- Navigation coverage: All key revenue and pillar pages accessible via primary or secondary navigation
- Internal link density: Every content page has 3-5 outbound contextual internal links with descriptive anchor text
- Silo coherence: Each topic cluster has a defined hub page with bidirectional links to all spokes
- Breadcrumb coverage: BreadcrumbList schema present on 100% of non-homepage pages
---
Scope & Limitations
In scope:
- URL hierarchy design and restructuring
- Navigation architecture (primary, secondary, breadcrumb, footer, contextual, sidebar)
- Silo structure and topic cluster planning
- Internal linking strategy and optimization
- Crawl equity analysis and optimization
- Architecture audits with scored reports
- URL migration planning with redirect mapping
Out of scope:
- Content creation for hub pages (use Content Production)
- Schema markup implementation (use Schema Markup)
- Technical SEO beyond architecture (use SEO Audit)
- Visual design or UX design of navigation
- CMS development or template coding
- External link building
Known limitations:
- Crawl equity distribution is estimated — Google does not publish exact PageRank flow data
- Architecture changes on large sites (10K+ pages) require careful phased migration to avoid traffic loss
- Navigation testing (card sorts, tree tests) requires user research tools and participants
- Cross-silo linking recommendations are qualitative — no deterministic formula exists for optimal cross-linking density
---
Scripts
# Analyze sitemap for depth and structure issues
python scripts/sitemap_analyzer.py --file sitemap.xml --json
# Check URLs for redirect chains and patterns
python scripts/redirect_checker.py --file urls.txt --json
# Map internal link structure from a sitemap
python scripts/link_mapper.py --sitemap sitemap.xml --json#!/usr/bin/env python3
"""
Internal Link Structure Mapper
Maps internal linking structure from a sitemap, analyzes hub-spoke
relationships, identifies potential orphan pages, and scores link
equity distribution across the site architecture.
Usage:
python link_mapper.py --sitemap sitemap.xml
python link_mapper.py --sitemap sitemap.xml --json
python link_mapper.py --urls urls.txt --hub-analysis
"""
import argparse
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
from urllib.parse import urlparse
try:
import xml.etree.ElementTree as ET
except ImportError:
ET = None
SITEMAP_NS = {'sm': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
def parse_sitemap(filepath):
"""Extract URLs from XML sitemap."""
if not ET:
return [], "XML parser not available"
content = filepath.read_text(encoding="utf-8", errors="replace")
try:
root = ET.fromstring(content)
except ET.ParseError as e:
return [], f"Parse error: {e}"
urls = []
for loc in root.findall('.//sm:url/sm:loc', SITEMAP_NS):
if loc.text:
urls.append(loc.text.strip())
if not urls:
for loc in root.findall('.//url/loc'):
if loc.text:
urls.append(loc.text.strip())
return urls, None
def load_urls(filepath):
"""Load URLs from a text file."""
return [l.strip() for l in filepath.read_text().splitlines()
if l.strip() and not l.startswith('#')]
def build_hierarchy(urls):
"""Build a hierarchical structure from URLs."""
hierarchy = defaultdict(list)
url_info = {}
for url in urls:
parsed = urlparse(url)
segments = [s for s in parsed.path.split('/') if s]
depth = len(segments)
# Determine parent
if depth == 0:
parent = None
elif depth == 1:
parent = "/"
else:
parent_path = "/" + "/".join(segments[:-1]) + "/"
parent = parent_path
# Determine section (first segment)
section = segments[0] if segments else "root"
url_info[url] = {
"url": url,
"path": parsed.path,
"depth": depth,
"section": section,
"parent_path": parent,
"segments": segments,
}
hierarchy[section].append(url)
return hierarchy, url_info
def identify_hubs(hierarchy, url_info):
"""Identify potential hub pages in each section."""
hubs = []
for section, urls in hierarchy.items():
if section == "root":
continue
# The shallowest page in a section is likely the hub
section_urls = sorted(urls, key=lambda u: url_info[u]["depth"])
if section_urls:
hub_url = section_urls[0]
spoke_count = len(section_urls) - 1
hubs.append({
"section": section,
"hub_url": hub_url,
"hub_depth": url_info[hub_url]["depth"],
"spoke_count": spoke_count,
"spoke_urls": section_urls[1:],
})
return hubs
def analyze_link_equity(url_info, hubs):
"""Estimate link equity distribution."""
total = len(url_info)
depth_distribution = Counter()
section_distribution = Counter()
for info in url_info.values():
depth_distribution[info["depth"]] += 1
section_distribution[info["section"]] += 1
# Estimate equity concentration
# Pages at depth 1-2 get more equity than deep pages
equity_estimates = {}
for url, info in url_info.items():
depth = info["depth"]
if depth == 0:
equity = 100 # Homepage
elif depth == 1:
equity = 50
elif depth == 2:
equity = 25
elif depth == 3:
equity = 12
else:
equity = 5
equity_estimates[url] = equity
# Section balance
sections = list(section_distribution.keys())
if sections:
avg_pages = total / len(sections)
imbalanced = {s: c for s, c in section_distribution.items()
if c > avg_pages * 3 or (c < avg_pages * 0.2 and c < 3)}
else:
imbalanced = {}
return {
"depth_distribution": dict(depth_distribution),
"section_distribution": dict(section_distribution.most_common(20)),
"equity_estimates": equity_estimates,
"imbalanced_sections": imbalanced,
}
def detect_potential_orphans(url_info, hubs):
"""Identify pages that might be orphans based on structure."""
potential_orphans = []
hub_sections = {h["section"] for h in hubs}
for url, info in url_info.items():
# Deep pages not in any hub section
if info["depth"] >= 3 and info["section"] not in hub_sections:
potential_orphans.append({
"url": url,
"reason": "Deep page in section with no hub",
"depth": info["depth"],
})
# Pages at unusual depth for their section
section_urls = [u for u, i in url_info.items() if i["section"] == info["section"]]
section_depths = [url_info[u]["depth"] for u in section_urls]
if section_depths:
avg_depth = sum(section_depths) / len(section_depths)
if info["depth"] > avg_depth + 2:
potential_orphans.append({
"url": url,
"reason": "Unusually deep for its section",
"depth": info["depth"],
})
return potential_orphans
def main():
parser = argparse.ArgumentParser(
description="Map internal link structure from sitemap"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--sitemap", help="XML sitemap file")
group.add_argument("--urls", help="Text file with URLs")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--hub-analysis", action="store_true", help="Show hub-spoke analysis")
args = parser.parse_args()
urls = []
if args.sitemap:
fp = Path(args.sitemap)
if not fp.exists():
print(f"Error: {fp} not found", file=sys.stderr)
sys.exit(1)
urls, err = parse_sitemap(fp)
if err:
print(f"Error: {err}", file=sys.stderr)
sys.exit(1)
else:
fp = Path(args.urls)
if not fp.exists():
print(f"Error: {fp} not found", file=sys.stderr)
sys.exit(1)
urls = load_urls(fp)
if not urls:
print("No URLs found.", file=sys.stderr)
sys.exit(1)
hierarchy, url_info = build_hierarchy(urls)
hubs = identify_hubs(hierarchy, url_info)
equity = analyze_link_equity(url_info, hubs)
orphans = detect_potential_orphans(url_info, hubs)
result = {
"total_urls": len(urls),
"sections": len(hierarchy),
"hubs": hubs,
"equity_analysis": {
"depth_distribution": equity["depth_distribution"],
"section_distribution": equity["section_distribution"],
"imbalanced_sections": equity["imbalanced_sections"],
},
"potential_orphans": orphans,
}
if args.json:
print(json.dumps(result, indent=2, default=str))
else:
print(f"\n{'='*60}")
print(f" LINK STRUCTURE MAP — {len(urls)} URLs, {len(hierarchy)} sections")
print(f"{'='*60}")
print(f"\n Depth Distribution:")
for depth in sorted(equity["depth_distribution"].keys()):
count = equity["depth_distribution"][depth]
pct = round(count / len(urls) * 100, 1)
print(f" Depth {depth}: {count} pages ({pct}%)")
print(f"\n Top Sections:")
for section, count in list(equity["section_distribution"].items())[:10]:
print(f" /{section}/: {count} pages")
if args.hub_analysis and hubs:
print(f"\n Hub-Spoke Analysis:")
for hub in sorted(hubs, key=lambda h: h["spoke_count"], reverse=True):
print(f"\n Hub: /{hub['section']}/ ({hub['spoke_count']} spokes)")
print(f" Hub URL: {hub['hub_url']}")
for spoke in hub["spoke_urls"][:5]:
print(f" -> {spoke}")
if hub["spoke_count"] > 5:
print(f" ... and {hub['spoke_count'] - 5} more")
if equity["imbalanced_sections"]:
print(f"\n Imbalanced Sections:")
for section, count in equity["imbalanced_sections"].items():
print(f" /{section}/: {count} pages (check balance)")
if orphans:
print(f"\n Potential Orphan Pages ({len(orphans)}):")
for o in orphans[:10]:
print(f" {o['url']} — {o['reason']} (depth: {o['depth']})")
if len(orphans) > 10:
print(f" ... and {len(orphans) - 10} more")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
URL Redirect Chain Checker for Site Architecture
Checks URLs for redirect chains, HTTP/HTTPS consistency, trailing slash
consistency, and common URL pattern issues. Designed for architecture
migration audits and restructuring projects.
Usage:
python redirect_checker.py --file urls.txt
python redirect_checker.py --file urls.txt --json
python redirect_checker.py --url https://example.com/old-page
"""
import argparse
import json
import re
import sys
from pathlib import Path
from urllib.parse import urlparse
def normalize_url(url):
"""Ensure URL has a scheme."""
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
return url
def check_url_patterns(url):
"""Analyze URL for architectural issues."""
issues = []
parsed = urlparse(normalize_url(url))
# HTTP check
if parsed.scheme == 'http':
issues.append({
"type": "no_https",
"severity": "High",
"detail": "URL uses HTTP — should be HTTPS",
})
# Path depth
segments = [s for s in parsed.path.split('/') if s]
depth = len(segments)
if depth > 3:
issues.append({
"type": "deep_url",
"severity": "Medium",
"detail": f"URL depth is {depth} — ideally 3 or fewer levels",
})
# Uppercase
if parsed.path != parsed.path.lower():
issues.append({
"type": "uppercase",
"severity": "Medium",
"detail": "URL contains uppercase characters — may cause duplicates",
})
# Underscores
if '_' in parsed.path:
issues.append({
"type": "underscores",
"severity": "Low",
"detail": "URL uses underscores — hyphens preferred by Google",
})
# Parameters
if parsed.query:
issues.append({
"type": "parameters",
"severity": "Medium",
"detail": f"URL contains parameters: {parsed.query}",
})
# File extensions (non-standard)
if re.search(r'\.(php|asp|aspx|jsp|cgi)$', parsed.path, re.IGNORECASE):
issues.append({
"type": "legacy_extension",
"severity": "Low",
"detail": "URL has legacy file extension — clean URLs preferred",
})
# Double slashes in path
if '//' in parsed.path:
issues.append({
"type": "double_slash",
"severity": "Medium",
"detail": "URL contains double slash in path",
})
# IDs or numbers in URL
if re.search(r'/\d{4,}', parsed.path):
issues.append({
"type": "numeric_id",
"severity": "Low",
"detail": "URL contains numeric IDs — descriptive slugs preferred",
})
return {
"url": url,
"normalized": normalize_url(url),
"depth": depth,
"segments": segments,
"has_trailing_slash": parsed.path.endswith('/') and parsed.path != '/',
"issues": issues,
"passed": len([i for i in issues if i["severity"] in ("High", "Critical")]) == 0,
}
def check_redirect(url, max_hops=10):
"""Check URL for redirect chains via HTTP."""
url = normalize_url(url)
chain = []
current = url
visited = set()
issues = []
try:
import http.client
import ssl
for hop in range(max_hops):
if current in visited:
issues.append({
"type": "redirect_loop",
"severity": "Critical",
"detail": f"Loop detected at {current}",
})
break
visited.add(current)
parsed = urlparse(current)
try:
if parsed.scheme == 'https':
ctx = ssl.create_default_context()
conn = http.client.HTTPSConnection(parsed.netloc, timeout=10, context=ctx)
else:
conn = http.client.HTTPConnection(parsed.netloc, timeout=10)
path = parsed.path or '/'
if parsed.query:
path += '?' + parsed.query
conn.request('HEAD', path, headers={'User-Agent': 'Architecture-Audit/1.0'})
resp = conn.getresponse()
status = resp.status
chain.append({"hop": hop + 1, "url": current, "status": status})
if status in (301, 302, 303, 307, 308):
location = resp.getheader('Location', '')
if location:
from urllib.parse import urljoin
next_url = urljoin(current, location)
if status == 302:
issues.append({
"type": "temporary_redirect",
"severity": "High",
"detail": f"302 at hop {hop + 1} — should be 301 for permanent moves",
})
current = next_url
else:
break
else:
break
conn.close()
except Exception as e:
chain.append({"hop": hop + 1, "url": current, "status": "error", "error": str(e)})
break
except ImportError:
chain.append({"hop": 1, "url": current, "status": "skipped", "note": "HTTP check unavailable"})
hops = max(len(chain) - 1, 0)
if hops > 2:
issues.append({
"type": "long_chain",
"severity": "High",
"detail": f"Redirect chain has {hops} hops — max 2 recommended",
})
return {
"original_url": url,
"final_url": chain[-1]["url"] if chain else url,
"total_hops": hops,
"chain": chain,
"issues": issues,
}
def main():
parser = argparse.ArgumentParser(
description="Check URLs for redirect chains and architecture issues"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--url", help="Single URL to check")
group.add_argument("--file", help="File with URLs (one per line)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--patterns-only", action="store_true", help="Only check URL patterns, no HTTP requests")
args = parser.parse_args()
urls = []
if args.url:
urls = [args.url]
else:
fp = Path(args.file)
if not fp.exists():
print(f"Error: {fp} not found", file=sys.stderr)
sys.exit(1)
urls = [l.strip() for l in fp.read_text().splitlines() if l.strip() and not l.startswith('#')]
results = []
trailing_slash_counts = {"with": 0, "without": 0}
for url in urls:
pattern_result = check_url_patterns(url)
if pattern_result["has_trailing_slash"]:
trailing_slash_counts["with"] += 1
elif pattern_result["depth"] > 0:
trailing_slash_counts["without"] += 1
if not args.patterns_only:
redirect_result = check_redirect(url)
pattern_result["redirect"] = redirect_result
pattern_result["issues"].extend(redirect_result["issues"])
results.append(pattern_result)
# Check trailing slash consistency across all URLs
consistency_issue = None
if trailing_slash_counts["with"] > 0 and trailing_slash_counts["without"] > 0:
consistency_issue = {
"type": "inconsistent_trailing_slash",
"severity": "Medium",
"detail": f"{trailing_slash_counts['with']} URLs with trailing slash, {trailing_slash_counts['without']} without",
}
passed = sum(1 for r in results if r["passed"])
output = {
"total_urls": len(results),
"passed": passed,
"trailing_slash_consistency": consistency_issue,
"results": results,
}
if args.json:
print(json.dumps(output, indent=2))
else:
print(f"\n{'='*60}")
print(f" URL ARCHITECTURE CHECK — {len(results)} URLs")
print(f"{'='*60}")
print(f" Passed: {passed}/{len(results)}")
if consistency_issue:
print(f" [{consistency_issue['severity']}] {consistency_issue['detail']}")
for r in results:
all_issues = r["issues"]
status = "OK" if r["passed"] else "ISSUES"
print(f"\n [{status}] {r['url']} (depth: {r['depth']})")
if all_issues:
for issue in all_issues:
print(f" [{issue['severity']}] {issue['detail']}")
if "redirect" in r and r["redirect"]["total_hops"] > 0:
print(f" Redirects: {r['redirect']['total_hops']} hops -> {r['redirect']['final_url']}")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Site Architecture Sitemap Analyzer
Analyzes XML sitemaps for architectural issues including URL depth
distribution, directory structure patterns, orphan detection signals,
and crawl equity concentration.
Usage:
python sitemap_analyzer.py --file sitemap.xml
python sitemap_analyzer.py --file sitemap.xml --json
python sitemap_analyzer.py --file sitemap.xml --depth-report
"""
import argparse
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
from urllib.parse import urlparse
try:
import xml.etree.ElementTree as ET
except ImportError:
print("Error: xml.etree.ElementTree not available", file=sys.stderr)
sys.exit(1)
SITEMAP_NS = {'sm': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
def parse_sitemap(content):
"""Parse sitemap XML and extract URLs."""
try:
root = ET.fromstring(content)
except ET.ParseError as e:
return [], f"XML parse error: {e}"
urls = []
# Try with namespace
for url_elem in root.findall('.//sm:url/sm:loc', SITEMAP_NS):
if url_elem.text:
urls.append(url_elem.text.strip())
# Try without namespace
if not urls:
for url_elem in root.findall('.//url/loc'):
if url_elem.text:
urls.append(url_elem.text.strip())
# Check for sitemap index
if not urls:
for loc in root.findall('.//sm:sitemap/sm:loc', SITEMAP_NS):
if loc.text:
urls.append(loc.text.strip())
if not urls:
for loc in root.findall('.//sitemap/loc'):
if loc.text:
urls.append(loc.text.strip())
return urls, None
def analyze_depth(urls):
"""Analyze URL depth distribution."""
depth_counts = Counter()
depth_urls = defaultdict(list)
for url in urls:
parsed = urlparse(url)
segments = [s for s in parsed.path.split('/') if s]
depth = len(segments)
depth_counts[depth] += 1
if len(depth_urls[depth]) < 5: # Keep max 5 examples per depth
depth_urls[depth].append(url)
return dict(depth_counts), {k: v for k, v in depth_urls.items()}
def analyze_directories(urls):
"""Analyze directory/section structure."""
directories = Counter()
for url in urls:
parsed = urlparse(url)
segments = [s for s in parsed.path.split('/') if s]
if segments:
directories[segments[0]] += 1
return dict(directories.most_common(20))
def analyze_patterns(urls):
"""Detect URL patterns and anomalies."""
issues = []
stats = {
"total_urls": len(urls),
"unique_domains": len(set(urlparse(u).netloc for u in urls)),
"with_params": 0,
"with_uppercase": 0,
"with_underscores": 0,
"inconsistent_trailing_slash": False,
}
trailing_slash = {"with": 0, "without": 0}
for url in urls:
parsed = urlparse(url)
if parsed.query:
stats["with_params"] += 1
if parsed.path != parsed.path.lower():
stats["with_uppercase"] += 1
if '_' in parsed.path:
stats["with_underscores"] += 1
if parsed.path.endswith('/') and parsed.path != '/':
trailing_slash["with"] += 1
elif parsed.path and parsed.path != '/':
trailing_slash["without"] += 1
# Check trailing slash consistency
if trailing_slash["with"] > 0 and trailing_slash["without"] > 0:
stats["inconsistent_trailing_slash"] = True
issues.append({
"type": "inconsistent_trailing_slash",
"severity": "Medium",
"detail": f"{trailing_slash['with']} URLs with trailing slash, {trailing_slash['without']} without — pick one convention",
})
if stats["with_params"] > 0:
issues.append({
"type": "parameterized_urls",
"severity": "Medium",
"detail": f"{stats['with_params']} URLs contain query parameters — verify canonical handling",
})
if stats["with_uppercase"] > 0:
issues.append({
"type": "uppercase_urls",
"severity": "Medium",
"detail": f"{stats['with_uppercase']} URLs contain uppercase characters",
})
if stats["with_underscores"] > 0:
issues.append({
"type": "underscore_urls",
"severity": "Low",
"detail": f"{stats['with_underscores']} URLs use underscores instead of hyphens",
})
if stats["unique_domains"] > 1:
issues.append({
"type": "multiple_domains",
"severity": "High",
"detail": f"Sitemap contains URLs from {stats['unique_domains']} domains",
})
return stats, issues
def analyze_architecture_health(depth_counts, total_urls):
"""Score architectural health based on depth distribution."""
issues = []
# Calculate percentage at each depth
depth_pcts = {}
for depth, count in depth_counts.items():
depth_pcts[depth] = round(count / max(total_urls, 1) * 100, 1)
# Deep pages check
deep_pages = sum(count for depth, count in depth_counts.items() if depth >= 4)
deep_pct = round(deep_pages / max(total_urls, 1) * 100, 1)
if deep_pct > 5:
issues.append({
"type": "excessive_depth",
"severity": "High",
"detail": f"{deep_pct}% of pages are 4+ levels deep — target <5%",
})
# Shallow concentration check
shallow = sum(count for depth, count in depth_counts.items() if depth <= 2)
shallow_pct = round(shallow / max(total_urls, 1) * 100, 1)
if shallow_pct < 50:
issues.append({
"type": "deep_heavy",
"severity": "Medium",
"detail": f"Only {shallow_pct}% of pages at depth 1-2 — architecture may be too deep",
})
# Score
score = 100
for issue in issues:
if issue["severity"] == "High":
score -= 20
elif issue["severity"] == "Medium":
score -= 10
score = max(0, score)
return {
"depth_distribution": depth_pcts,
"deep_page_percentage": deep_pct,
"shallow_page_percentage": shallow_pct,
"score": score,
"issues": issues,
}
def main():
parser = argparse.ArgumentParser(
description="Analyze sitemap for site architecture issues"
)
parser.add_argument("--file", required=True, help="XML sitemap file path")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--depth-report", action="store_true", help="Show detailed depth analysis")
args = parser.parse_args()
fp = Path(args.file)
if not fp.exists():
print(f"Error: {fp} not found", file=sys.stderr)
sys.exit(1)
content = fp.read_text(encoding="utf-8", errors="replace")
urls, error = parse_sitemap(content)
if error:
print(f"Error: {error}", file=sys.stderr)
sys.exit(1)
if not urls:
print("No URLs found in sitemap.", file=sys.stderr)
sys.exit(1)
depth_counts, depth_examples = analyze_depth(urls)
directories = analyze_directories(urls)
url_stats, url_issues = analyze_patterns(urls)
health = analyze_architecture_health(depth_counts, len(urls))
all_issues = url_issues + health["issues"]
result = {
"source": str(fp),
"total_urls": len(urls),
"depth_distribution": depth_counts,
"directories": directories,
"url_stats": url_stats,
"architecture_health": health,
"issues": all_issues,
}
if args.json:
print(json.dumps(result, indent=2))
else:
print(f"\n{'='*60}")
print(f" SITE ARCHITECTURE ANALYSIS — {len(urls)} URLs")
print(f"{'='*60}")
print(f" Architecture Score: {health['score']}/100")
print(f"\n Depth Distribution:")
for depth in sorted(depth_counts.keys()):
count = depth_counts[depth]
pct = health["depth_distribution"].get(depth, 0)
bar = "#" * int(pct / 2)
print(f" Depth {depth}: {count:>6} pages ({pct:>5.1f}%) {bar}")
if args.depth_report:
print(f"\n Examples by Depth:")
for depth in sorted(depth_examples.keys()):
print(f" Depth {depth}:")
for url in depth_examples[depth][:3]:
print(f" {url}")
print(f"\n Top Directories:")
for dir_name, count in list(directories.items())[:10]:
print(f" /{dir_name}/: {count} pages")
if all_issues:
print(f"\n Issues ({len(all_issues)}):")
for issue in all_issues:
print(f" [{issue['severity']}] {issue['detail']}")
else:
print(f"\n No issues found.")
print()
if __name__ == "__main__":
main()
Related skills
FAQ
How deep should URLs be?
It recommends staying flat where possible and never going beyond 4 levels, and not creating a directory unless the intermediate URL will be a real ranking page.
What operating modes does it have?
Audit current architecture, plan new architecture, internal linking optimization, and URL restructuring.