Wire Strapi to content/ via a two-way file sync provider

Adds slug (uid) fields to all four content types so Strapi documents
can be matched to files deterministically, plus:

- apps/cms/src/utils/content-file.ts: read/write/delete the +++ TOML
  frontmatter files, using smol-toml's TomlDate so dates round-trip as
  bare local dates (matching the legacy Zola format), not quoted
  datetime strings.
- apps/cms/src/utils/content-sync.ts: per-type frontmatter <-> Strapi
  field mappings, a boot-time importer that upserts every content/
  file into Strapi by slug, and a Document Service middleware that
  writes any create/update/delete made through Strapi straight back
  to the same files.

Strapi's SQLite DB is now genuinely a disposable cache: content/
stays buildable and readable with Strapi entirely absent, and
restarting Strapi rebuilds its DB from content/ with no duplicates
(matched by slug).

Verified end-to-end: fresh boot imports all seed files once; a second
boot re-imports idempotently (0 created, all matched by slug); create/
update/delete through the content-manager API all correctly write to
and remove the corresponding file on disk.
This commit is contained in:
2026-08-30 18:45:44 +03:00
parent 27efeb8b26
commit 85efb824ae
10 changed files with 807 additions and 328 deletions
@@ -15,6 +15,12 @@
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"description": "Matches the file name under content/bookshelf/"
},
"kind": {
"type": "enumeration",
"enum": ["book", "paper"],
@@ -15,6 +15,12 @@
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"description": "Matches the file name under content/blogs/"
},
"url": {
"type": "string",
"required": true
@@ -15,6 +15,12 @@
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"description": "Matches the file name under content/portfolio/"
},
"description": {
"type": "text"
},
@@ -15,6 +15,12 @@
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"description": "Matches the file/directory name under content/writings/"
},
"description": {
"type": "text"
},