Status: Active
Effective Date: 2026-02-11
Applies To: CLI, HTTP API, CmsService
Define one canonical contract for content identity so every ingress path behaves the same.
In v1, contentId is slug-backed:
- Canonical
contentId= canonicalslug contentIdis immutable for a given slug lineage
A slug is canonicalized with:
- Unicode normalization:
NFKC - Trim surrounding whitespace
- Lowercase
After canonicalization, it must satisfy:
- Length:
1..64 - Pattern:
^[a-z0-9]+(?:-[a-z0-9]+)*$ - Reserved values forbidden
Reserved slugs:
.,..admin,api,assets,chunksdraft,new,publishedrefs,root
Allowed content kinds:
articlespublishedcomments
Any other kind is rejected.
Because slugs are canonicalized before use, values that normalize to the same slug are the same identity.
Examples:
Hello-World->hello-worldhello-world->hello-world
Both target the same refs and same logical content lineage.
During snapshot save:
- If no
trailers.contentId/trailers.contentidis provided, system writes canonical slug as trailercontentid. - If
trailers.contentIdortrailers.contentidis provided, it must canonicalize to the same value asslug. - Mismatches are rejected.
Explicit rename is not implemented as a first-class operation yet.
Operationally in v1:
- A new slug creates/targets a different identity.
- Moving history between slugs is out of scope for this policy and will be addressed with state-machine work (
M1.2+).
Validation failures return CmsValidationError with:
error: human-readable messagecode: machine-friendly code (slug_invalid_format,slug_reserved, etc.)field: offending field (slug,contentId,kind, ...)
HTTP API maps validation failures to 400.