Information Architecture skill for Claude Code
Navigation built from user tasks and tested labels. Claude builds structure from ranked user tasks and a real content inventory rather than the org chart, picks a navigation model that fits the content, and writes labels in the words found in support tickets and search logs. A bundled script audits the sitemap outline for depth, single-child levels, vague labels and duplicates, and tree-test targets decide when the structure is done.
- name
information-architecture - size195 lines · ≈2,300 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Structures websites, docs and apps so people find things — content inventory and audit, user task lists, open and closed card sorting, choosing a navigation model (hierarchy, hub-and-spoke, faceted, sequential), labeling in users' words, sitemaps, URL and breadcrumb alignment, and tree testing to validate findability before visual design. Includes a script that audits a sitemap outline for depth, breadth and label problems. Use when planning a site or app navigation, reorganizing docs or a help center, merging products, or when users say they cannot find things. Try asking
- Reorganize our help center, people keep saying they can't find anything
- Design the navigation for our invoicing app before we start on screens
- Audit this sitemap and tell me what is buried or badly labeled
Use it when
- Planning navigation for a new site, app or docs portal
- Users or support tickets report that things are hard to find
- Merging products or sections into one structure
- Preparing card sorts or tree tests
Skip it when
- You need tab bars, stacks and sheets for a native app, use mobile-ui-patterns instead
- The problem is crawlability or indexing rather than human findability, use technical-seo instead
How to check the result
The sitemap audit reports no errors and a tree test of the top tasks reaches at least 70-80 percent success with mostly direct paths.
The common mistake it prevents
Grouping content by internal team or department names, which mirrors the company instead of the tasks people arrive with.
IA is done when the top user tasks can be completed from the home screen in a tree test with at least 70–80% success and little backtracking, every label uses the words users use, each piece of content has exactly one home, and the URL, breadcrumb and navigation agree with each other.
Scope
This skill owns structure, grouping, labels and navigation models. Visual styling of
nav components belongs to responsive-layouts or the product’s design system;
mobile tab/stack mechanics are mobile-ui-patterns; docs site tooling is docs-site;
search engines and crawl structure are technical-seo.
Ground rules
- Start from tasks and content, never from the org chart. “Departments” is a structure for the company, not the user.
- Users’ words beat internal jargon. If support tickets say “receipt”, do not label it “Payment artifacts”.
- Prefer broad and shallow over narrow and deep — but not 25 top-level items. Target 5–7 primary items and 3 levels or fewer for routine tasks.
- One home per item; use cross-links, related links and search for secondary paths.
- Validate structure with a text-only tree test before any visual design — styling hides bad labels.
Workflow
- [ ] 1. Inventory: list what exists (pages, screens, docs, features)
- [ ] 2. Tasks: the top 10–15 things users come to do, ranked by frequency and value
- [ ] 3. Audit content: keep, merge, rewrite, delete
- [ ] 4. Card sort to learn users' mental model (or a lightweight substitute)
- [ ] 5. Choose the navigation model
- [ ] 6. Draft the sitemap with labels; run scripts/sitemap_audit.py
- [ ] 7. Tree test the top tasks; revise; retest
- [ ] 8. Specify URLs, breadcrumbs, local nav, cross-links and search behavior
1. Inventory
For an existing site or app, get the real list instead of guessing:
curl -s https://example.com/sitemap.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' > urls.txt
find app src/pages src/routes -name 'page.*' -o -name '+page.*' 2>/dev/null # framework routes
find docs content -name '*.md*' | sort # docs pages
Record per item in a spreadsheet: URL/route, title, type (task, reference, marketing, legal), owner, last updated, traffic (if analytics exist), and top search queries landing on it.
2. Tasks
Source tasks from evidence: site search logs, support tickets, analytics top pages and exits, sales/support interviews. Write each as a verb phrase in user language: “Download last month’s invoice”, “Add a teammate”, “Find the API rate limits”. Rank by frequency and importance; the top 5 tasks must be reachable in 3 clicks or fewer (or one search).
3. Content audit decisions
| Finding | Action |
|---|---|
| Two pages answer the same question | Merge; redirect the loser |
| Page has no traffic, no task, outdated | Delete (redirect if it has inbound links) |
| Page serves a task but uses internal terms | Rewrite title and label |
| Page mixes audiences (admin + end user) | Split by audience task |
| Long page answers five tasks | Split, or add anchored sections linked from nav |
4. Card sorting
- Open sort (participants create and name groups) to discover mental models for a new structure. 15–20 participants give stable patterns; 30–60 cards, each a real content item phrased neutrally (avoid the label words you plan to test).
- Closed sort (participants place cards into your groups) to check a proposed grouping.
- Hybrid when you have candidate groups but allow new ones.
- Tools: Optimal Workshop (OptimalSort), Maze, UXtweak, or paper cards in person.
- Analyze with the similarity matrix and dendrogram: items placed together by more than 60% of participants belong together; items scattered across groups need clearer wording or a cross-link.
Without access to users, run a proxy: cluster support tickets and search queries by topic, and sort cards with 3–5 people from support or sales, flagging the result as a hypothesis to tree test.
5. Navigation model
| Model | Use when | Watch out for |
|---|---|---|
| Hierarchy (tree) | Most sites and docs; clear categories | Depth creep; single-child levels |
| Hub-and-spoke | Mobile apps, task-focused tools: return to a home between tasks | Cross-task jumps are slow |
| Faceted / filtered | Large homogeneous catalogs (products, templates, jobs, docs reference) | Facets must match how users decide; show counts and zero-result handling |
| Sequential | Linear processes (onboarding, checkout, setup) | Needs progress, back, save-and-resume |
| Object-oriented (nouns first) | SaaS apps: Projects, Invoices, Clients, then actions inside | Verb-only items like “Manage” hide objects |
| Audience split | Truly different audiences with different tasks (Developers / Admins) | Users who do not know which audience they are |
App rule of thumb: primary navigation lists objects users work on (Invoices, Clients, Reports), settings and account live in a separate utility area, and creation actions sit on the object screens plus one global “New” if creation is frequent.
6. Sitemap and labels
Write the sitemap as an indented outline with URLs (one item per line):
- Invoices | /invoices
- Drafts | /invoices?status=draft
- Recurring | /invoices/recurring
- Clients | /clients
- Reports | /reports
- Profit and loss | /reports/profit-loss
- Tax summary | /reports/tax
- Settings | /settings
Label rules:
- 1–3 words, nouns for places (“Invoices”), verbs only for actions (“Create invoice”).
- Front-load the distinguishing word: “Tax summary”, not “Summary of taxes”.
- Siblings are mutually exclusive and parallel in grammar and casing.
- Ban containers that hide content: Resources, Solutions, More, Tools, Misc, General. If needed for marketing sites, the dropdown must name what is inside.
- Match the terms from support tickets and search logs; keep a glossary so the same thing has the same name in nav, page title, button and docs.
Audit the outline:
python3 scripts/sitemap_audit.py sitemap.md # defaults: depth 3, 7 top items
python3 scripts/sitemap_audit.py sitemap.md --max-depth 4 --strict
It reports item counts per level and flags: too many top-level items, items deeper than
the target, single-child parents (merge the levels), parents with more than 12 children,
vague labels, long labels, duplicate sibling labels, the same label in several places,
mixed sibling casing, and child URLs that do not sit under their parent’s path. Exit code
1 on errors (or warnings with --strict).
7. Tree testing
Test the text-only hierarchy with tasks before design:
- 8–12 tasks covering the top tasks plus 2–3 risky areas; write them as scenarios without using label words (“You were charged twice last month. Where would you go?”).
- 30–50 participants for quantitative confidence (Treejack, Maze, UXtweak).
- Read three numbers per task: success rate (target 70–80%+ for top tasks), directness (success without backtracking), and first click (wrong first clicks predict failure).
- When a task fails, look at where people went: if most chose the same wrong branch, that branch is where users expect it — move or cross-link the item.
- Change the structure, retest the failed tasks only, stop when top tasks pass.
8. Wayfinding specification
- URLs reflect the hierarchy for content sites (
/docs/billing/invoices); apps may use object routes (/invoices/123). Never break existing URLs without 301s. - Breadcrumbs on any page at depth 3+, generated from the same tree as the nav.
- Local navigation (sidebar, tabs) for sections with 5+ siblings; current item
marked with
aria-current="page". - Cross-links for items with a second home: “Related: Tax summary”.
- Search as a complement, not a fix: show it for collections over ~50 items, and map common queries (from logs) to the right page with synonyms.
- Mega menus only when there are 3+ groups of 4+ items; group headings must be links themselves and the menu must work by keyboard.
Gotchas
- Card sort results describe how people group content, not how they navigate; always follow with a tree test.
- Designing the IA around launch-day content; plan for the category that will have 80 items in a year (that is where faceting or an index page is needed).
- Labels tested in English, shipped in other languages without re-checking length and
meaning — see
i18n-localization. - Moving pages without redirects destroys search traffic and bookmarks.
- Hiding primary sections in a hamburger on desktop; discoverability drops sharply when navigation is not visible.
Output contract
**Top tasks (ranked):** 1. Download an invoice … 10. Change plan
**Model:** object-oriented app nav + faceted Reports
**Sitemap:** sitemap.md (34 items, 6 top-level, max depth 3; audit: 0 errors, 2 kept warnings)
**Content decisions:** merge 7, delete 12 (redirect map attached), rewrite 9 titles
**Tree test:** 10 tasks, 42 participants — top-5 tasks 84% success, 71% direct;
"Tax summary" moved under Reports after 58% chose that branch
**Glossary:** 18 terms with nav/page/docs usage Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/sitemap_audit.py
#!/usr/bin/env python3
"""Audit a sitemap / navigation outline for structural IA problems.
Usage:
python3 scripts/sitemap_audit.py sitemap.md [--max-depth 3] [--max-top 7] [--strict]
Input: an indented outline, one label per line. Markdown bullets ("- ", "* ", "1. ")
and plain indentation (2 or 4 spaces, or tabs) both work. An optional URL may follow
the label after " | " or " -> ", e.g. " - Invoices | /billing/invoices".
Lines starting with "#" are treated as comments.
Reports depth, breadth, single-child parents, duplicate and vague labels, long labels,
inconsistent sibling casing, and URL paths that disagree with the hierarchy.
Exits 1 on errors (or on warnings too with --strict). Standard library only.
"""
from __future__ import annotations
import argparse
import re
import sys
from collections import defaultdict
from dataclasses import dataclass, field
VAGUE = {
"more", "misc", "miscellaneous", "other", "others", "general", "resources", "solutions",
"stuff", "info", "information", "tools", "explore", "discover", "hub", "center", "centre",
"overview", "home page", "learn", "things", "items", "content", "extras", "advanced",
}
BULLET_RE = re.compile(r"^(\s*)(?:[-*+]\s+|\d+[.)]\s+)?(.*)$")
@dataclass
class Node:
label: str
url: str | None
depth: int
line: int
parent: "Node | None" = None
children: list["Node"] = field(default_factory=list)
def path(self) -> str:
parts, n = [], self
while n is not None:
parts.append(n.label)
n = n.parent
return " > ".join(reversed(parts))
def parse(text: str) -> list[Node]:
roots: list[Node] = []
stack: list[tuple[int, Node]] = []
for lineno, raw in enumerate(text.splitlines(), 1):
if not raw.strip() or raw.lstrip().startswith("#"):
continue
expanded = raw.replace("\t", " ")
m = BULLET_RE.match(expanded)
indent, body = len(m.group(1)), m.group(2).strip()
url = None
for sep in (" | ", " -> "):
if sep in body:
body, url = (s.strip() for s in body.split(sep, 1))
break
while stack and stack[-1][0] >= indent:
stack.pop()
parent = stack[-1][1] if stack else None
node = Node(body, url, (parent.depth + 1) if parent else 1, lineno, parent)
(parent.children if parent else roots).append(node)
stack.append((indent, node))
return roots
def walk(nodes: list[Node]):
for n in nodes:
yield n
yield from walk(n.children)
def case_style(label: str) -> str:
words = [w for w in re.findall(r"[A-Za-z][A-Za-z']*", label)]
if len(words) < 2:
return "single"
caps = sum(1 for w in words[1:] if w[0].isupper() and w.lower() not in {"and", "of", "the", "for", "to", "in", "a"})
if caps == 0:
return "sentence"
if caps == len([w for w in words[1:] if w.lower() not in {"and", "of", "the", "for", "to", "in", "a"}]):
return "title"
return "mixed"
def audit(roots: list[Node], max_depth: int, max_top: int) -> list[tuple[str, int, str]]:
out: list[tuple[str, int, str]] = []
nodes = list(walk(roots))
if len(roots) > max_top + 2:
out.append(("ERROR", 0, f"{len(roots)} top-level items; keep primary navigation to about {max_top} or fewer"))
elif len(roots) > max_top:
out.append(("WARN", 0, f"{len(roots)} top-level items; above the {max_top}-item target for primary navigation"))
by_label: dict[str, list[Node]] = defaultdict(list)
for n in nodes:
by_label[n.label.strip().lower()].append(n)
if n.depth > max_depth + 1:
out.append(("ERROR", n.line, f"depth {n.depth}: '{n.path()}' is buried; flatten or add a cross-link/hub"))
elif n.depth > max_depth:
out.append(("WARN", n.line, f"depth {n.depth}: '{n.path()}' is below the {max_depth}-level target"))
if len(n.children) == 1:
out.append(("WARN", n.line, f"'{n.label}' has a single child '{n.children[0].label}'; merge the levels"))
if len(n.children) > 12:
out.append(("WARN", n.line, f"'{n.label}' has {len(n.children)} children; group them or use a filterable index"))
if n.label.lower() in VAGUE:
out.append(("WARN", n.line, f"vague label '{n.label}' ('{n.path()}'); name what is inside"))
if len(n.label) > 28 or len(n.label.split()) > 4:
out.append(("WARN", n.line, f"long label '{n.label}'; navigation labels work best at 1-3 words"))
if n.url and n.parent and n.parent.url and not n.url.startswith("http"):
parent_url = n.parent.url.rstrip("/")
if parent_url and not n.url.startswith(parent_url + "/") and n.url != parent_url:
out.append(("INFO", n.line, f"URL {n.url} is not under parent {n.parent.url}; fine if intentional, else breadcrumbs and URLs disagree"))
for label, group in by_label.items():
if len(group) < 2:
continue
parents = defaultdict(list)
for g in group:
parents[id(g.parent)].append(g)
for same in parents.values():
if len(same) > 1:
out.append(("ERROR", same[1].line, f"duplicate sibling label '{same[0].label}' under '{same[0].parent.label if same[0].parent else '(root)'}'"))
if len(parents) > 1 and not group[0].children:
locs = "; ".join(g.path() for g in group[:4])
out.append(("WARN", group[1].line, f"label '{group[0].label}' appears in {len(group)} places ({locs}); one home + cross-links, or disambiguate"))
for parent_children in [roots] + [n.children for n in nodes if n.children]:
styles = {case_style(c.label) for c in parent_children} - {"single"}
if len(styles) > 1:
names = ", ".join(c.label for c in parent_children[:6])
out.append(("WARN", parent_children[0].line, f"mixed label casing among siblings ({names}); pick sentence or title case"))
order = {"ERROR": 0, "WARN": 1, "INFO": 2}
out.sort(key=lambda f: (order[f[0]], f[1]))
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("file")
ap.add_argument("--max-depth", type=int, default=3, help="target maximum depth (default 3)")
ap.add_argument("--max-top", type=int, default=7, help="target top-level item count (default 7)")
ap.add_argument("--strict", action="store_true", help="exit 1 on warnings as well")
args = ap.parse_args()
try:
with open(args.file, encoding="utf-8") as fh:
roots = parse(fh.read())
except OSError as exc:
print(f"ERROR cannot read {args.file}: {exc}", file=sys.stderr)
return 1
if not roots:
print("ERROR no outline items found")
return 1
nodes = list(walk(roots))
depth_counts = defaultdict(int)
for n in nodes:
depth_counts[n.depth] += 1
leaves = [n for n in nodes if not n.children]
print(f"== {args.file}")
print(f" {len(nodes)} items, {len(roots)} top-level, {len(leaves)} leaves, max depth {max(depth_counts)}")
print(" per level: " + ", ".join(f"L{d}={c}" for d, c in sorted(depth_counts.items())))
findings = audit(roots, args.max_depth, args.max_top)
for level, line, msg in findings:
print(f" {level:<5} {('L' + str(line)) if line else 'doc':<6} {msg}")
errors = sum(f[0] == "ERROR" for f in findings)
warns = sum(f[0] == "WARN" for f in findings)
print(f" -> {errors} error(s), {warns} warning(s)")
return 1 if errors or (args.strict and warns) else 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.