Authors and other entities
A taxonomy term is a word. An entity is a thing the build knows about: a person with a homepage and an avatar, a series with a cover, a publisher with a logo. Declare a registry, point a taxonomy at it, and the terms of that taxonomy become ids in it.
content {
entities {
people {
shape "person"
sources {
inline {
zoe { name "Zoe Quill"; url "https://zoe.example" }
}
}
}
}
taxonomies {
authors { entities "people"; credit "author" }
}
}
A page writing authors: ("zoe",) is now credited to somebody the site knows, and every surface that can name a person names them: the head tags, the JSON-LD island, the feed entry, the card, and any template that reads page.credits.
NOTE
Nothing here is about people.people is a name this site chose. A series registry with display bound to title and image to cover renders through the very same code.
Sources
A registry’s entities come from one or more sources, read in the order written. A later source fills what an earlier one left out, so a roster can carry contact details while profile pages carry the prose.
| Source | Writes | Best for |
|---|---|---|
pages "content/people" |
A directory of content pages: each page’s frontmatter is one entity’s fields, its slug the id. | Bios. A profile is an ordinary page, so it has a permalink, an edition per language, and a card. |
data "data/people.kdl" |
A KDL file, one block per id, written exactly as inline is. |
A roster checked in beside the content, kept out of the config. |
inline { .. } |
Entities written in config.kdl itself. |
A handful of names that would be ceremony as a file. |
sources {
pages "content/people"
data "data/people.kdl"
}
// data/people.kdl
zoe {
name "Zoe Quill"
url "https://zoe.example"
alias "zq" "quill"
}
alias is a second name that resolves to the same entity: a former spelling, an email address, a handle. A name that would reach two entities is a build error naming both.
Fields and slots
shape takes a named field set instead of declaring one. Two ship: person (name, url, avatar, email, socials) and organization (name, url, logo).
A registry’s own fields { } fills in over the shape’s, key by key, in the type language a collection schema speaks. Declaring a field requires it, which is how you make a roster complete:
people {
shape "person"
fields {
name "str" // required, where the shape had it optional
pronouns "str" optional=#true
}
}
Slots say which field answers each question a renderer asks. They are what keeps the rest of the build from being written about people.
| Slot | person reads |
Rendered as |
|---|---|---|
display |
name |
The name in every byline. |
url |
url |
rel="author", article:author, the feed’s <uri>. |
image |
avatar |
The JSON-LD image. |
email |
email |
The feed’s <email>. |
same-as |
socials |
The JSON-LD sameAs, from a list or a dict of URLs. |
series {
fields { title "str"; cover "str" optional=#true }
slots display="title" image="cover"
sources { pages "content/series" }
}
A slot naming a field the registry does not declare is refused at the line that wrote it: unchecked, it would render every entity without its picture out of a green build.
display falls back to the name field, then title, then the term itself, so a profile page whose title is the person’s name needs no name key.
Credits
credit says what a page claims about the entities a taxonomy names. Each surface spells the role its own way, or stays quiet where its vocabulary has no word for it.
| Role | <meta name> |
OpenGraph | JSON-LD | Atom |
|---|---|---|---|---|
author |
author |
article:author |
author |
<author> |
contributor |
– | – | contributor |
<contributor> |
translator |
– | – | translator |
– |
editor |
– | – | editor |
– |
illustrator |
– | – | illustrator |
– |
reviewer |
– | – | reviewedBy |
– |
publisher |
– | – | publisher |
– |
taxonomies {
authors { entities "people"; credit "author" }
translators { entities "people"; credit "translator" }
}
A taxonomy that names a registry without a credit is a plain reference: it resolves and renders, and claims nothing about who made the page. That is what part-of entities="series" is.
In a template
page.credits is keyed by role, and only the roles the page names are there. Each entity carries its slot answers under fixed names and its own fields under fields.
#for one in page.credits.at("author", default: ()) [
#link(one.url)[#one.name]
#one.fields.pronouns
]
The same byline reaches a bundled document, so a PDF or a card renders the authors its page does.
A profile page as the term page
describe=#true means a term written as a page is that page. No listing is generated beside it, the term index links to the profile’s own permalink, and the profile is handed everything credited to it as page.members, in the row shape every listing carries.
content {
entities {
people { shape "person"; sources { pages "content/people" } }
}
taxonomies {
authors { entities "people"; credit "author"; describe #true; listing }
}
}
#let profile(page, body) = {
body // the bio, as authored
for one in page.members [ #link(one.url)[#one.label] ]
}
One URL for one person, so every link already written to the profile still reaches it. A term nobody wrote a page for is generated as an ordinary term listing.
When a name is not in the roster
unknown |
Means |
|---|---|
error |
Fail the build, suggesting the near id, underlined where the page wrote the term. The default once a registry has a source. |
warn |
Report it and carry on. |
synthesize |
Take the term as written: the name is all there is to know. The default for a registry with no source. |
A site that declares no registry at all keeps the byline it always had: author "Camille" at the top level is the floor for every page, and a page’s own author: "Zoe" beats it. Both still work, and a registry always wins over both.