Require CLAUDE.md changes to keep docs/ in sync with new features or architecture.
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()infrontend/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, viapopulateCreatorFields: 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 thecmscontainer (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:grantPublicReadAccess— grants thepublicrole exactlyfind/findOneon 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.disablePublicSignUp— turns offusers-permissions' public registration (allow_register: false), since no front-end user accounts should ever exist.
- Any permission beyond
find/findOnefor Public needs to be justified explicitly — this is a deliberate least-privilege boundary, not an oversight. cms/config/plugins.tsfurther restricts theuploadplugin's allowed MIME types (images, video, audio, PDF, office docs, text/CSV) and explicitly denies executables.
Editorial workflow
- Log into
/admin. - Create/edit a Category if needed (name → slug is generated automatically).
- Create/edit an Article: title (→ slug), Markdown content, cover image, category.
- Publish (Draft & Publish). Unpublished drafts are never served by the
find/findOnepermissions above — Strapi's default behavior already excludes non-published entries from the public API. - 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.