Permalinks
Link to the docs from your product by permalink, check those links in the product’s CI, and keep them working when a section moves, gets a second name, or goes away.
For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.
A heading marked as a permalink, @id {permalink=true}: weave-config, gets an address of its own, go/weave-config under the base path, which sends the reader to the heading wherever it is now. A page with permalink: options in its frontmatter gets go/options, which leads to the top of the page. Link to that address from outside the docs, such as from your product’s interface or its error messages, and the link keeps working when the page moves.
This guide keeps a product’s links to the docs working: linking to permalinks from the product, checking those links in the product’s CI, and what to do in the docs when a section moves, gets a second name, or goes away. The site publishes each address: with Astro, @ascribed/astro does.
In VS Code, the sidebar’s This page lists the permalinks a page declares, its own and its headings’, and each opens its line.
Link from your product
A product links to a permalink’s address: the site, the permalinks’ path, and the name, ending the way the site’s routes do. It can add what it knows about its reader, as parameters named for the content model’s dimensions:
// The docs' permalinks, as `[consumer]` publishes them.
const permalinks = "https://docs.example.com/docs/go/";
function docsLink(name: string, reader: Record<string, string> = {}): string {
const url = new URL(name, permalinks);
for (const [dimension, value] of Object.entries(reader)) url.searchParams.set(dimension, value);
return url.href;
}
// https://docs.example.com/docs/go/weave-config?pm=pnpm
docsLink("weave-config", { pm: "pnpm" });
A parameter that names a dimension, with one of its values, sets the reader’s choice, as if they’d picked the tab: this link opens the section with the pnpm tab showing. Anything else is ignored, so send what the product knows, without checking what the docs accept (the parameters).
What an agent gets
An agent given a permalink, by ascribe --help, ascribe explain, or a diagnostic’s link, gets the section it names in one fetch:
- Its section as Markdown, at the address with
.mdin place of its end,go/weave-config.md, under the section’s title. A host that answers a page asked for withAccept: text/markdownwith its Markdown finds it for the address too, and the forwarding page names it,<link rel="alternate" type="text/markdown">, beside the JSON entry a product fetches. - A
301to the section, withhost = "netlify", since an agent’s fetcher doesn’t follow the forwarding page (permalinks on Netlify). A retired permalink’s address answers410.
Check the links in the product’s CI
A product that links to the docs can check, in its own CI, that every name it links to is still a permalink there, without building or even having the docs. The docs commit their list of permalinks, ascribe.permalinks, which ascribe permalinks sync writes beside ascribe.toml (The permalinks list). Then ascribe check in the docs warns when a permalink is removed, and the product checks its names against a copy of the list:
# .github/workflows/docs-links.yml, in the product's repository
name: Docs links
on: [pull_request]
jobs:
docs-links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The docs' list, from their repository; or keep a copy in this one.
- run: curl -fsSL -o docs.permalinks https://raw.githubusercontent.com/example/docs/main/ascribe.permalinks
- run: npx @ascribed/cli permalinks verify --against docs.permalinks --from docs-links.txt
docs-links.txt lists the names the product links to, one on each line. ascribe permalinks verify exits with 1, naming each name the list doesn’t have, and each the docs retired, with the reason they gave, so a link to a section the docs removed, or never had, fails the product’s pull request rather than reaching its users. An alias passes, as the permalink it leads to does.
In the docs, ascribe check reports a list that’s behind the content (permalinks-list-behind): a new permalink, or one that moved. Run ascribe permalinks sync, or take the editor’s quick fix, and commit the list with the change; ascribe permalinks sync --check fails in CI when it wasn’t. Moving a permalink to another page changes one line of the list and removes nothing. Deleting one is a warning (permalink-lost), which sync doesn’t clear: restore the name, or record what happened to it (when a section moves or goes away).
When a section moves or goes away
A permalink is a promise to whatever links to it, so what to do when its section changes depends on what happened to the section. What happened to a name no heading declares any more is recorded in ascribe.addresses, keyed go: and the name, with the rest of where the docs’ addresses went:
- It moved, to another page or under another heading: move the
@idwith it, and runascribe permalinks sync. The address leads to the new place, and nothing else changes. - It has a second name, such as one an older release of the product prints: move that name to the permalink,
ascribe addresses move go:config go:weave-config. It’s an alias now: its address leads where the permalink’s does, andverifyaccepts both names. - It’s gone, merged into another section or removed with the feature it described: move it to where its readers should go now, or retire it, with a reason a reader can understand.
ascribe permalinks retire rotate-keys --to guides/security.md#keys
ascribe permalinks retire legacy-sync --reason "The sync agent was removed in 4.0"
ascribe permalinks retire writes each to ascribe.addresses, "go:rotate-keys" = "guides/security.md#keys" in [moved] with --to, and "go:legacy-sync" = "…" in [retired] with --reason, and records it in ascribe.permalinks, so ascribe check no longer warns. A moved name’s address leads to its new place, and verify accepts it. A retired name’s address is a page that says the section was removed, with the reason, and a product’s verify fails on the name, with the reason, so the product changes its link. In the editor, the quick fix on permalink-lost writes a retired entry with a placeholder reason, which is an error until you replace it. To retire every permalink on a page you’re about to delete, run it with --page before deleting the page, or with --under for every page in a folder.
Retire rather than delete the line from the list: a deleted line makes the address a 404 with nothing to say why, and a product’s verify says only that the name is missing.
See also
- Permalinks, in the directive reference: how a heading or a page gets one, and the names it may have.
- Show the docs in your product: a permalink’s section, from the build’s
_ascribe/help.json, in a tooltip or beside an error. ascribe.addresses: the moved and retired names, with the rest of where the docs’ addresses went.ascribe permalinks:sync,list,verify, andretire, their options, and their output.- The permalinks list: the file a product’s CI checks against.
- Permalinks, in the
@ascribed/astroreference: the page at each address, its parameters, andMissingPermalinkfor the site’s 404 page.