Writing and embedding content
For administrators. Every Text field in Kartotek is normally written in Markdown, extended with a small set of Kartotek-specific {{...}} references that pull in other content from the same Post, or link to another Post entirely. This doc covers native Markdown, the addressing model those references point into (Role, Rank, and Reference), the reference syntax itself, and the editor tooling around both. See Welcome to Kartotek for the vocabulary this doc assumes, and Posts and publishing for the Post/Version/ContentElement model this content lives inside.
Content Types
Every piece of content on a Post — beyond the running Markdown text itself — is one of these fixed Content Types, each with its own Content Type reference page covering its fields, how it's edited, and what metadata it carries:
- Image
- Text (markdown)
- Link
- Contact
- Video
- Audio
- Attachment (a downloadable file)
- Geospatial
- Timeline
(This list is expected to keep growing. Every Content Type — old and new — participates in the same immutability, Role, Rank, and Reference model described below; that's not special to any particular one.)
Every Content Element also has its own permanent address, domain/_element/<kind>/<id>, showing just that one element's own public metadata outside any Post's page layout — gated exactly like visiting its owning Post directly (a Private Post's elements aren't reachable at all; a PrePub/Hidden Post's elements show the same gated notice). This standalone page resolves in the same one effective language as everything else — an explicit ?lang=, a visitor's stored preference, or the Domain's own default, in that order — with the same field-by-field fallback described in Languages and translations: a translated description, caption, or label shows when it exists, and only a genuinely missing one falls back to the original.
Roles, Rank, and Reference
Each ContentElement (see Posts and publishing) has:
- A Role (e.g.
cover,intro,footnote) — a classification, not a unique-per-element name: it's purely structural, and it's the only thing a Post Type's Display Schema looks at when filling a Slot (see Posts and publishing's "Post Types"). For a single Slot, exactly one ContentElement of a given Role is "current" at a time — editing replaces it with a new one of the same Role, and "the current cover" always means whichevercover-Role element the latest Version points to. For a repeatable Slot, many elements can share the same Role at once, distinguished by Rank (below) — that's how, for example, several footnotes all exist as separate elements sharing the Rolefootnote. See "Roles: naming and validation" below for the rules Role follows. - A Rank (integer) — orders elements that share a Role, and is how an individual one among them is addressed by Role alone (e.g.
{{footnote:3}}means "thefootnote-Role element with Rank 3" — see "Content references" below). Rank is scoped to a single Role, kept 1-based and contiguous (1, 2, 3, … with no gaps) — a Role's Nth element always has Rank N. The admin editor exposes this as a "Position" field per element, editable directly: changing it reorders that element among its same-Role siblings only, and every sibling is renumbered to close the gap it left and make room at its new spot. Elements of other Roles (even the same Content Type) are untouched by this, since they're a separate 1..x sequence of their own. - A Reference (optional, free text) — a user-chosen, human-readable name used only for inline addressing from markdown, independent of Role and Rank. See "Reference: inline addressing" below.
Roles: naming and validation
A Role is free text — anything typed into a ContentElement's Role field is accepted, with nothing enforcing what it can be. That flexibility is what lets a new Post Type introduce whatever Roles it needs (booking, route-start, program-history, …) without a fixed list to extend first. The cost is that a typo doesn't error — it silently creates a ContentElement under an orphaned Role that no Slot is looking for (a quietly stored, never-rendered element).
Role's only job is Slot-matching — deciding where, if anywhere, a ContentElement gets rendered by the Post's current Post Type. Naming an element for inline {{...}} addressing is a separate concern — see "Reference: inline addressing," next.
Allowed characters: a Role follows the same character rule as a UID (see Domains, UIDs and addresses): lowercase letters, numbers, and hyphens only — no spaces, capitals, underscores, or punctuation. This matters beyond consistency: a Role is also usable directly inside {{role}} / {{role:rank}} (see "Content references," below), and a narrow, predictable character set is what keeps that parse unambiguous.
Reserved wildcard names: a Slot whose Role is just its Content Type's name, pluralized (images, texts, links, contacts, videos, audios, attachments, geospatials, timelines), is already special-cased as a generic "any Role of this kind" wildcard. These nine names are therefore reserved — a genuinely-named Role can't reuse one, since that would make it ambiguous whether a ContentElement was meant as a real named Role or was just falling into the wildcard bucket.
Surfacing typos without a fixed enum: since Slots match ContentElements by Role name, a mistyped Role fails silently rather than loudly. Two things close that gap without turning Role into a closed list: the admin editor's Role field offers autocomplete drawn from the current Post Type's standard Roles plus every Role already used elsewhere on the Post, but still accepts free text; and on save, if a ContentElement's Role matches no Slot in the Post's current Display Schema, the editor shows a plain, non-blocking notice instead of silent acceptance. No fixed, closed list of Role names is introduced by this — that would undercut the extensibility the whole Role/Post Type mechanism depends on.
Reference: inline addressing
Reference is a separate, optional field on every ContentElement, alongside Role and Rank — a free-text, user-written name whose only purpose is to be pointed at from a {{...}} inline reference in markdown. It plays no part in Slot matching or Display Schemas; a ContentElement's Reference has no effect on whether or where a Post Type renders it — that's Role's job alone.
Reference exists because Role and Rank, while sufficient to address any element ({{footnote:3}}), aren't always the most natural way for someone writing markdown to think about what they're pointing at. Reference lets an element be given a memorable name instead — e.g. an image with Role gallery and Rank 3 could also be given the Reference team-photo, addressable as {{team-photo}} rather than {{gallery:3}}. Both forms keep working side by side; Reference is an addition, not a replacement for Role/Rank addressing.
Same character rule as Role and UID. A Reference must be unique within a Post — unlike Role, which relies on Rank to disambiguate duplicates, Reference has no Rank equivalent, so exactly one ContentElement on a Post can claim a given Reference at a time.
Because both a Role (for a single Slot) and a Reference can be addressed by a bare {{name}}, Reference is checked first; Role is the fallback — a {{name}} that matches a Reference always resolves to that specific, deliberately-named element, even if some other element's Role happens to share the same text. If no Reference matches, resolution falls through to Role exactly as before.
HTML mode is a different storage format for the same block
A Text block can instead be stored as HTML rather than Markdown — an explicit, trusted mode for content that's already HTML and shouldn't be reinterpreted as Markdown, mainly useful for legacy content brought in from elsewhere. Everything below this section describes Markdown-mode Text specifically; an HTML-mode block skips all of it — its stored content is rendered exactly as written, with no Markdown conversion applied. The one exception: Kartotek's {{...}} content references (below) still resolve inside an HTML-mode block the same way they do in Markdown, since reference resolution happens before either format is rendered. Switching a Text block's Format to HTML in the admin editor shows an Active content notice right there, listing any external domain(s) the block's own markup references, plus an Embed height (px) field controlling the sandboxed iframe's height on the live site — see Text's "Safe rendering of HTML blocks" for what visitors actually see.
Native Markdown
Kartotek's Markdown editor supports standard Markdown, rendered the same way whether you write it directly (source mode) or use the rich-text toolbar.
Paragraphs and line breaks
A blank line between two lines of text starts a new paragraph. A single line break inside a paragraph is treated as a space, not a line break — end a line with two spaces, or use a blank line, to force one.
Headings
# Heading 1 through ###### Heading 6 (a # per level, then a space). The rich-text toolbar's H button toggles a Heading 2 specifically; other levels are only reachable by typing the ## markdown directly (source mode, or typed inline in rich-text — the editor recognizes it as you type).
Emphasis and strong emphasis
*italic* or _italic_ → italic. **bold** or __bold__ → bold. The toolbar's I and B buttons toggle these for the current selection.
Lists
Unordered: a line starting with -, *, or + followed by a space. Ordered: a line starting with 1., 2., etc. The toolbar's List button toggles a bulleted list for the current line(s); numbered lists are only reachable by typing 1. directly.
Links
[link text](https://example.com). The toolbar's Link button prompts for a URL and applies it to the current selection (or the word under the cursor); leaving the prompt empty removes an existing link.
Images
Native Markdown images () aren't how Kartotek handles media — an Image is its own Content Element, uploaded or fetched through the admin's media fields (see Media and the Media Archive) and placed inline with {{role}} or {{kind:reference}} (see below), not written as a raw Markdown image tag in a Text field.
Blockquotes
A line starting with > renders as a blockquote.
Inline code and fenced code blocks
Inline: text wrapped in single backticks, `like this`. A code span can use more than one backtick as its delimiter (``like this`` `) if the content itself needs to contain a single backtick — the closing delimiter must be the same number of backticks as the opening one.
Fenced: a line of three or more backticks (```) or tildes (~~~), then the code, then a matching closing fence line. An optional info string after the opening fence (e.g. ```markdown) is cosmetic only.
Kartotek's {{...}} references and the {{{{ escape are never interpreted inside either form — see "Where Kartotek references are (and aren't) interpreted," below. This is the one Kartotek-specific behavior that affects native code syntax, and it's exactly what makes it possible to write about the reference syntax itself in a Text field without triggering it.
Horizontal rules
A line containing only three or more -, *, or _ characters.
Tables
Kartotek's editor supports Markdown tables (pipe-delimited rows, a --- separator row under the header). Column alignment (:---, :---:, ---:) is supported. Tables can be edited directly in the rich-text view (row/column structure is a real editor feature, not just raw text) or written by hand in source mode.
Escaping native Markdown characters
A backslash before a Markdown special character (\*, \_, \[, etc.) makes it literal, per standard Markdown — this is unrelated to Kartotek's own {{{{ escape (below), which exists specifically because a backslash doesn't survive round-tripping through the rich-text editor reliably (see "Why {{{{ and not \{{," below).
Source vs. rich-text mode
The editor's </> toggle switches between the WYSIWYG rich-text view and a plain-text source view of the exact same underlying Markdown — both edit the same document, and switching back and forth never loses content. A few things are only practical in one mode: numbered lists and heading levels other than H2 are easiest to type directly as Markdown (source mode, or typed inline in rich-text); everything else works identically in both.
Kartotek's content references
Inside a Text field, {{...}} pulls in another Content Element from the same Post, or links to another Post. Every form is documented below with its exact syntax, what it resolves to, where it's valid, what happens when it can't resolve, and whether it's interpreted inside code.
{{role}}
Inline-embeds the Image, Video, or Audio Content Element currently filling a given Role (or matching that text as a Reference — see "Role versus Reference," below; Reference is checked first). Only these three Content Types resolve this way — a Link, Contact, Geospatial, or Timeline element isn't reachable through a bare {{role}}.
- Resolves to: the element's native embed — an
<img>,<video controls>, or<audio controls>tag for an uploaded/fetched original, or the raw embed snippet for an embedded one (e.g. a live chart). - Valid: any Text field on the same Post.
- Unresolved: if no element currently matches that Role (or Reference) — including an element that exists but isn't currently eligible for public view — the reference simply shows nothing to a visitor; it's never left as literal
{{role}}text, and never replaced with an error. Viewing the same Post in Admin Preview instead shows a small inline flag naming the unresolved reference, so an admin notices and can fix it. - Example:
{{cover}}embeds the Post's currentcover-Role image.
{{kind:reference}}
Selects one exact Content Element by its stable Reference, regardless of Role — the form to use when the author means "this particular element," not "whichever element currently fills this Role." kind is the Content Type, lowercase (image, video, audio, or link); reference is the element's own Reference field.
- Resolves to: the same embed forms as
{{role}}forimage/video/audio; forlink, a styled inline<a>using the Link's label (or the reference text itself, if no label is set). - Valid: any Text field on the same Post. The named element must actually carry that Reference — Role is not consulted at all for this form. (
linkis the one exception for backward compatibility:{{link:x}}checks Reference first, then falls back to checking Role, matching this project's existing{{link:role}}behavior from before this form existed.) - Unresolved: no element of that kind carries that Reference (or it does, but isn't currently eligible for public view) → shows nothing to a visitor, and flags inline in Admin Preview — same as
{{role}}, above. - Interpreted inside code: never — see below.
- Toolbar: the Kartotek insertion menu's "Insert exact Content Element by Reference" opens a picker listing every attached element with a Reference, showing its Content Type, Reference, Role (where set), and a short preview — pick one to insert the exact form.
- Example:
{{image:team-photo}}selects the Image whose Reference is exactlyteam-photo, even if another Image also has Rolegallery.
A few more existing kind-prefixed forms, distinct from {{kind:reference}} above (they select by something other than Reference alone):
{{footnote:3}}— the Text element with Rolefootnoteand Rank3, rendered as a numbered footnote reference that links down to a footnote list appended to the end of the rendered content.{{map:route-start}}— an embedded, interactive map (OpenStreetMap tiles, no account or key) for the Geospatial element with Roleroute-start, rendering its point/line/polygon/heatmap data.{{timeline:role}}(or{{timeline:reference}}, Reference checked first — same precedence as{{link:role}}) — inserts the complete Timeline: its optional Title, then every Event with its formatted time, Label, and fully rendered Markdown Description, in Event order. An Event's own Description may itself use{{role}}/{{kind:role}}to reference/embed a Link or Image from the same Post — those nested references resolve too. See Timeline.{{contact:role}}(or{{contact:reference}}, Reference checked first) — inserts the Contact's every entry (Kind icon, linked handle, and a Description shown parenthetically whenever that entry's Kind repeats within the same card).
Each of these four resolves — and fails to resolve — the same way {{role}}/{{kind:reference}} do above: no matching element shows nothing to a visitor and flags inline in Admin Preview, never raw {{...}} text.
{{post:uid}}
An internal cross-reference to another Post, anywhere on the site.
- Resolves to: a styled inline link using the target Post's current title — resolved fresh every time the page is viewed, so a later title change is reflected automatically, without editing the referencing Text.
- Valid: any Text field, on any Post.
- Unresolved: if the uid doesn't match any Post, it renders as plain, unstyled text (the uid itself) — never a broken link.
- Hidden/Private targets: a Hidden target's title still shows (clicking through shows the normal "this content has been hidden" message); a Private target reveals nothing at all — it resolves exactly as if the uid didn't exist, consistent with Private meaning "the owner deliberately never made this public."
- Toolbar: the Kartotek insertion menu's "Post" opens a picker searchable by uid or title.
- Example:
{{post:inside-the-stargate-program}}.
{{{{ — literal {{
Writing about this syntax (like this very document does) needs a way to produce a literal {{ without it being interpreted. Typing four opening braces renders as two literal ones: {{{{ → {{. Only the opening delimiter has an escape — a lone }} has no special meaning on its own, escaped or not.
{{{{role}}renders as the literal text{{role}}— never as a live reference. The escape is recognized first, before any reference form is considered, so the trailingrole}}is just ordinary text once the leading{{{{has been consumed.- Interpreted inside code: never — see below.
- Toolbar: the Kartotek insertion menu's "Literal
{{" inserts the four-character escape at the cursor.
Why {{{{ and not \{{
A backslash escape (\{{) was considered, but the rich-text editor's Markdown round-trip doesn't preserve it reliably: a backslash before a brace can be silently dropped when the document is serialized back to Markdown, meaning content that looked correctly escaped when saved could quietly turn into a live reference the next time it's loaded. {{{{ uses only ordinary characters the editor already round-trips exactly, with no special handling required.
Where Kartotek references are (and aren't) interpreted
Every form above — {{role}}, {{kind:reference}}, {{post:uid}}, and the {{{{ escape — is left completely untouched inside inline code and fenced code blocks. This is what makes it possible to write documentation (like this doc) that shows the raw syntax without it resolving:
`{{cover}}`renders as literal inline code reading{{cover}}, not an embedded image.- A fenced block containing
{{cover}}renders as a literal code block, not an embedded image.
Malformed or unmatched brace sequences (an opening {{ with no closing }}, or the reverse) are always preserved exactly as typed — never silently rewritten or dropped.
Role versus Reference
These are two different, non-interchangeable ways to address the same element, and Kartotek's editor, picker, and this doc are all deliberately consistent about which word means which:
- Role is semantic. It describes the function an element fulfills in the Post — "the lead image," "the primary map." The Post Type's schema defines or permits which Roles it uses, and its renderer resolves a Role against whichever Content Element currently carries it. Use a Role when your editorial intent is "the element fulfilling this function," whatever that happens to be right now.
- Reference is identity. It selects one exact Content Element, by a name the author chose when creating it, independent of whatever Role that element has. Use a Reference when your editorial intent is "this exact element" — regardless of its Role, and regardless of whether some other element later takes over that Role.
Role is not an alias for Reference, Rank, or a database ID. Reference is not the Content Element's Role, Rank, filename, or caption. The admin editor's Role picker shows a Role's name, its Post Type meaning, and whichever Content Element currently resolves it (if any); its Reference picker lists attached Content Elements by Content Type, stable Reference, Role (where set), and a short preview — so choosing between them is always a choice between "the current occupant of this slot" and "this specific thing."
A side-by-side example
An Article has two images: one with Role cover and Reference launch-photo, another with Role gallery and Reference team-photo.
- Writing
{{cover}}always shows whichever image is currently the cover — if the cover is later replaced with a different image (same Role, new element), this reference picks up the new one automatically, with no edit needed to the Text. - Writing
{{image:launch-photo}}always shows that specific image — even if it stops being the cover later (its Role changes, or a different image takes overcover), this reference keeps pointing at the same element.
Practical inline-content guidance
An inline reference places an existing Content Element in the editorial flow of a Text field — it doesn't duplicate it, and it doesn't remove the element from wherever else it's configured to appear.
- Placing a specific image between two paragraphs: write the reference on its own line, between the surrounding paragraphs of Markdown —
{{team-photo}}on its own line renders the image as its own block there. - Through a Role, when the Post Type supplies the semantic slot: if a Gallery's
introtext writes{{cover}}and the Post's cover image is later swapped for a different one, the inline reference shows the new image automatically — it was never tied to the specific element, only to the Role. - By stable Reference, when several elements share a Content Type or Role: if three images all have Role
gallery, only a Reference ({{image:team-photo}}) unambiguously picks one of them inline;{{gallery}}alone only resolves cleanly when there's exactly one current element for that Role. - Referencing another Post:
{{post:some-other-uid}}anywhere in a Text field, resolved at view time (see{{post:uid}}, above). - Showing reference syntax literally in prose or technical documentation: use the
{{{{escape, or wrap the syntax in inline code (`{{role}}`) — either reliably prevents it from resolving. - Appearing in more than one place: an inline reference doesn't remove or hide the element from its own enabled end-of-Post content group (e.g. a gallery grid, an "Images" section) — the same Content Element can be shown inline and in its normal group, if the Post Type's Display Schema includes both.
Syntax quick reference
| Syntax | Resolves to | Unresolved behavior |
|---|---|---|
{{role}} |
Current Image/Video/Audio for that Role (or Reference) | Nothing publicly; flagged in Admin Preview |
{{kind:reference}} |
The exact element with that Reference | Nothing publicly; flagged in Admin Preview |
{{footnote:N}} |
Numbered footnote reference + list entry | Nothing publicly; flagged in Admin Preview |
{{link:role}} |
Styled inline link | Nothing publicly; flagged in Admin Preview |
{{map:role}} |
Embedded interactive map | Nothing publicly; flagged in Admin Preview |
{{timeline:role}} |
Complete Timeline (Title + Events) | Nothing publicly; flagged in Admin Preview |
{{contact:role}} |
Contact's entries | Nothing publicly; flagged in Admin Preview |
{{post:uid}} |
Link to another Post's current title | Plain text (the uid) |
{{{{ |
Literal {{ |
— |
Examples
The mission briefing is summarized below.
{{briefing-video}}
See {{gate-diagram}} for the address sequence, and read more about the
program on {{post:inside-the-stargate-program}}.
Escaped syntax, for documentation: {{{{role}}
Renders as: an embedded video, a paragraph with an inline map/image reference and a link to another Post by its current title, and a final line showing the literal text {{role}}.
Troubleshooting
- A reference (other than
{{post:uid}}) doesn't show anything. The Role, Reference, or identifier doesn't currently match an eligible element on this Post — check for a typo, that the target element actually exists, or (for Image/Video/Audio/Attachment) that it's currently eligible for public view. This is deliberate: an unresolved reference is never publicly shown as raw{{...}}syntax or replaced with an error — it's simply omitted, the same as a Post authored with nothing in that spot. Viewing the same Post in Admin Preview shows a small inline flag naming the unresolved reference instead, so it doesn't disappear silently while you're editing. {{{{didn't produce a literal{{. Confirm all four opening braces are actually present and contiguous — three braces plus a real reference ({{{role}}) is a different, unescaped case (see "Malformed... brace sequences," above) and resolves as an ordinary{{role}}reference with one leftover literal brace before it.- A reference inside a code block or inline code resolved anyway. This shouldn't happen — every form is protected inside both. If it does, check that the code span/fence is actually well-formed Markdown (a code span needs matching backtick counts on both sides; a fence needs three or more of the same character on both the opening and closing lines).
- The same element appears twice on the page. Expected if it's both referenced inline and shown in its own content group (e.g. a gallery grid) — see "Appearing in more than one place," above. Referencing the same element inline more than once is also not deduplicated; each occurrence renders its own copy.
- Switching between rich-text and source mode changed something unexpectedly. Both modes edit the same underlying Markdown — if something looks different, it's usually because the rich-text renderer normalizes equivalent Markdown differently than how it was typed (e.g.
*vs_for emphasis), not a content change.{{...}}references and the{{{{escape round-trip exactly in both modes. - A Role field's typo doesn't error, and the content just doesn't show up. Expected — see "Roles: naming and validation," above. Check the admin editor's non-blocking save notice for a Role matching no Slot.
The Kartotek insertion toolbar
Every Markdown field has a Kartotek menu in its toolbar, next to the native formatting buttons, with four actions:
- Insert by Role — a searchable picker of this Post's available Roles (for Image/Video/Audio), each showing its Content Type and whichever element currently resolves it.
- Insert exact Content Element by Reference — a searchable picker of every attached Content Element carrying a Reference, showing its Content Type, Reference, Role, and a short preview.
- Post — a searchable picker of every Post on the site, by uid or title.
- Literal
{{— inserts the{{{{escape directly.
When editing a field with no Post in scope (the admin Docs/Type reference view), the Role/Reference/Post pickers fall back to a plain prompt for a manual value instead — the syntax is still valid to type, there's just no list to choose from. A small Markdown help (ⓘ) icon beside the toolbar shows a compact summary of all of this on hover, keyboard focus, or click/tap, with a link back to this doc.