feat(cms): U1 content model + migration scaffold for blog/news/events
Establish content/ as the source of truth for editable content and back-fill it from the existing detail pages, per U1 of the no-code CMS plan. - docs/content-model.md: front-matter schema (EN/MK/SI), body_format html-vs-markdown, content/events -> projects/ output mapping. - tools/migrate_pages_to_content.py: bs4/yaml extractor; pulls title, subtitle, category, ISO date (from ARTICLE_DATES), hero image+caption; preserves the article body PLUS trailing sibling sections (event galleries) verbatim as body_format: html. Reports unparseable pages instead of emitting half-formed files. - content/**: 51 files (17 entries x EN/MK/SI), 0 skips. - tools/tests/test_migrate_roundtrip.py: 12 characterization tests (structured fields, news-has-no-subtitle, event gallery/lightbox preserved, Cyrillic UTF-8 round-trip, every page parses). - .gitignore: ignore __pycache__/ and *.pyc. U1 is unblocked and standalone. U2 (render content -> pages) is next; U5/U6 remain blocked on Open Question Q1 (Gitea<->GitHub topology). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6204db5f0c
commit
9ddd0f88ef
55 files changed
+4572
No files matched your search
@@ -0,0 +1,97 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
Reference in new issue
Block a user