Skip to content

tags.json reference

src/content/tags.json holds the site’s section tags: one site-wide list of tags, and which sections carry each one. A section can carry several tags, and a tag can be on sections across any number of pages. Viewers never see tag names; tags are how editors group related sections.

Like nav.json, it is a sidecar, deliberately not a key in index.json or in a section file, and it is optional: a site without one simply has no tags. The reasons are the same as nav’s, covered in Why a sidecar.

{
"tags": [
{ "id": "tag_3f2a…", "label": "Logo usage", "sectionIds": ["logo-hero", "clearspace"] },
{ "id": "tag_91c0…", "label": "Colour", "sectionIds": [] }
]
}

tags is the registry, in the order the editor lists it.

FieldTypeDefaultNotes
idstring—Required, non-empty, unique. The tag’s stable identity — renaming the tag does not change it.
labelstring""Display name. May be blank; the UI then shows “Untitled tag”.
sectionIdsstring[][]The sections carrying this tag: ids from index.json’s sections. Order is not meaningful.

Each tag owns its assignments, so deleting a tag removes it from every section by construction. A tag with no sections is still a registry entry, not an error.

Entries are parsed one at a time, and only a missing or empty id makes an entry invalid. A bad label reads as blank, and a sectionIds value that is not an array of non-empty strings is repaired: non-string and empty ids are dropped. An invalid entry is dropped on its own, and the read is marked degraded with a warning. A degraded file is read-only (see Writing it), which is why everything except id repairs instead of rejecting.

Source of truth: TagsFileSchema in packages/primitives/src/schemas/tags.ts.

tags.json is repaired against the current index on every read by reconcileTags() (packages/primitives/src/lib/tags.ts), which is total, pure, and idempotent:

  • An assignment to a section that no longer exists is dropped. This is how deleting a section removes its tags; nothing has to edit tags.json at delete time.
  • A section id listed twice in one tag is kept once.
  • A duplicate tag id keeps only the first tag.
  • A tag with no usable id is dropped.

Read tags through siteTags(index), which always reconciles. pnpm exec authoring validate reports a file that does not parse, including one with a tag that has no id, as an error. It reports assignments to sections that no longer exist, and duplicate tag ids, as warnings.

Writes follow nav.json’s rules: they are wholesale replacements.

  • Leave tags out → tags.json is untouched.
  • Send tags → the file is replaced, after reconciling against the index being written. tags: [] deletes every tag on a site that has the file. On a site that has never had one, an empty list creates no file.
  • A payload that is not a valid tag list is declined, not applied: the rest of the write lands, the file is left as it was, and the response carries a warning.
  • If the file on the branch holds entries this package version cannot parse (written by a newer version, or corrupt), it is read-only: the write leaves it alone and warns, rather than replacing it with the subset this version understood.

On the wire, tags travel as a tags key on the posted index, alongside nav. The write path lifts the key off before the index is validated, so it can never reach index.json.

  • Tagging a section: every section’s chrome has a Tags pill next to the audience pill. It lists the registry as checkboxes, with a field for creating a tag inline. Typing the name of an existing tag reuses that tag.
  • Managing the registry: Site Settings → Tags adds, renames, and deletes tags. Adding a name that is already in use is refused, and a rename that collides is flagged. Deleting a tag that sections carry asks for confirmation first; the sections themselves stay. An unused tag is deleted straight away.

Container children cannot be tagged; tag the container.

When two editors are working at once, a save that has to rebase onto a peer’s commit merges tags rather than replacing them. Your unsaved tag and untag actions, creates, renames, and deletes are replayed onto the tags the peer saved. A tag the peer deleted stays deleted.

If an editing session never loaded the registry (local state left by an editor build that predates tags), tag editing stays off until a reload reads the sidecar. Editing an unloaded registry would replace the real file.

Reads that return the index (get_site_index, get_site_content) carry the registry as index.tags; compare_branches reports tagsChanged when the draft’s tags.json differs from main’s. save_sections writes it two ways, both wholesale under the rules above:

  • a top-level tags param — the full registry on its own, combinable with indexOps or sections, so tagging never needs a full index;
  • a tags key on a posted full index.

Sending both is refused. Omit both and tags.json is untouched: indexOps never write tags, and the index the connector reads for itself is posted without them. Build the list from a fresh get_site_index read, and never from an incomplete read — one that returned missing, navDegraded: true or tagsDegraded: true. tagsDegraded means the file holds entries the connector can’t read, so a tags write is declined (with a warning) anyway. The index the connector reads for itself, when a save omits index, is posted without nav as well as without tags.

save_sections also checks every reference section in the payload against the tags and index the write leaves behind, and fails with [validation] (writing nothing) when a Reference names a tag that does not exist, picks a section that is not top-level on a page, picks another Reference, or picks itself. That is stricter than the viewer on purpose: the agent has just chosen those ids, so a bad one is a mistake, not a deleted source. To tag sections and reference the tag in one step, create the tag in tags in the same call. validate_site and pnpm exec authoring validate run the same check over committed content and report what they find as warnings, since the site still renders.

  • nav.json reference — the sidebar sidecar this file’s rules are modelled on.
  • Content model — index.json, per-section files, and how content is assembled.