Posts and publishing
For administrators. How a Post, its Versions, and its Stage work — the model everything else in Kartotek builds on. See Welcome to Kartotek first if Post/Category/Stage are unfamiliar terms. Everyday tasks come first; "How Kartotek preserves content," further down, is reference material you don't need to read to publish your first Post. Content structure — Roles, Rank, Reference, and Kartotek's {{...}} inline syntax — is its own doc, Writing and embedding content; Post/Content translation is Languages and translations; Link and page archival is Links and web preservation.
Everyday: creating and editing a Post
Creating or editing a Post is one form: pick (or change) its Post Type (Article is the general-purpose choice — see "Post Types," below, for the rest), give it a title and UID, assign it to a Category, and fill in whichever content sections apply — Texts, Images, Videos, Audio, Attachments, Links, Geospatials, Timelines, Contacts (see Writing and embedding content for how these are addressed and referenced).
Save Draft vs. Publish update vs. Stage — three different things:
- Save Draft privately stages the form's current state on the server without publishing anything or touching what's publicly visible. One Draft per Post; each save overwrites the last. There's no separate autosave — Save Draft is an explicit action.
- Publish update is what actually creates a new Version of the Post — a real, permanent snapshot. This is the one action that changes the Post's real content history.
- Stage (PrePub, Published, Hidden, or Private) is separate from both — it controls visibility, not content. A brand-new Post starts in PrePub: it publishes and previews exactly like a normal page at its normal address, but only you can see it, until Stage is explicitly moved to Published.
Editing just one field, or switching a Post's Type, never loses anything else already on the Post — publishing carries forward everything you didn't touch (see "How Kartotek preserves content," below, for why).
Common tasks, from a Post's own admin page:
- Publish an update — edit the form, click Publish update.
- Preview before going public — open the Post's own live address while it's still in PrePub; only you can see it there.
- Go live — move Stage to Published.
- Schedule a future publish — while in PrePub, set an optional PublishAt date/time; the Post moves to Published automatically once that passes.
- Correct a past mistake without hiding the whole Post — mark just that one past Version Hidden (with an optional Reason), leaving the Post's current live Version untouched.
- Take a Post down without deleting it — move Stage to Hidden (with an optional Reason, shown publicly), or detach it from its Category so it stops appearing in feeds while its direct address still works.
- Rename its address — change its UID; the old one keeps working as a redirect-free alias (see Domains, UIDs and addresses).
Common mistakes:
- Expecting Save Draft to publish anything — it doesn't; only Publish update creates a real Version.
- Forgetting a new Post starts in PrePub — if a Post "isn't showing up," check Stage first.
- Assuming Hidden deletes a Version — it doesn't; Hidden Versions still exist and are still visible to you as an admin, just excluded from what a public visitor sees.
What visitors see
A Post's Full page is Post-Type-specific in its actual content, but every Full view shares the same shell: title, a small byline (Published date, Updated date, a History link to the Metadata page), and content. See Explore a Kartotek site for the full visitor-facing picture, and each Post Type's own reference page for what a Brief listing card and Full page show for that Type specifically.
How Kartotek preserves content
This section explains why nothing you publish is ever silently lost — the model underneath the everyday workflow above.
A Post is made of three layered concepts: the Post itself (a stable identity), its Versions (a history of published states), and ContentElements (the actual material — text, images, and so on — that Versions reference). This split is what lets a Post be edited over time without losing its history, and what lets switching a Post's Type bring old content back automatically instead of losing it.
Post — identity, not content
A Post is an identity, not a container of content. What it directly is:
- Internal ID — never changes, never shown publicly.
- UID (+ historical aliases) — the short, URL-friendly name used to address the Post directly at
domain/<uid>. Shared with Categories in a single namespace, and renaming one leaves a working alias behind — see Domains, UIDs and addresses. - Category — zero or one. A Post is normally assigned a Category, but can later be detached from it — see "Removing a Post from view" below. See Organizing and presenting content for how hierarchy and display work for a Post that does have one.
A Post's Post Type is not one of these stable fields — see Version below for why.
Version — a published snapshot
A Version is created only when an edit is explicitly published — not automatically while it's being drafted. There's no separate autosave/draft feature for a Post's own content: the form being edited in the admin is the de facto draft, and clicking Publish update is the explicit action that turns it into a new Version.
Each Version has:
- A timestamp.
- A Post Type — this is where Post Type actually lives (see "Post Types" below). A Post's "current Post Type" is simply whatever its latest Version says; Post Type can change from one Version to the next.
- A set of Slot references — for each named Slot the Version's Post Type declares (its Display Schema), one or more references to ContentElements (below) that fill that Slot.
- A public / hidden flag, settable independently per Version, default public. Hiding a past Version (e.g. it contained a mistake) doesn't affect the Post's current live content or any other Version — it only affects how that specific Version appears when someone browses history.
The Post's live page always shows its latest Version. Past Versions are never deleted — see Domains, UIDs and addresses for how a specific one is addressed and browsed.
A Post, once created, is never deleted. Its content and history persist indefinitely — what can change is its visibility, through the two controls described next, not its existence. This is a content-persistence guarantee, separate from (and in addition to) the UID-persistence guarantee in Domains, UIDs and addresses — both exist so that a link to a Kartotek site, once shared, keeps working (or, deliberately, shows a clear "this has been hidden" message instead of silently 404ing).
Removing a Post from view
There is no "delete." Instead, two independent controls manage how visible a Post is:
Detaching its Category. A Post can be removed from its Category, leaving it with none. A Post with no Category can't be found through the site's normal navigation — it won't appear on any Category's page — but its UID still resolves directly if someone visits or links to it, showing its normal content, unless its Stage says otherwise (below).
Stage. Every Post has a Stage, one of four values:
- PrePub — the Post renders exactly like a normal live page, at its normal address, but only to a logged-in admin; a public visitor gets a "this hasn't been published yet" message instead. Save Draft and Publish both work completely normally while a Post is in this Stage — publishing while in PrePub still creates a real, permanent Version, it just doesn't go public until the Stage itself changes. A newly created Post starts in PrePub, not Published — making it public is a separate, explicit step. Optionally, a PublishAt date/time can be set while in PrePub: once that moment passes, the Post automatically transitions to Published using whatever its latest Version happens to be at the time. Manually publishing before then clears the scheduled PublishAt automatically.
- Published — fully public. Existing Posts (from before Stage existed) are all Published, and any Post an admin explicitly promotes stays Published until it's explicitly moved elsewhere.
- Hidden — removed from public view, typically after having been Published, though that's not required — a Post can move directly to Hidden from any other Stage. Independent of the per-Version Hidden flag described below. An optional Reason can be given, shown publicly as part of the "this content has been hidden" message (it's visitor-facing text, not a private note).
- Private — deliberately retained by you, with nothing shown publicly at all. Kartotek is archival-first (see Welcome to Kartotek): a Post you're keeping around but don't intend to make public — an imported draft you decided not to publish, a note to yourself — belongs here rather than being deleted or left sitting in PrePub indefinitely. Private is meaningfully stronger than PrePub or Hidden: those two both show a gated "not available" notice acknowledging the Post exists; a Private Post gives a public visitor nothing at all — the same response as a UID that was never used, not even in cross-post
{{post:uid}}references (see Writing and embedding content). Private is admin-only in every sense: absent from Category feeds, navigation, search, analytics, and every export (static PDF, static JSON, JSON-LD). Reachable from any Stage, including an already-Published Post — moving a previously-Published Post to Private is a deliberate action that erases the public record it was ever Published (unlike Hidden, which preserves that history); use Hidden instead if that's not what you mean to do.
A Post in PrePub, Hidden, or Private Stage is excluded from its Category's public feed. Published Posts show normally. Every Stage is reachable from every other Stage, with no precondition — a Post can go straight to Hidden without ever having been Published, or from Published straight to Private. A backward move within the ordinary PrePub → Published → Hidden sequence (e.g. Published back to PrePub, or Hidden back to Published) asks for confirmation first, since reversing a rare, deliberate action like Hidden deserves a deliberate pause too. Moving into or out of Private never asks for confirmation — it's meant to be as low-friction as toggling a draft, not a rare or destructive action, though moving an already-Published Post to Private is worth pausing on yourself even without a prompt, given it erases that Post's public history.
PrePub ──────── Published ──────── Hidden ↕ ↕ Private ─────────────────────────────┘ (every arrow works in both directions, from any Stage to any other)Separately, and independently of the Post's own Stage, a specific past Version can also be marked Hidden (with its own optional Reason) — excluded from the public Version History list, showing the same "this has been hidden" message when its own URL is visited directly. This means a Published Post can still have an individual old Version marked Hidden (e.g. "this one had an error") while the Post overall stays live at its current Version, unaffected.
An admin viewing gated content (PrePub, Hidden, Private, or a Hidden Version) themselves always sees the real thing, with an indication of the current Stage, so it can be reviewed or moved to a different Stage.
These two controls are independent and can be combined: a Post can be attached to a Category and Hidden, detached and Published, and so on.
Both are also editable straight from the Posts list (/_admin/posts) and from a Category's own Content inventory (see Organizing and presenting content), without opening the Post itself: each row shows a Category dropdown, a Stage dropdown, and a small OK button that applies whichever of the two was changed, together, in one step — the same backward-move confirmation described above still applies. This is the fast path for routine reclassification and Stage cleanup across many Posts; the full Post editor is still where Reason and PublishAt are set, since those need more room than a single row.
A Category's presentation post (see Organizing and presenting content) is unaffected by its own Category membership: detaching it from its Category doesn't stop it being that Category's presentation post — the two links are independent, even though having a presentation post detached from the Category it presents isn't the intended everyday setup. If the presentation post isn't Published, the Category's page simply doesn't show a header at all.
ContentElements — immutable material
A ContentElement is the actual material a Version's Slots point to: text blocks, images, links, contact entries, and the other Content Types. Each Content Type is stored in its own table, and each ContentElement carries a Role, a Rank, and an optional Reference — the addressing model that governs how it's matched to a Slot and how it can be embedded inline in Markdown; see Writing and embedding content for the full detail, and Content Type reference for what each kind's own fields and editing behavior are.
ContentElements are immutable once created. Editing one never changes it in place — it creates a new ContentElement. The Version being published then points its Slot at that new one; every other Slot that wasn't touched keeps pointing at whatever it already pointed to. This is structural sharing (the same idea as how Git stores an unchanged file only once across many commits): publishing a small edit doesn't duplicate the rest of the Post's content into a new copy.
Two rules that matter for how this behaves
- Nothing is ever deleted by changing Post Type. Changing a Post's Post Type publishes a new Version whose Slots are whatever the new Post Type's Display Schema declares. The admin interface only shows fields for the Slots that matter to the Post Type currently being worked with — but content with a Role from a different Post Type's Slots doesn't go away; it's simply not referenced by this particular Version. It still exists as ContentElements, and reappears automatically the moment a Version (this Post Type, later, or a Post Type switched back to) references that Role again.
- Content is independent of display. A ContentElement and the Versions that reference it are two separate concerns. The same underlying content can be redisplayed differently just by publishing a new Version under a different Post Type, without touching or losing the content itself.
Post Types
A Post Type is a named way of displaying a Post. Assigning a Post Type to a Post scaffolds that Post Type's declared Slots onto the Post as empty, ready to be filled — the admin editor pre-fills a block for each of those Roles, as a starting-point suggestion, not a restriction: every admin Post page shows the same full set of Content Element sections regardless of Post Type, and any Role can be added to any of them.
Each Post Type declares a Display Schema: a list of Slots, usually one per Role, though a Slot can instead be a wildcard covering every Role of a given Content Type. For each Slot: which Role (or "any Role") fills it, which Content Type can fill it, whether it's required or optional, and whether it holds one ContentElement or many. This Display Schema governs public rendering only — which Roles a Post Type's page actually displays.
| Post Type | Uses (summary) |
|---|---|
| Article | Cover image + body text, plus any number of additional elements of any kind |
| Link | 1 link |
| Image | 1 image |
| Gallery | Any number of images, videos, and/or audio clips (any Role), shown as one responsive matrix, plus optional intro text |
| Category Presentation | 0–1 avatar image, 1+ bio text blocks, 1+ contact entries |
| FullScreen | Preview image + summary text + optional call-to-action link, plus any number of additional elements of any kind — its Full page replaces the normal site chrome and occupies the complete viewport |
| Facebook Post | Optional post text, any number of images/videos (Rank order), links, and attachments — an owner-authored Facebook post imported by an external converter |
| Instagram Post | Optional caption, at least one image/video (Rank order — a carousel), links, and attachments — an owner-authored Instagram post imported by an external converter |
| LinkedIn Post | At least one of post text, image/video, link, or attachment — an owner-authored LinkedIn post imported by an external converter |
Each Post Type has its own Post Type reference page (the sidebar's Post Type reference group) — that's where its exact Display Schema, standard Roles, rendering behavior, and admin-editor guidance live. Code is what actually implements a Post Type's behavior; its reference page is a hand-written contract and guidance document alongside that code, not something that changes behavior on its own. Changing how an existing Post Type behaves, or adding a new one, is requested through a GitHub Issue and implemented, together with its reference page, through the repository's normal workflow — not by editing that page directly.
Changing a Post's Post Type
- Changing a Post's Post Type publishes a new Version whose slots are whatever the new Post Type's Schema declares.
- Slots are filled by matching role against content the Post already has from any Post Type it's ever carried — so switching back to a Post Type you've used before pulls that old content straight back, automatically.
- A Schema can declare a slot role that another Post Type also uses — that's exactly the mechanism that lets content survive a Type switch. Slot roles don't need to be unique to one Post Type; reusing a role (e.g.
cover) across Post Types is expected and useful.
Byline
Every Full page (see Explore a Kartotek site) shows a compact metadata line beneath its title's hidden-content banner: Published (the moment the Post first went live — see "Stage," above — or "Not published" for a PrePub Post an admin is previewing), Updated (this Version's own timestamp, shown only when its calendar day differs from Published's — a same-day edit isn't worth a second date), Original (only when the language you're currently viewing differs from the Version's own original — "Original: DA, AI Translated," for example, where the language code itself is a control: selecting it switches your language preference back to the original and reloads the page, exactly like using the language selector in the top-right — never a local, partial swap; see Languages and translations), and a single History link to the Metadata page described next (replacing what used to be two separate Version-number and Metadata links). Each item is omitted when it doesn't apply, with a separator only ever shown between two already-visible items. Published is set once, the first time a Post's Stage becomes Published, and never changes afterward — republishing, hiding, and re-publishing again all leave it alone; only Updated moves. Category Presentation is the one exception: its presentation-chrome nature means it never shows the Published/Updated/History items at all, only the Original item on its own when applicable — see Category Presentation's own Page regions.
There's also a third, Admin-only concept: Sorting date controls a Post's position in Category and Tag listings — never shown on the page itself, and never changed by an ordinary edit. It's set automatically the first time a Post is Published (from that same Published timestamp, unless a Full Admin or a migration already assigned one), and stays put through every ordinary republish after that. A Full Admin can view and edit it directly in the Post editor, independently of publishing anything; explicitly marking a publish as a Major Update there resets it to that publish's own timestamp, replacing any earlier manual or migrated value. This is what lets an imported Post carry its true original date into listings, or an Admin deliberately re-surface an old Post as new, without touching Published or Updated.
Article's own Full page additionally shows an optional explicit brief and an "Additional Information" area for supplementary content beside/after the metadata line — shared with Image, Category Presentation, and FullScreen's own Full pages too, but not Gallery's, which shows its own eligible content as one media matrix instead and has no separate Additional Information area — see Article's own Schema and rendering contract for the full detail, and Gallery's own for its matrix.
If the displayed Version carries any Tags, they show as a row of #tag links directly beneath the metadata line, each linking to that Tag's public page — see Organizing and presenting content's "Tags" section for what a Tag is and how it's authored.
Metadata page and JSON-LD export
domain/<uid>/metadata is a human-readable page showing a Post's facts (UID and any aliases, Stage, Type, Category, Published/Updated dates, "Version X of Y"), every past Version in a table, an Exports section linking to the machine-readable formats below, and every Content Element the current Version holds, each linking to its own standalone page (see Content Type reference). For an Image/Video/Audio/Attachment, that standalone page is visible to a non-admin exactly whenever the owning Post itself is — governed solely by the Post's own Stage, the same as everywhere else Post Media appears.
The current Version's data is also available as raw JSON-LD using Schema.org types — a real, recognized standard, so a generic system (not just this site's own client) can meaningfully ingest a Post's data. Three raw addresses:
/metadata/current— the current Version only./metadata/history— every Version the Post has ever had./metadata/archive— everything/historyhas, plus every referenced media file's actual bytes, base64-encoded and embedded inline (as adata:URI) — a genuinely self-contained export needing no follow-up requests to reconstruct the Post's full media content.
For a non-admin request, all three show exactly the media the current/each Version actually holds — Post Media's visibility is governed solely by the Post's own Stage (see "Removing a Post from view," above), not by any separate media-level gate.
All three are raw responses (Content-Type: application/ld+json), not pages, generated fresh on every request.
Stage-gated exactly like the Post's normal page (see "Removing a Post from view," above): a non-admin gets the same gated response the Metadata page always has for a PrePub or Hidden Post; the three raw addresses 404 instead. A Private Post 404s here too, on the Metadata page itself as well as all three raw addresses — no gated notice, consistent with Private revealing nothing to a non-admin. An admin always sees the real thing, and the Metadata page and /history//archive additionally include Hidden Versions.
Each Post Type maps to a Schema.org @type — Article → Article, Link → WebPage, Image → ImageObject, Gallery → ImageGallery, Category Presentation → ProfilePage.
Each Version's JSON-LD also states its own source language (inLanguage) and which configured languages it's currently available in (availableLanguage) — see Languages and translations.
Static PDF export
Every Published Post also has a PDF snapshot, at domain/_static/<uid>.pdf — a full rendered copy of the live page as it looked the moment it was last published, in its own original language. Deliberately not versioned: each Publish simply overwrites the same file at the same address. (Its sibling WARC capture is versioned instead — see Links and web preservation.)
Once a translation into another configured language is genuinely complete, its own PDF snapshot appears alongside the original, at domain/_static/<uid>--<language>.pdf (e.g. ...--da-DK.pdf) — never overwriting the original, and removed again if that translation is later invalidated by a source-language correction.
Only exists while the Post is Published. The moment a Post's Stage moves to PrePub or Hidden, the original PDF and every language variant are deleted immediately. Renaming a Published Post's uid deletes the old address's PDF (and variants) too.
Generation happens in the background, fire-and-forget — a Publish action never waits on it; a generation failure is logged, not surfaced to the admin or retried.
Native JSON export and import
Alongside its PDF, every Published Post also has a native, lossless JSON export, at domain/_static/<uid>.json — same lifecycle as the PDF, but a different purpose: where the JSON-LD metadata export above is descriptive metadata for a generic consumer, this is a complete, versioned, round-trippable document of the Post's own structure, meant to be re-imported — into this Kartotek instance under a new uid, or another one entirely. It also records the Post's source language and, per Version, the translation status of every configured language. An admin can also download a self-contained variant (Hidden Versions included, media embedded as base64) for a Post in any Stage, and upload/import a document through the admin editor or a scoped external API. Importing a document never recreates its previous translations automatically — an imported Post simply starts fresh, as if newly Published. See Import, export and external publishing for the full format and workflow.
Preservation guarantees
Beyond the Version history above, Kartotek independently preserves a Post's own public page and every external Link it carries — a per-Domain Wayback Machine submission on every publish and periodically thereafter, a permanent-per-Version WARC capture, and a live-health check with an honest archive fallback when a Link's destination goes away. All of this is automatic, asynchronous, and never delays publishing or saving — see Links and web preservation for the complete picture.
Visit analytics
If enabled (see Privacy and analytics), every qualifying visit to a Post's live page counts as one aggregate page view — no cookies, no visitor-level data. Only a Published Post's own live page counts; its History, Version, Metadata, JSON-LD, PDF, and native-JSON export addresses never do, and neither does a PrePub, Hidden, or Private Post's gated (or, for Private, 404) response.