# Content model — blog / news / events (EN / MK / SI) Source of truth for editable content. Implements **U1** of `docs/plans/2026-08-08-001-feat-no-code-cms-plan.md`. Today the detail pages under `en|mk|si/{blog,news,projects}//index.html` are hand-authored HTML and are the source of truth. This model moves the source of truth into `content/`: one Markdown-with-front-matter file per **entry per language**. The detail HTML pages become *build output* (produced by U2). ## Layout ``` content/ blog//{en,mk,si}.md news//{en,mk,si}.md events//{en,mk,si}.md # renders to the existing /projects/ output ``` - One folder per entry (the **slug**), one file per language. - A language file may be **absent** — that language simply is not published for that entry (U2 skips it; it does not emit a broken half-page). - `content/events/` maps to the site's existing `projects/` output folder. The URL path and the SEO date-registry key stay `projects/` for compatibility (see `source_folder` below). ## File shape Standard front-matter: a YAML block delimited by `---`, then the body. ```markdown --- type: blog # blog | news | event slug: how-to-open-slovenian-bank-account lang: en # en | mk | si source_folder: blog # blog | news | projects (URL + date-registry key) title: "How to open a Slovenian bank account as an international student" subtitle: "A practical guide for Macedonian students…" # optional (news has none) category: "Student life" # optional; blog category shown before the date date: "2026-08-01" # ISO 8601 — authoritative for sorting (from ARTICLE_DATES) date_line: "Student life · Last checked 1 August 2026" # verbatim human date line, optional hero: src: "/images/blog-slovenian-bank-account.webp" # root-relative alt: "International student using mobile banking…" width: "6048" height: "4024" caption_html: " · Photo by …" # optional body_format: html # html | markdown (see below) --- ``` ### Field reference | Field | Required | Notes | |-------|----------|-------| | `type` | yes | `blog`, `news`, or `event`. | | `slug` | yes | URL slug; matches the folder name. | | `lang` | yes | `en` / `mk` / `si`. | | `source_folder` | yes | `blog` / `news` / `projects`. Drives the output URL and the `ARTICLE_DATES` key (`/`). | | `title` | yes | Plain text; rendered into `

` and ``. | | `subtitle` | no | Blog/event lead paragraph (`.article-subtitle`). News has none. | | `category` | no | Shown before the date on blog pages. | | `date` | yes | ISO date, authoritative for listing order. Sourced from `tools/seo_inject.py` `ARTICLE_DATES`, else extracted from the page. | | `date_line` | no | The exact human-readable date line from the page, preserved for fidelity. U2 may instead regenerate it per-language from `date`. | | `hero.*` | no | Lead image + caption. `src` is root-relative (`/images/…`). `caption_html` is verbatim (may contain attribution links). | | `body_format` | yes | See below. | ### `body_format`: `html` vs `markdown` - **`html`** — the body is a verbatim HTML fragment: the page's `.article-body` / `.news-article-body` element **with its wrapper**, plus any following sibling sections inside the article container (notably `section.event-gallery` on event pages, which sits *after* `.article-body`). This is what **migration produces**, so rich elements the site already uses — `guide-box` asides, `article-inline-image` figures, `event-gallery` lightbox grids, ordered lists, nested links — survive **losslessly**. The U2 builder injects this fragment as-is, in place of everything after the hero figure. Because migrated output pages sit at the same directory depth as the originals (`<lang>/<type>/<slug>/index.html`), relative asset paths inside the body (`../../../images/…`) remain valid without rewriting. The blog scrollspy sidebar is *not* stored — it is derived from the body's `<h2 id>` headings by U2. - **`markdown`** — the body is Markdown. This is the intended format for **new posts authored in the CMS**; the U2 builder converts it to article HTML using the existing converter in `tools/build_blog.py`. The blog scrollspy sidebar is regenerated from the `##` (h2) headings. Migration deliberately chooses `html` over a lossy HTML→Markdown conversion. The scaffold is a starting point a human proofs, **not** a guaranteed lossless transform — pages the extractor cannot parse cleanly are reported, not silently emitted. ## Non-goals for U1 - No CMS wiring (U7), no build/render of pages (U2), no listings/date refactor (U3). U1 only defines this model and back-fills `content/` from current pages.