Page identity and aliases

A page’s URL changes when you move or rename it. Identity gives a page two things that do not: a uid, minted once when the page is created, and an optional alias – the human label the page is known by, such as a document number. Both are claims: once minted, a claim belongs to that page and is never reused.

Declare identity on a model

# models/decision.yaml
uid:
  required: true          # every decision page carries a uid
alias:
  grammar: "d{number}"    # the label's shape, in the filename-pattern language
  scope: global           # or container: unique among siblings only
  renumber: retire        # or never; there is no way to free a label
  allocation: local       # how a new number is claimed
fields:
  supersedes:
    type: reference
    model: decision       # resolved against aliases and uids at index time
  • grammar uses the same segment language as filename: patterns ({number}, {alpha}, {field:name}, literals). Current and retired aliases must match it.
  • scope is where uniqueness holds: global across the site, or container among pages sharing a parent URL.
  • renumber: retire lets a page change its alias by keeping the old label in its aliases list, where it stays claimed and still resolves. never forbids the change. No label is ever freed, so an external reference to a retired label keeps working forever.
  • allocation: local claims the next free number with a create-only file write; accent validate is the gate that catches a race. allocation: ref claims a create-only ref refs/claims/<model>/<alias> locally and on the shared remote, so two writers who share the remote cannot claim one label: the second push is rejected and that writer takes the next label. It needs a repository with an origin remote and a network at filing time, and there is deliberately no fallback to local.

Moves and old URLs

A model may declare grammar: path: the page’s URL is its alias. Moving such a page through the admin, the CLI, or the API appends the old URL to aliases automatically (renumber: never refuses the move instead), and a retired URL keeps answering – accent serve redirects it to the page, and accent build emits a redirect stub at the old URL, so a static host needs nothing. The same applies to any retired alias written as a URL path.

What a page carries

---
uid: 0192a6f4-3c2e-7d5b-9e1a-4b8c2d7f0a11
alias: d042
aliases: [d007]     # retired; still claimed, still resolves
title: Use the new queue
supersedes: d019
---

You never write uid by hand. Every creation path mints it through the model when the model declares identity: accent new, the admin editor, the content API, agents, and MCP. The docs-promotion tooling mints a fresh uid for each copied page, because a copy is a new page, not the same claim in two places.

Reference fields

A field of type: reference with a model: target holds an alias, a retired alias, or a uid, and the content index resolves it to a page. accent validate reports a reference that resolves to nothing, or to a page of the wrong model, as an error. A reference written as origin/alias names a page from another site; when it is not imported, validate reports a warning, not an error. A reference field without a model: target keeps its old behaviour: a plain string that is never resolved.

Resolving in templates

identity_index() maps every claim – uid, current alias, retired alias – to its page’s URL in one pass:

{% set ids = identity_index() %}
<a href="{{ ids["d042"] | url }}">Decision 42</a>

A current claim outranks a retired one, so a renumbered page’s old label links to wherever the label’s holder lives now.

What accent validate reports

FindingSeverity
Two pages claim the same uiderror
The model requires a uid and the page has noneerror
Two pages claim the same alias, current or retired, in one scopeerror
An alias does not match the model’s grammarerror
Retired aliases on a model declaring renumber: nevererror
A reference that resolves to nothing, or to the wrong modelerror
An origin/alias reference to a page that is not importedwarning

Versioned docs are handled: a fallback copy of a page shares its original’s source file, and claims are compared by source file, so a copy is never a duplicate claim.