# 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}/<slug>/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/<slug>/{en,mk,si}.md
  news/<slug>/{en,mk,si}.md
  events/<slug>/{en,mk,si}.md      # renders to the existing <lang>/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/<slug>` 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: "<span class=\"fig-illustrative\">…</span> · Photo by …"  # optional
body_format: html              # html | markdown  (see below)
---
<the article body, verbatim inner HTML of .article-body / .news-article-body>
```

### 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 (`<source_folder>/<slug>`). |
| `title` | yes | Plain text; rendered into `<h1>` and `<title>`. |
| `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 and for the article's schema date. Since U3 this is the source of truth (`tools/content_index.py` builds the date registry from it; the old `ARTICLE_DATES` table in `seo_inject.py` is now only a fallback). |
| `date_type` | no | `published` (default, omitted) or `modified`. Selects `datePublished` vs `dateModified` in the Article schema. |
| `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.

## Deferred refinements

- **`toc` (blog/event scrollspy labels).** The sidebar labels are often
  hand-shortened relative to the `<h2>` headings (e.g. heading "How an idea for
  socializing became the start of a movement" → sidebar "Idea to Movement"). To
  avoid regressing that editorial curation, U2 **preserves an existing populated
  scrollspy** and only auto-generates one for a page that has none. A future
  optional `toc:` front-matter list (ordered `{id, label}` pairs) would let the
  CMS edit those short labels directly. Not needed to reproduce today's pages.
- **`mobile_title`.** Some blog pages use a shortened mobile header title; U2
  preserves the existing one and does not overwrite it.

## 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.
