Files
blog/docs/content-model.md
T
davide 47a8e7788f Add docs/ with architecture, content model, frontend reference
Require CLAUDE.md changes to keep docs/ in sync with new features
or architecture.
2026-09-09 15:28:38 +02:00

3.6 KiB

Content model & admin panel

Content types

Defined in cms/src/api/*/content-types/*/schema.json. Both use plain Strapi factory defaults (createCoreRouter/createCoreController/createCoreService) — no custom controllers, routes, services, or policies exist for either type.

Type Fields
Article title (string, required, max 160), slug (UID generated from title), content (richtext — Markdown source), cover (single media/image), category (many-to-one relation to Category)
Category name (string, required, unique), slug (UID from name), articles (inverse one-to-many)

Deliberately minimal: no Author (the only author is the admin account), no tags, no separate SEO fields. Don't add these back without a concrete need — see CLAUDE.md:

  • Meta description is derived at request time from the article body (summarise() in frontend/server/utils/strapi.ts).
  • Publish date is Draft & Publish's publishedAt.
  • Social preview image is the cover.
  • The article byline is the admin user's name (createdBy, via populateCreatorFields: true), not a content field.

Draft & Publish is enabled on Article (draftAndPublish: true) and disabled on Category (draftAndPublish: false) — categories aren't drafted. Public URLs always use the slug, never the numeric id.

Admin login & permissions

  • The admin panel lives at /admin, routed by Caddy straight to the cms container (architecture.md). It's Strapi's standard email/password admin authentication — no custom auth code in this repo.
  • Public visitors never authenticate. There is no visitor account system, no comments, no public write access of any kind.
  • cms/src/index.ts (bootstrap) runs two idempotent setup steps on every Strapi start:
    1. grantPublicReadAccess — grants the public role exactly find/findOne on Article and Category, and nothing else (no create/update/delete, no other content type). This is what lets the Nitro endpoints read published content without a token.
    2. disablePublicSignUp — turns off users-permissions' public registration (allow_register: false), since no front-end user accounts should ever exist.
  • Any permission beyond find/findOne for Public needs to be justified explicitly — this is a deliberate least-privilege boundary, not an oversight.
  • cms/config/plugins.ts further restricts the upload plugin's allowed MIME types (images, video, audio, PDF, office docs, text/CSV) and explicitly denies executables.

Editorial workflow

  1. Log into /admin.
  2. Create/edit a Category if needed (name → slug is generated automatically).
  3. Create/edit an Article: title (→ slug), Markdown content, cover image, category.
  4. Publish (Draft & Publish). Unpublished drafts are never served by the find/findOne permissions above — Strapi's default behavior already excludes non-published entries from the public API.
  5. The change is live immediately: the public site has no cache layer to invalidate (see architecture.md).

Why Markdown, not a rich-text/WYSIWYG field

content is a plain richtext (Markdown) field. Strapi stores the raw Markdown; conversion to HTML happens once, server-side, in the Nuxt Nitro endpoint (renderMarkdown(), using marked) — never in Strapi and never in the browser. This keeps marked out of the client bundle and keeps HTML generation in one place. There is no sanitization step: this is intentional, since the only author is the trusted admin, not arbitrary users.