msos/docs/content-model.md

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