| name | site-explorer |
| description | Creates an interactive website map — a zoomable JointJS tree showing every page of a documentation or marketing site, grouped by section, with click-to-open and search. Use when the user wants to visualize the structure of a website, explore a docs site, create a sitemap tree, or map out page hierarchy. Examples: 'create a site map for docs.jointjs.com', 'build a website explorer for react.dev', 'show me the structure of the Next.js docs', 'map out this documentation site', 'visualize the page hierarchy of stripe.com/docs', 'build a sitemap tree for our website'. |
Site Explorer Playground
Generates a self-contained HTML file with a zoomable JointJS tree showing every page of a website — pages as nodes colored by section, parent/child URL hierarchy as links, click-to-open, search, and section filter pills.
Key difference from other skills: Claude fetches the data (sitemap.xml or root page) during skill execution, then embeds it hardcoded in the HTML. The HTML itself makes no network requests.
How to use this skill
- Fetch the sitemap — see Phase 1 below
- Parse URLs into a tree — Phase 2
- Compute layout — Phase 3
- Load the template from
templates/site-explorer.md and write the HTML file
- Open in browser —
open <filename>-site-explorer.html
Filename: <domain>-site-explorer.html — e.g. docs-jointjs-site-explorer.html, react-dev-site-explorer.html
Phase 1 — Fetch
Try in order, stop when you get data:
1. WebFetch('<baseUrl>/sitemap.xml')
2. WebFetch('<baseUrl>/sitemap_index.xml') → lists sub-sitemaps, fetch each
3. WebFetch('<baseUrl>/sitemap/')
4. WebFetch(baseUrl) → parse <a href> links from the HTML as fallback
Extract all <loc> values from XML, or all internal href values from HTML.
Filter to URLs that start with baseUrl — discard external links, anchors (#), and query strings.
Deduplicate. Aim for 20–200 URLs — if more, keep only paths with depth ≤ 3.
Phase 2 — Build tree
function buildTree(urls, baseUrl) {
const nodes = new Map();
const root = { id: '__root__', label: baseUrl.replace(/https?:\/\//, ''), url: baseUrl, depth: 0, children: [] };
nodes.set('__root__', root);
const paths = urls
.map(u => u.replace(baseUrl, '').replace(/^\/|\/$/g, ''))
.filter(p => p.length > 0)
.sort();
paths.forEach(path => {
const segments = path.split('/');
segments.forEach((seg, i) => {
const id = segments.slice(0, i + 1).join('/');
if (nodes.(id)) ;
parentId = i === ? : segments.(, i).();
node = {
id,
: (seg).(, ),
: baseUrl + + id,
: + id,
: seg,
: i + ,
: segments[],
: [],
};
nodes.(id, node);
parent = nodes.(parentId);
(parent) parent..(node);
});
});
{ root, nodes };
}
Trim ghost nodes: if a URL segment appears only as a parent (no actual page at that URL), mark it isGhost: true — render it smaller/dimmer.
Phase 3 — Layout
Compute x/y positions with a simple level-order layout. Center each level horizontally:
const NW = 150, NH = 44, H_GAP = 24, V_GAP = 72;
function layoutTree(root) {
const levels = [];
const queue = [root];
while (queue.length) {
const node = queue.shift();
if (!levels[node.depth]) levels[node.depth] = [];
levels[node.depth].push(node);
node.children.forEach(c => queue.push(c));
}
const maxLevelW = Math.max(...levels.map(lvl => lvl.length * NW + Math.max(0, lvl.length - 1) * H_GAP));
const TREE_W = maxLevelW + 160;
levels.forEach(() => {
totalW = level. * + .(, level. - ) * ;
offsetX = ( - totalW) / ;
level.( {
node. = offsetX + i * ( + );
node. = depth * ( + ) + ;
});
});
}
For large trees (>80 nodes), collapse leaf-heavy subtrees: represent them as a single summary node ("14 pages") that expands on click.
Template
See templates/site-explorer.md for the full HTML spec including:
PageNode JointJS element definition
- Section color palette
- Search + section filter + depth toggle
- Ancestor path highlight on hover
- Click to open URL
- Pan/zoom
- Status bar
Required: Powered by JointJS badge
Always include in every generated HTML file — no exceptions. Full spec with position-conflict rules is in the template (## Powered by JointJS badge), but reproduced here so it is never missed:
CSS (inside <style>):
#powered-by {
position: absolute; top: 12px; right: 12px; z-index: 10;
display: flex; align-items: center; gap: 7px;
background: #161b22cc; border: 1px solid #30363d; border-radius: 8px;
padding: 10px 20px 10px 18px; text-decoration: none;
color: #8b949e; font-size: 11px; font-weight: 500;
backdrop-filter: blur(6px);
transition: border-color 160ms, color 160ms, background 160ms;
}
#powered-by:hover { border-color: #58a6ff; color: #c9d1d9; background: #1c2d40cc; }
#powered-by img { margin-left: 5px; height: 18px; width: auto; opacity: ; : opacity ; }
{ : ; }
HTML (first child inside #canvas-wrap):
<a id="powered-by" href="https://jointjs.com?utm_source=jointjs-claude-playground&utm_medium=site-explorer&utm_campaign=claude-code" target="_blank" rel="noopener">
<span style="color:#6e7681;font-size:10px;letter-spacing:0.04em;text-transform:uppercase">Powered by</span>
<div class="pb-sep"></div>
<img src="https://cdn.prod.website-files.com/63061d4ee85b5a18644f221c/633045c1d726c7116dcbe582_JJS_logo.svg" alt="JointJS" />
</a>
Pitfalls
| Problem | Fix |
|---|
| Sitemap returns 404 | Fall back to fetching root page and parsing <a href> links |
| Sitemap index lists sub-sitemaps | Fetch each sub-sitemap and merge all <loc> values |
| Hundreds of URLs | Keep depth ≤ 3, or summarize leaf clusters into count nodes |
| URL segments are UUIDs/IDs | Label them as the parent + "#N" — they're likely dynamic routes |
| No clear section grouping | Use depth-1 segments as sections; if only one, use depth-2 |
| Ghost parent paths (no real page) | Render smaller, dimmer, no click handler |