Docs Writing
Tags
Public tags, the archive routes they generate, English labels for a Chinese blog, and the internal tags that change how a post renders.
Tags come from Ghost and split into two kinds. Ghost calls them public and internal; the difference is one character in the editor and a large difference in what happens.
Public tags
An ordinary tag. It groups posts and gets its own archive.
Each public tag that carries at least one post gets a route at
/blog/tag/[slug], appears in the directory at /blog/tags, and shows on post
rows and cards. An empty tag gets nothing — it is filtered out of the directory,
the home page rail, and the archive routes, so a tag created and never used never
404s from a stale link.
Labels
The blog runs in Chinese, and the tag name is used as-is there. Elsewhere — the English home page, agent-facing Markdown — a label is resolved in this order:
- The tag’s meta title
- The tag’s OG title
- The slug, title-cased (
design-systems→Design Systems)
So a tag named 设计系统 reads correctly on the Chinese blog and shows
Design Systems on the English home page if you set its meta title. Otherwise the
slug is the fallback, which is why slugs are worth choosing deliberately.
Internal tags
In the Ghost editor, a tag whose name starts with # is internal. Ghost stores
it with a hash- slug prefix — #no-toc becomes hash-no-toc — and marks its
visibility internal.
Internal tags never surface to a reader. They are filtered out of the tag
directory, the home page rail, and archive routes entirely: requesting
/blog/tag/hash-no-toc is a 404, not an empty archive. They exist to flip
behaviour on a post.
#no-toc
Suppresses the table of contents on a post — both the desktop rail and the section menu in the reading topbar.
A post gets a table of contents when it has two or more h2/h3 headings
and is not tagged #no-toc. The tag is the author’s override for a post that
is technically long enough but reads worse chopped into sections.
The reading topbar itself still shows; it just carries the post title with no section menu behind it.
#not-by-ai
Historical. The human-authorship pledge is now the default at the foot of
every post, and is replaced only when a post carries an
[!authors] directive. The tag no longer gates
anything, and does not need to be applied to new posts.
Adding an internal tag
Two places, and they must agree:
const hasNoTocTag = post.tags.some(
(tag) => tag.slug === 'hash-no-toc' || tag.name === '#no-toc',
);
Both forms are checked because the slug is what the Ghost Content API returns and the name is what a human typed. Match on either and a tag renamed in the editor does not silently stop working.
Nothing else is required — internal tags are already excluded from every reader-facing surface by visibility, so a new one cannot leak into the directory.
Notes
- Tag visibility is read from the Ghost Content API and normalised in
src/features/posts/adapter/ghost/dataset.ts. - Directory and archive filtering:
src/features/posts/server/content.ts. - Label resolution:
src/features/posts/display.ts.