110 lines
5.5 KiB
Markdown
110 lines
5.5 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. 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.
|
|
|
|
## 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.
|