> For the complete documentation index, see [llms.txt](https://ascribed-dev.com/docs/llms.txt). This documentation describes Ascribe 0.4.4. Anything marked in main, not yet released isn't in that release yet.

# Permalinks

> How-to · 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.

A heading marked as a [permalink](https://ascribed-dev.com/docs/writing/directives.md#permalinks), `@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](https://ascribed-dev.com/docs/glossary.md#permalink) 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](https://ascribed-dev.com/docs/astro/reference.md#permalinks).

In VS Code, the sidebar's [This page](https://ascribed-dev.com/docs/editor/sidebar.md#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](https://ascribed-dev.com/docs/glossary.md#content-model)'s [dimensions](https://ascribed-dev.com/docs/content-model/variation.md#11-dimensionsname):

```ts
// 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](https://ascribed-dev.com/docs/glossary.md#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](https://ascribed-dev.com/docs/astro/reference.md#permalinks)).

## What an agent gets

Available: Ascribe (in main, not yet released, 0.4.5)

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 `.md` in place of its end, `go/weave-config.md`, under the section's title. A host that answers a page asked for with `Accept: text/markdown` with 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](https://ascribed-dev.com/docs/permalinks/in-your-product.md#fetch-your-lines-entry).
- **A `301` to the section,** with `host = "netlify"`, since an agent's fetcher doesn't follow the forwarding page ([permalinks on Netlify](https://ascribed-dev.com/docs/publishing/redirects.md#permalinks-on-netlify)). A retired permalink's address answers `410`.

## 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](https://ascribed-dev.com/docs/contracts/permalinks-list.md)). Then `ascribe check` in the docs warns when a permalink is removed, and the product checks its names against a copy of the list:

```yaml
# .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`](https://ascribed-dev.com/docs/permalinks/command.md#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](https://ascribed-dev.com/docs/permalinks.md#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`](https://ascribed-dev.com/docs/publishing/redirects.md#ascribeaddresses), 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 `@id` with it, and run `ascribe 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, and `verify` accepts 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.

```sh
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`](https://ascribed-dev.com/docs/permalinks/command.md#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](https://ascribed-dev.com/docs/writing/directives.md#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](https://ascribed-dev.com/docs/permalinks/in-your-product.md): a permalink's section, from the build's `_ascribe/help.json`, in a tooltip or beside an error.
- [`ascribe.addresses`](https://ascribed-dev.com/docs/publishing/redirects.md#ascribeaddresses): the moved and retired names, with the rest of where the docs' addresses went.
- [`ascribe permalinks`](https://ascribed-dev.com/docs/permalinks/command.md): `sync`, `list`, `verify`, and `retire`, their options, and their output.
- [The permalinks list](https://ascribed-dev.com/docs/contracts/permalinks-list.md): the file a product's CI checks against.
- [Permalinks](https://ascribed-dev.com/docs/astro/reference.md#permalinks), in the `@ascribed/astro` reference: the page at each address, its parameters, and [`MissingPermalink`](https://ascribed-dev.com/docs/astro/reference.md#a-permalink-that-isnt-here) for the site's 404 page.
