Files
guilan-ace-backend/docs/content-api-schema-draft.md

7.7 KiB

Public editorial content API — proposed v1 schema

Superseded for implementation on 2026-09-27. The owner chose direct-save current content with no draft, revision, or private preview API. See the replacement plan; this schema remains historical context until T10 defines the simpler response.

Status: response contract approved through Bundle 0 review on 2026-09-26. This document specifies the response contract; it does not add endpoints. The architecture and permissions rationale live in ../../guilan-ace-deployment/tasks/cms-api-contract-draft.md.

Common types

  • revision: positive integer, monotonic per site or page.
  • published_at: ISO 8601 UTC timestamp.
  • Media: { "url": "https://…", "alt": "…", "width": 320, "height": 320 } or null. URL is served from backed-up media; informative images require nonempty alt.
  • RouteId: one of home, about, events, blog. Backend returns IDs; frontend maps IDs to application paths. No editor-supplied route href.
  • Seo: { "title": string, "description": string, "og_image": Media|null, "canonical_path": string }; canonical path comes from the fixed slug map.
  • Text fields are plain text. Clients escape them; no HTML payload is accepted.

GET /api/v1/content/site

200 only for a published site identity; otherwise 404.

{
  "revision": 2,
  "published_at": "2026-09-24T10:00:00Z",
  "official_name": "انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکده‌ی فنی و مهندسی شرق گیلان)",
  "short_name": "انجمن علمی مهندسی کامپیوتر",
  "faculty_subline": "دانشکده‌ی فنی و مهندسی شرق گیلان",
  "description": "…",
  "logo": { "url": "https://example.invalid/media/logo.ico", "alt": "نشان انجمن علمی مهندسی کامپیوتر", "width": 64, "height": 64 },
  "navigation": [
    { "route_id": "home", "label": "خانه", "position": 0, "visible": true },
    { "route_id": "about", "label": "درباره انجمن", "position": 1, "visible": true },
    { "route_id": "events", "label": "رویدادها", "position": 2, "visible": true },
    { "route_id": "blog", "label": "بلاگ", "position": 3, "visible": true }
  ],
  "contacts": [
    { "kind": "email", "label": "ایمیل", "url": "mailto:eastguilanceassociation@gmail.com", "position": 0, "visible": true },
    { "kind": "telegram", "label": "تلگرام", "url": "https://t.me/guilancea", "position": 1, "visible": true },
    { "kind": "instagram", "label": "اینستاگرام", "url": "https://instagram.com/guilance.ir", "position": 2, "visible": true }
  ],
  "footer_copy": "انجمن علمی مهندسی کامپیوتر دانشگاه گیلان",
  "seo_defaults": { "title": "…", "description": "…", "og_image": null, "canonical_path": "/" }
}

The example media URL is illustrative and must be replaced with a real managed-media URL after asset import. A missing logo is null; the short name remains visible. Contact kind is a closed enum (email, phone, telegram, instagram, website); URL schemes are validated by kind.

GET /api/v1/content/pages/{slug}

Allowed slugs: home, about, events, blog. 200 returns only the published revision. Unknown and unpublished-only slugs both return 404. events and blog have sections: [].

{
  "slug": "home",
  "revision": 3,
  "published_at": "2026-09-24T10:00:00Z",
  "title": "انجمن علمی مهندسی کامپیوتر",
  "intro": "…",
  "seo": { "title": "…", "description": "…", "og_image": null, "canonical_path": "/" },
  "sections": [
    {
      "key": "hero", "type": "hero", "position": 0,
      "heading": "انجمن علمی مهندسی کامپیوتر",
      "body": "…", "image": null,
      "primary_action": { "label": "آشنایی با انجمن", "route_id": "about" },
      "secondary_action": { "label": "دیدن رویدادها", "route_id": "events" }
    },
    {
      "key": "featured_events", "type": "featured_events", "position": 1,
      "heading": "رویدادهای برگزیده",
      "intro": "نگاهی به برنامه‌های انجمن",
      "all_link_label": "همه رویدادها"
    },
    {
      "key": "featured_posts", "type": "featured_posts", "position": 2,
      "heading": "از بلاگ انجمن",
      "intro": "نوشته‌ها و تجربه‌هایی برای یادگیری",
      "all_link_label": "همه نوشته‌ها"
    },
    {
      "key": "participation", "type": "participation", "position": 3,
      "heading": "در جریان برنامه‌های انجمن باشید",
      "body": "رویدادها و نوشته‌های تازه را دنبال کنید.",
      "primary_action": { "label": "دیدن رویدادها", "route_id": "events" },
      "secondary_action": { "label": "ارتباط با انجمن", "route_id": "about" }
    }
  ]
}

sections contains only visible sections, sorted by position. Required Home keys are hero, featured_events, featured_posts, then participation; required About keys are identity first and contact last. Optional About keys are mission, activities, values, committees, affiliations, and faq. Each optional section has a bounded heading/body or typed entry array as specified in the architecture note. The Home featured sections contain headings and link labels only. Event and post cards are fetched from their existing domain APIs, so no stale title, poster, availability, or price is copied into the CMS response. Required routes remain available through navigation. The response contains editorial content, not layout instructions.

An About page uses the same envelope. Its required sections have these shapes:

[
  { "key": "identity", "type": "identity", "position": 0, "heading": "درباره انجمن", "body": "…" },
  { "key": "contact", "type": "contact", "position": 2, "heading": "ارتباط با انجمن" }
]

The frontend resolves official name, mark, faculty subline, and contacts from the published site response. It must not duplicate those values in page section payloads. For editor preview, the site draft and page draft may be selected independently; the preview UI labels both revision numbers so a mixed preview cannot be mistaken for one published snapshot.

Draft preview

  • GET /api/v1/content/preview/site?revision=<id> returns the site shape for the exact authorized draft.
  • GET /api/v1/content/preview/pages/{slug}?revision=<id> returns the page shape for the exact authorized draft; for editor review it may include visible: false sections and entries so hidden content is inspectable, but the preview UI must distinguish them and never render them as public content.
  • Unauthenticated requests receive 401 with no revision details; authenticated users without permission receive 403; missing or wrong-target draft receives 404. Preview sets Cache-Control: private, no-store, Vary: Cookie, Authorization, and X-Robots-Tag: noindex, nofollow.
  • Public reads use the published revision only, with ETag derived from revision and response checksum. Proposed shared cache bound: Cache-Control: public, s-maxage=60, must-revalidate; the frontend CMS adapter must avoid a second persistent cache that extends this bound.

Compatibility and validation

The frontend must reject an unknown section type safely, log the mismatch, and render the rest of the page. The backend rejects unknown section keys/types at draft validation. Changing a field name or meaning requires a v2 contract; adding a new optional section type requires coordinated backend/frontend rollout. No public response exposes draft metadata, editor identity, or unpublished assets.