pseo-linking
Skill54 starsChanged 15 days agoMITView on GitHub
Build an intelligent internal linking system using hub-and-spoke structures, related page suggestions, breadcrumb navigation, and topical clustering for programmatic SEO. Use when implementing or improving internal linking, adding breadcrumbs, building category hub pages, or creating related-pages components.
- Reads credentials
- Sends data out
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- karrolcia/hippocampusedited, first seen Mar 2, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- karrolcia/hippocampus★ 1378% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository17 other files it gives its agents.
- gscSkill
- pseo-auditSkill
- pseo-dataSkill
- pseo-discoverySkill
- pseo-llm-visibilitySkill
- pseo-metadataSkill
- pseo-orchestrateSkill
- pseo-performanceSkill
- pseo-quality-guardSkill
- pseo-scaleSkill
- pseo-schemaSkill
- pseo-templatesSkill
- seo-content-refreshSkill
- seo-i18nSkill
- seo-log-analysisSkill
- seo-migrationSkill
- seo-regression-guardSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 10 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈10
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 29
Every page reachable: No orphan pages - every page is linked from at least 2 other pages
- Distinctive: line 88
Use the page's H1 or title as anchor text
- Distinctive: line 113
Add persistent navigation that links to:
- Distinctive: line 114
All category hub pages
- Very specific: line 236
Call it from the publish/update/delete job, not on every build.
- Very specific: line 241
If facets don't need to be indexed, block them in robots.txt (for example disallow: /*?*color=) or use URL fragments.
- Very specific: line 242
If facets must be indexable: use the standard & separator, keep path-encoded filter order fixed with no duplicates, and return 404 for …
- Distinctive: line 253
All redirects must be permanent (301 or 308)
- Distinctive: line 254
When a slug changes, automatically add the old URL to the redirect map
- Distinctive: line 255
When a category is renamed, redirect all child URLs under the old category path
- Very specific: line 259
Next.js pattern: put pattern rules in redirects() and per-URL entries in a proxy.ts lookup (map or DB).
- Distinctive: line 282
All redirect sources must not match any current live slug
- Distinctive: line 283
All redirect destinations must resolve to a live page
- Distinctive: line 288
Create or update robots.txt to:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
pSEO Internal Linking
Scope parameter
| Value | Sections |
breadcrumbs | 1 |
related | 2, 4 |
hubs | 3, 5 |
sitemap | 6 (including pushing URLs) |
facets | 6, "Faceted Navigation, Empty Combinations and Internal Search" |
redirects | 7 |
robots | 8, 9 |
all | everything |
Build an internal linking system that creates strong topical authority, distributes link equity, and ensures every page is discoverable.
Core Principles
- Hub-and-spoke architecture: Category hubs link to all child pages; child pages link back to their hub
- Topical clustering: Pages within the same topic cluster interlink
- Every page reachable: No orphan pages - every page is linked from at least 2 other pages
- Crawl depth <= 3: Any page reachable within 3 clicks from the homepage
- Descriptive anchor text: Link text describes the target page, not generic "click here"
Architecture
Homepage
├── Category Hub A ──→ Page A1, A2, A3, ...
│ ├── Page A1 ──→ Hub A, A2, A3 (related)
│ ├── Page A2 ──→ Hub A, A1, A3 (related)
│ └── Page A3 ──→ Hub A, A1, A2 (related)
├── Category Hub B ──→ Page B1, B2, B3, ...
│ └── ...
└── Sitemap (links all hubs)Implementation Steps
1. Breadcrumb Navigation
Build a breadcrumb component that:
- Renders the full path: Home > Category > Current Page
- Uses semantic HTML (
<nav>with<ol>) - Generates BreadcrumbList schema markup (coordinate with pseo-schema)
- Appears on every pSEO page
interface BreadcrumbItem {
label: string;
href: string;
}
function Breadcrumbs({ items }: { items: BreadcrumbItem[] }) {
return (
<nav aria-label="Breadcrumb">
<ol>
{items.map((item, i) => (
<li key={item.href}>
{i < items.length - 1 ? (
<a href={item.href}>{item.label}</a>
) : (
<span aria-current="page">{item.label}</span>
)}
</li>
))}
</ol>
</nav>
);
}2. Related Pages Component
Build a component that displays links to related pages:
- Pull related pages from the data layer's
getRelatedPages(slug, limit)function - Show 3-6 related pages per page
- Prioritize pages in the same category, then adjacent categories
- Use the page's H1 or title as anchor text
- Include a brief description or differentiator for each link
Relatedness signals (in priority order):
- Same category
- Shared tags or attributes
- Similar content type
- Alphabetically adjacent (fallback)
3. Hub Pages (Category Landing Pages)
Hub page creation is owned by pseo-templates (section 7). From a linking perspective, ensure every hub page:
- Links to all child pages in the category (or paginates if >100)
- Is linked from the homepage or global navigation
- Optional: CollectionPage or ItemList semantic markup (no rich result; coordinate with pseo-schema)
4. Cross-Category Linking
Identify and implement cross-category linking opportunities:
- "See also" sections linking to related pages in other categories
- Comparison pages that bridge categories
- Tag-based linking (pages sharing the same tag link to each other)
5. Footer/Sidebar Navigation
Add persistent navigation that links to:
- All category hub pages
- Most popular or cornerstone content pages
- This ensures hubs are reachable from every page (crawl depth = 2)
6. XML Sitemap
Generate a dynamic sitemap that:
- Includes only indexable, canonical, 200-status pSEO pages
- Groups URLs by category using a sitemap index
- Updates automatically when pages are added
- Splits files at 50,000 URLs or 50MB uncompressed per file (Google's limits). Search Console accepts up to 500 sitemap index files per site, and an index itself holds up to 50,000 entries.
What Google reads (build-sitemap doc, updated 2026-07):
<changefreq>and<priority>are ignored. Do not generate them.<lastmod>is used only if it is consistently and verifiably accurate. Set it to the last significant update (main content, structured data, links), not the build time and notnew Date(). Store a real content-modified timestamp per page (lastModifiedonPageIndex, see pseo-data) and use that. If you can't supply a truthful date, omitlastmod.
// app/sitemap.ts (Next.js App Router, up to ~50,000 URLs)
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const categories = await getAllCategories();
const entries: MetadataRoute.Sitemap = [];
for (const cat of categories) {
entries.push({
url: `${baseUrl}/${cat.slug}`,
lastModified: cat.lastModified, // real content timestamp, never new Date()
});
// Iterate ALL pages with a cursor. Do not call a list function with a default limit.
let cursor: string | null = null;
do {
const batch = await getPagesCursor(cat.slug, cursor, 5000); // { items: PageIndex[], next: string | null }
for (const page of batch.items) {
entries.push({
url: `${baseUrl}${page.canonicalPath}`,
lastModified: page.lastModified, // PageIndex field (see pseo-data); a real content timestamp
});
}
cursor = batch.next;
} while (cursor);
}
return entries;
}Beyond 50,000 URLs: generateSitemaps plus a hand-written index. generateSitemaps serves each chunk at /sitemap/[id].xml, but Next.js does not create a sitemap index. Write one yourself and submit that:
// app/sitemap.ts
import type { MetadataRoute } from "next";
const CHUNK = 50000;
export async function generateSitemaps() {
const total = await countPublishedPages();
return Array.from({ length: Math.ceil(total / CHUNK) }, (_, id) => ({ id }));
}
export default async function sitemap(props: {
id: Promise<string>;
}): Promise<MetadataRoute.Sitemap> {
const id = Number(await props.id); // id is a Promise<string> in Next.js 16
const pages = await getPublishedPagesChunk(id * CHUNK, CHUNK);
return pages.map((p) => ({
url: `${baseUrl}${p.canonicalPath}`,
lastModified: p.lastModified,
}));
}// app/sitemap-index.xml/route.ts
export async function GET() {
const total = await countPublishedPages();
const count = Math.ceil(total / 50000);
const items = Array.from(
{ length: count },
(_, i) => ` <sitemap><loc>${baseUrl}/sitemap/${i}.xml</loc></sitemap>`
).join("\n");
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${items}
</sitemapindex>`;
return new Response(xml, { headers: { "Content-Type": "application/xml" } });
}Reference the index in robots.txt (Sitemap: https://example.com/sitemap-index.xml) and submit it in Search Console.
Pushing URLs to search engines:
- IndexNow notifies Bing, Yandex, Naver, Seznam, Yep and Amazon. Google does not use IndexNow, so it does not speed up Google indexing.
- Bing: also submit the sitemap index in Bing Webmaster Tools (IndexNow is additive, not a replacement).
- Google's Indexing API is only for JobPosting and BroadcastEvent-in-VideoObject pages. Default quotas: 200 publish requests/day (URL_UPDATED plus URL_DELETED), 180 metadata requests/min, 380 requests/min total. The old quota-increase form was removed on 2026-10-16. Using it for other pSEO pages is against its terms and can lose API access.
- For Google, rely on sitemaps, internal links and accurate
lastmod. The URL Inspection API is for monitoring, not submission (quota 2,000/day and 600/min per site).
IndexNow setup:
- Generate a key (8-128 chars, a-z, A-Z, 0-9, hyphen) and serve it at
https://example.com/{key}.txtwith the key as the file body (static file inpublic/or a route handler). - POST changed URLs on publish, update and delete, up to 10,000 URLs per request, all on the same host as the key file.
const INDEXNOW_KEY = process.env.INDEXNOW_KEY!;
export async function pingIndexNow(urls: string[]) {
for (let i = 0; i < urls.length; i += 10000) {
const res = await fetch("https://api.indexnow.org/indexnow", {
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({
host: new URL(baseUrl).host,
key: INDEXNOW_KEY,
keyLocation: `${baseUrl}/${INDEXNOW_KEY}.txt`,
urlList: urls.slice(i, i + 10000),
}),
});
if (!res.ok && res.status !== 202) console.error("IndexNow failed", res.status);
}
}Call it from the publish/update/delete job, not on every build. Only send URLs that actually changed.
Faceted Navigation, Empty Combinations and Internal Search
Linking to filter or combination URLs creates crawl waste fast. Follow Google's faceted navigation guidance:
- If facets don't need to be indexed, block them in robots.txt (for example
disallow: /*?*color=) or use URL fragments.rel=canonicalandnofollowalone are less effective long term. - If facets must be indexable: use the standard
&separator, keep path-encoded filter order fixed with no duplicates, and return 404 for empty combinations, duplicate filters and nonsensical combos at the URL itself (not a redirect to a generic error page). - Nonexistent pagination URLs return 404. Don't link to combinations that have no results.
- Internal site-search URLs (
/search?q=...) are infinite and low value: block them in robots.txt (Disallow: /search), do not link to them from templates, and never put them in sitemaps.
Next.js 16 caveat: a streaming response starts with status 200. If notFound() runs after streaming has begun (inside a Suspense boundary or after loading.tsx renders), the response is 200 with an injected noindex, which Google may report as a soft 404. Check existence before any Suspense fallback renders, or return the 404 from proxy.ts (the Next.js 16 name for middleware.ts).
7. Redirect Management
At 1000+ pages, slug and URL changes are inevitable. Build a redirect system:
- Maintain a redirect map (JSON file, database table, or framework config) that maps old paths to new paths
- All redirects must be permanent (301 or 308)
- When a slug changes, automatically add the old URL to the redirect map
- When a category is renamed, redirect all child URLs under the old category path
- Implement a redirect config (or
proxy.tsin Next.js 16, which replacedmiddleware.ts) that processes the map on every request. Prefer pattern-based redirects over one rule per URL: on Vercel, redirects and rewrites count toward the 2,048-routes-per-deployment limit - Validate: no redirect chains (A → B → C), no redirect loops, no redirects to 404s
Next.js pattern: put pattern rules in redirects() and per-URL entries in a proxy.ts lookup (map or DB). Vercel allows 2,048 routes per deployment, so one rule per URL does not scale.
// next.config.js - pattern rules only
async redirects() {
return [
{ source: '/old-category/:slug', destination: '/new-category/:slug', permanent: true },
];
}// proxy.ts - per-URL entries from a map or DB lookup
import { NextRequest, NextResponse } from 'next/server';
export async function proxy(request: NextRequest) {
const target = await lookupRedirect(request.nextUrl.pathname); // map, KV or DB
if (target) {
return NextResponse.redirect(new URL(target, request.url), 308);
}
return NextResponse.next();
}Build-time validation:
- All redirect sources must not match any current live slug
- All redirect destinations must resolve to a live page
- Log warnings for redirects older than 12 months (consider removing)
8. robots.txt and AI Crawler Access
Create or update robots.txt to:
- Allow all pSEO pages for traditional crawlers (Googlebot and others)
- Allow AI search/retrieval crawlers needed for LLM citation (OAI-SearchBot, PerplexityBot, Claude-SearchBot). ChatGPT-User, Perplexity-User and Claude-User are user-triggered fetchers; their robots.txt handling varies by vendor
- Optionally block AI training crawlers (GPTBot, ClaudeBot, Google-Extended, CCBot). Google-Extended does not affect Google Search, AI Overviews or AI Mode
- Reference the sitemap (and llms.txt if you publish one)
- Block utility/admin pages
Critical: Blocking an AI search crawler removes you from that product. Google ignores crawl-delay, and robots.txt is limited to 500 KiB. See pseo-llm-visibility for full AI crawler configuration.
9. llms.txt
llms.txt creation is owned by pseo-llm-visibility (section 2). From a linking perspective, ensure llms.txt references all category hub pages and the sitemap. The robots.txt should include a comment pointing to llms.txt.
Link Health Checks
Verify:
- No orphan pages (every page linked from at least 2 others)
- Hub pages link to all their child pages
- Child pages link back to their hub
- Breadcrumbs are present on every page
- Related pages section shows 3-6 links
- No broken internal links (all hrefs resolve to 200)
- Sitemap includes all indexable public pages, has no
changefreq/priority, andlastmodis a real content-modified date - Each sitemap file is under 50,000 URLs and 50MB; a sitemap index is written and submitted
- Empty facet/combination URLs return a real 404 (not 200 + noindex)
- Internal search URLs are blocked in robots.txt, unlinked and absent from sitemaps
- Crawl depth from homepage <= 3 for any page
- Redirect map exists and covers all historical URL changes
- No redirect chains or loops
- All redirect destinations resolve to live pages
- robots.txt allows AI retrieval crawlers (OAI-SearchBot, PerplexityBot, Claude-SearchBot)
- (Optional) llms.txt exists at site root with category hubs and page type descriptions
Verify
Build, start the app, and run seo-regression-guard against localhost (npx -y tsx <seo-regression-guard-dir>/scripts/seo-check.ts --site http://localhost:3000 ...). Zero errors for sitemap, robots, redirect and hub/related templates before calling this done.
Relationship to Other Skills
- Depends on: pseo-data (category data, related page queries)
- Provides to: pseo-schema (breadcrumb items for BreadcrumbList schema)
- Works with: pseo-templates (linking components are rendered in templates)
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

