msos/docs/content-model.md

111 lines
5.8 KiB
Markdown

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