Internal linking
Build and validate a frontmatter-authored internal-link graph with contentbit links.
Internal links are content metadata, not renderer behavior. Author each page's place in the graph in frontmatter, let contentbit derive backlinks, and fail CI when a slug drifts after a rename.
---
slug: beginner-pizza-dough
linksTo:
- cold-fermentation-pizza
- detroit-pizza-dough
aliases:
- intro-pizza-dough
keywords:
primary: how to make pizza dough
secondary: [easy pizza dough recipe, homemade pizza dough]
---Only slug, linksTo, aliases, and keywords belong in source files.
linkedFrom is derived and written only to .contentbit/link-index.json.
Build the index
Point links at the same content glob you validate:
contentbit links "content/**/*.md"The command reads frontmatter, resolves aliases, derives backlinks, writes
.contentbit/link-index.json, and prints a summary:
12 page(s), 34 link(s), 1 orphan(s): 0 errors, 1 warnings
index written to /site/.contentbit/link-index.jsonThe JSON artifact is intentionally small and stable, so an LLM agent or pipeline can load it before writing a page:
{
"pages": [
{
"slug": "beginner-pizza-dough",
"path": "/repo/content/beginner.md",
"title": "Beginner Pizza Dough",
"linksTo": ["cold-fermentation-pizza"],
"linkedFrom": ["pizza-flour-guide"],
"aliases": ["intro-pizza-dough"],
"keywords": {
"primary": "how to make pizza dough",
"secondary": ["easy pizza dough recipe"]
}
}
],
"aliases": {
"intro-pizza-dough": "beginner-pizza-dough"
}
}Validate links
contentbit validate runs the cross-file link checks automatically whenever any
matched file declares a slug:
contentbit validate "content/**/*.md" --registry ./blocks/registry.tsUse the full content glob for linked documents. Validating only one linked file does not give the checker the rest of the graph.
The link diagnostics use the same file:line:col severity CODE message shape as
block validation:
CB_LINK_UNRESOLVEDfor alinksTotarget that is not a slug or alias.CB_SLUG_DUPLICATEfor two pages with the sameslug.CB_ALIAS_CONFLICTfor aliases that collide with another alias or real slug.CB_LINK_LOCALE_MISSINGfor a target that exists in another locale but not the current locale.CB_LINK_CROSS_LOCALEfor an explicit object target that intentionally points across locales.CB_LINK_SELFfor a page linking to itself.CB_LINK_ORPHANfor a page with no inbound links.
Multilingual projects
Use the resolver mode that matches your URL model:
# Same translated slug in every locale: /en/blog/espresso and /fr/blog/espresso
contentbit links "content/**/*.md" --link-resolve same-locale-slug
# Translated slugs with stable content keys:
contentbit links "content/**/*.md" --link-resolve same-locale-key
# Prefer stable keys, but allow same-locale slug links for simple projects:
contentbit links "content/**/*.md" --link-resolve prefer-same-locale-key-fallback-slugFor translated slugs, author a stable key and a locale-specific slug:
---
locale: fr
slug: regler-espresso
key: espresso-dial-in
linksTo:
- espresso-grinder-settings
---The matching English page can use the same key with its own slug:
---
locale: en
slug: dialing-in-espresso
key: espresso-dial-in
linksTo:
- espresso-grinder-settings
---The generated index keeps locale, slug, and key together so an app can build URLs
with its own route helper, such as /{locale}/blog/{slug}. To point across
locales intentionally, use an object target:
linksTo:
- locale: en
slug: dialing-in-espressoThat emits a warning instead of silently hiding the cross-locale edge.
Heal renames
When a page changes slug, keep the old slug in aliases on the destination page:
---
slug: cold-fermentation-pizza
aliases:
- overnight-pizza-dough
---Then let --fix rewrite stale outbound references:
contentbit links "content/**/*.md" --fix--fix is deliberately narrow. It only rewrites values inside linksTo that
point at known aliases. It does not remove aliases, invent related pages, change
body Markdown, or write linkedFrom to source files.
Agent workflow
contentbit agents teaches installed skills and AGENTS.md to use the link
graph when it exists:
- Run
contentbit links <content glob>before writing to read current slugs, aliases, keywords, and backlinks. - Pick
linksTovalues from existing slugs; never guess a slug from a title. - For new linked pages, include
keywords.primaryandkeywords.secondarywith search-intent phrases that help future LLM agents choose related pages. - Run
contentbit links <content glob> --fixafter edits when aliases are present. - Run
contentbit validate <content glob> --registry <path>until clean.
For projects without generated LLM-agent files, add the same loop to your own skill or rules file. The CLI stays the source of truth.