11 KiB
Public content CMS and API contract — approved for implementation
Date: 2026-09-24; revised 2026-09-25; approved by the owner through bundle0-review.md on 2026-09-26. Status: Bundle 0 contract approved for implementation; no public API or schema exists yet. Inputs: content-inventory.md, product-review.md, frontend PRODUCT.md and DESIGN.md, existing Django Ninja routing in guilan-ace-backend/config/api.py.
Ownership boundary
The CMS owns association identity, approved public navigation/footer/contact entries, and page introductions/SEO. It owns ordered, visible sections of home and about from the finite registry below. It does not own event/blog records, registration success markdown already stored on each event, auth/form/admin text, payment outcomes, or free-form layouts. Stable slugs initially: home, about, events, blog.
Proposed persistent records
| Record | Fields and constraints |
|---|---|
SiteIdentity singleton |
Stable site key and nullable published_revision_id. Singleton enforced in service/admin and database. |
SiteRevision |
Immutable snapshot of official_name (required, max 180), short_name (required, max 80), faculty_subline (max 140), description (max 500), logo media FK and required alt when logo exists; approved contacts/social entries and navigation entries with fixed IDs, label, sort order and visibility; footer copy; default SEO. Tracks editor, publisher and timestamps like page revisions. |
EditorialPage |
Unique immutable slug from an allowed registry; page_kind (sections or listing_intro); current published revision FK nullable. Slug never derived from editable title. |
PageRevision |
Page FK, monotonic revision number, draft or published snapshot, title, intro, SEO title/description/OG media, creator, publisher, timestamps, source seed key/version and immutable content checksum. Store each publish as a new immutable snapshot; do not mutate the public revision in place. |
PageSection within revision |
Stable section key, approved type, position integer, visible boolean, bounded type-specific text fields, optional media FK and alt text. Unique (revision, key) and (revision, position); type/slug registry restricts allowed combinations. |
MediaAsset |
Uploaded file, MIME signature, bytes, dimensions, alt, license/source and approval status. Logo and OG media use the same managed-media path; no frontend build URL becomes permanent content. |
PublicationAudit |
Actor, site or page target, from/to revision, operation (create_draft, update_draft, publish, restore), timestamp. Append only. |
Proposed section registry aligned to the owner's fixed Home order. Section records supply editorial text; they do not duplicate event/post records or select a card layout, colors, or button styling:
| Page | Section key/type | Required | Editable content | Order/visibility |
|---|---|---|---|---|
home |
hero |
Yes | Heading (max 120), lead (max 500), primary/secondary CTA labels (max 40 each), each CTA target selected from fixed about, events, blog route IDs; optional approved image and alt |
First and visible. Site name/subline comes from SiteRevision, not a second copy. |
home |
featured_events |
Yes | Section heading (max 90), introduction (max 300), all-events link label (max 40) | Second and visible. Event cards, posters, dates, prices and status read the existing events API, not this section. |
home |
featured_posts |
Yes | Section heading (max 90), introduction (max 300), all-posts link label (max 40) | Third and visible. Post cards, titles, categories, images and links read the existing blog API, not this section. |
home |
participation |
Yes | Heading (max 90), body (max 600), up to 2 CTA labels (max 40 each) with fixed route IDs; optional approved image and alt | Fourth and visible, before the shared footer. |
about |
identity |
Yes | Heading (max 120), introduction (max 600); official name/mark from SiteRevision |
First and visible. |
about |
mission, activities, values, committees, affiliations, faq |
No | Heading (max 90), body (max 1200) or typed entries (max 20, each title/question max 120 and text/answer max 600) | Reorderable after identity or hidden. Unmapped statistics and affiliation claims are omitted from the initial seed. |
about |
contact |
Yes | Heading (max 90); approved visible contact entries from SiteRevision |
Last and visible; contacts have one source of truth. |
events and blog use bounded title/intro/SEO fields and no sections. Every allowed section key is unique within its page revision; publishing rejects missing required sections or an invalid order. The Home order is fixed by the owner's design requirement; About optional sections remain reorderable/hideable. Section text is plain text. Navigation destination IDs and route paths are code-owned; editors can edit display labels/order/visibility on the approved destination list, not supply arbitrary in-app hrefs. The initial public contact set is the owner's About-page selection. This registry and field limits were accepted for T06 implementation on 2026-09-26.
The Home featured-event source needs no new CMS event fields. The first release selects the next three published future events by start time; if none exist, it shows recent completed events with an archive label. Blog uses published is_featured posts, then recent published posts as a fallback. Empty results show a short honest message and a link to the full listing. This selection policy was accepted on 2026-09-26; it never changes registration, payment, or publication rules.
Edit, preview, publish and restore
| Actor | Draft | Preview | Publish | Restore | Public read |
|---|---|---|---|---|---|
| Anonymous | No | No | No | No | Published only |
| Authenticated nonstaff | No | No | No | No | Published only |
content_editor staff group |
Yes, on allowed pages | Own/authorized drafts | No | No | Published only |
content_publisher staff group |
Yes | Yes | Yes | Yes | Published only |
| Superuser | Yes | Yes | Yes | Yes | Published only |
Use explicit Django permissions (change_content_draft, preview_content_draft, publish_content, restore_content) and group assignment; is_staff alone does not grant publishing. A transaction checks permission and locks the page row, writes an immutable revision and swaps published_revision_id atomically. Restore creates a new revision from the chosen prior snapshot, preserving history. Publisher and editor identities remain visible in admin audit. Preview requires authentication, authorization and a draft revision; no public route accepts a draft ID.
Proposed API v1
Mounted under the existing /api/ Ninja root in config/urls.py:
GET /api/v1/content/site: only published identity.200withrevision,published_at, names, logo URL/alt, approved contacts, navigation route IDs/labels/order/visibility, footer and SEO defaults.404when there is no published identity.GET /api/v1/content/pages/{slug}: only published page.200withslug,revision,published_at,title,intro,seo, and ordered visiblesections;404for unknown or unpublished-only slugs. Hidden sections never leave the response.GET /api/v1/content/preview/pages/{slug}?revision=<id>: authenticated authorized staff only; exact draft revision;403for unauthorized staff and404for missing draft. The server must not expose draft existence to anonymous callers. SetCache-Control: private, no-store,Vary: Cookie, Authorization, andX-Robots-Tag: noindex, nofollow.GET /api/v1/content/preview/site?revision=<id>: the same staff permission and no-store rules for a site-identity draft, including logo, navigation, contacts, footer and SEO defaults. Public site data remains at its published revision during preview.
The backend API schema draft contains complete site, Home and About payload examples. Use canonical path from the fixed slug map, not an arbitrary editor URL. Proposed public cache: Cache-Control: public, s-maxage=60, must-revalidate plus ETag from the published revision and response checksum. The frontend CMS adapter performs uncached server reads so a second page cache does not extend that 60-second bound. Publish/restore invalidates application cache and requests frontend revalidation for /, /about, /events, /blog, and shared shell; revalidation failure is logged while the shared API cache still expires within 60 seconds. The deployment proxy already routes PathPrefix(/api) to the Django service; the release rehearsal must verify the deployed configuration. 404 is explicit for empty site/page; the frontend flag remains on legacy rendering until approved content exists. Draft preview uses no shared cache at any layer. Unknown slugs and unpublished-only pages have the same public 404 shape.
Media, validation and seed rules
- Reject unsafe markup; text fields are plain text or tightly validated Markdown only where specified. Reject non-HTTP(S) links except explicitly supported
mailto:andtel:contact types. Verify media by content signature, dimensions and byte limit; require meaningful alt for informative images. - Public images use backed-up Django media URLs. Optional image absent means the section remains legible with text alone. The source logo checksum and license/source are recorded before import.
- The T12 manifest records stable keys, source path, exact legacy value, checksum where relevant, destination, approval actor/status, and exclusion reason. T13 dry run emits create/skip/conflict and field diffs, never overwrites an editor-modified row by default, and creates drafts only.
Paper walkthrough and later release gates
- Editor changes
home.heroin a draft. PublicGETstays at published revision 3. Authorized preview of draft 4 returns new copy withno-store; anonymous preview returns no draft content. - Publisher reviews preview at 320px and desktop, publishes. Transaction swaps revision pointer to 4 and logs publisher. Public cache invalidates or expires within agreed TTL. Event and payment records are untouched.
- Publisher restores revision 3. Service creates revision 5 with revision 3 content, logs restore, swaps published pointer. Readers see revision 5; revision 4 stays in history.
- At 320px, long Persian identity wraps, navigation remains reachable without covering content, optional image leaves no blank frame, and hidden optional About sections do not leave layout gaps. At desktop, Home section order matches the owner's fixed sequence and the approved visual direction.
The earlier concepts were rejected. Frontend DESIGN.md records the approved logo-led Home/About direction. T05 and Checkpoint B were accepted on 2026-09-26: fixed Home order, featured-item selection policy, section registry and field limits above, content_editor/content_publisher permission policy, exact preview behavior, fixed-route payloads, and the 60-second API cache rule. Later release gates: assigning actual staff accounts, importing licensed media, deployment-proxy rehearsal, and mapping disputed content claims before seed publication.