msos/docs/content-model.md

4.8 KiB

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.

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