-
Notifications
You must be signed in to change notification settings - Fork 0
Content Model
Everything lives in cms-core as typed pydantic models. The portable source of truth is the JSON/Markdown export — never the database.
- EN is the source. Target languages: PT-PT, ES, FR, DE (configurable subset).
- Every translatable entity records, per translation, the checksum of the source content it was translated from. States are derived, never stored:
-
missing— no translation exists -
outdated— the source changed after the translation was made -
complete— the translation matches the current source checksum
-
- Editing the source automatically flips its translations to
outdated— no manual bookkeeping, ever. - Publishing requires
completein every required language (therequired-translationsrule: warnings inreview, errors inpublished).
Content moves through draft → review → published → archived. Only published content reaches builds and exports; review/published are validated for language parity.
Article: permanent id (slug), status, timestamps, category (declared slug), sorted unique tags (slugs), optional cover (media id), optional author byline, a featured flag (leads the home highlight), free-form custom fields, optional publish_at (scheduling — the build is the clock) and deleted_at (the trash flag). ArticleContent per language: title (required), summary, body_markdown (CommonMark, raw HTML disabled at render time), optional per-language slug (falls back to the article id).
Both entry types carry an optional publication window: publish_at (ADR-0024) and unpublish_at (#133) — the builder includes an entry only inside the window, deterministically from the build clock; a window that ends before it starts is refused at the model.
Page: id, status, timestamps, its own translatable PageContent (title, description, required slug) plus an ordered list of typed sections.
Section: key (unique per page), kind (names the theme template that renders it — the theme owns the meaning), and free-form SectionContent: a fields name→value map plus a media id list. Sections translate independently.
The kind set is open (extensions add kinds; unknown kinds render generically) and sections are unlimited and reorderable. ADR-0037 is fully executed: SectionContent.items is the unbounded, translatable repeating group (FAQ questions, expertise rows, story stat pairs — the old numbered-field convention is retired, with legacy content mapped into items at render time), kinds declare Markdown-capable fields rendered through the same safe renderer as article bodies, and PageContent.body_markdown lets a page be a document as well as a zone composition. Structured sections stay the contract because multilingual parity and the portable source of truth need data, never markup blobs — a behavioral end-to-end test proves the eighth FAQ item from the admin editor to both themes' final HTML.
A page's aggregate language state is the worst state across its own content and all its sections — one untranslated section keeps the whole page unpublished in that language.
A section whose fields include a menu entry contributes a home-anchor menu item.
MediaAsset: id, filename, MIME type, dimensions for images, mandatory EN alt text, translatable alt per language. Validation checks every referenced asset exists and that images carry alt text.
| Rule | Severity | What it checks |
|---|---|---|
required-translations |
error (published) / warning (review) | complete translations in every required language |
unique-slugs |
error | no URL collisions within any language |
media-references |
error | covers/section media point at existing assets |
media-alt-coverage |
error/warning | EN alt mandatory; translated alt coverage |
known-categories |
error | article categories are declared in config |
Rules are composable: any object with a name and check(content, context) is a rule; RuleSet runs the enabled ones and aggregates a Report (errors, warnings, ok). Publishing is gated on zero errors.
Sardine CMS — multilingual, static-first CMS framework · Repository · Live demo · Apache-2.0