Supercommerce API Docs
Admin API

Content Pages Module — Admin

HTTP surface for managing content (CMS) pages — operator-authored static pages addressable by slug. The content field stores raw HTML rendered verbatim by the storefront; pages form a tree via parentId.

HTTP surface for managing content (CMS) pages — operator-authored static pages (About, Return Policy, FAQ, landing pages, …) addressable by slug. The content field stores raw HTML typed by an admin and rendered verbatim by the storefront. Pages form a tree via a parentId self-reference, and the same module exposes unauthenticated storefront read endpoints.

Source: api-modules/content/src/controllers/admin-content-page.controller.ts.

HTML is stored as-typed with no server-side sanitisation — admins are trusted authors. The storefront read surface lives in store/content.md.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching content:* permission.

Endpoint groupPermission
GET /admin/content-pages, GET /admin/content-pages/:idcontent: read
POST /admin/content-pages, POST /admin/content-pages/:id/duplicatecontent: create
PUT /admin/content-pages/:id, POST /admin/content-pages/:id/restorecontent: update
DELETE /admin/content-pages/:idcontent: delete

Raw-HTML gate (content: manageRawHtml). Raw-HTML pages run author-controlled markup/scripts on the storefront, so a second permission gates them on top of the base grants above:

  • Create with template: "html" requires content: manageRawHtml (in addition to content: create); template: "default" does not.
  • Update rejects any change to template or content unless the caller holds content: manageRawHtml — re-sending the unchanged value is a no-op and allowed. This applies to every page, not only raw-HTML ones.
  • Duplicate of a subtree containing any raw-HTML page requires content: manageRawHtml.

A caller lacking the permission gets 403. The admin UI mirrors this: the Raw HTML template option is hidden on create, and the template + content fields are disabled on edit.

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* present on the paginated list only */ }
}

The list endpoint is offset-paginated (metadata: { total, limit, offset, hasMore }) and returns summary rows (no HTML content body).

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR (invalid parentId, cycle, self-parent)
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409CONFLICT (slug in use; deleting a page with children; restore slug clash)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

  • Slug — kebab-case, auto-derived from title when omitted on create. Unique among live pages only (a soft-deleted page's slug is reusable).
  • publishedAt — stamped the first time a page becomes PUBLISHED, then preserved across later status changes.
  • Soft delete — pages soft-delete via deletedAt. Deleting a page that has live children returns 409; reassign or delete the children first. POST .../restore reverses the deletion, returning 409 if the slug has since been claimed by another live page.
  • HierarchyparentId forms a tree. Updates that would set a page as its own parent or move it beneath one of its own descendants return 400.

Domain types

ContentPage

type ContentPageStatus = "DRAFT" | "PUBLISHED";

type ContentPage = {
  id: string;
  title: string;
  description: string | null;          // page subtitle / short description (distinct from metaDescription)
  slug: string;
  content: string;                     // raw HTML, rendered verbatim by the storefront
  status: ContentPageStatus;
  template: "default" | "html";        // storefront render mode (default = prose, html = raw)
  layout: string | null;               // free-form layout hint (preserved from legacy CMS)
  hideLowerMenu: boolean;              // hide the storefront lower (category) nav on this page; default false
  publishedAt: string | null;          // ISO; set once on first publish
  sortOrder: number;
  parentId: string | null;

  metaTitle: string | null;
  metaDescription: string | null;
  metaKeywords: string | null;
  metaRobots: string | null;           // defaults to "index, follow"

  ogTitle: string | null;
  ogDescription: string | null;
  ogImage: string | null;              // storage key (prefix with assets base URL)
  ogType: string | null;

  schema: Record<string, unknown> | null;   // JSON-LD structured data
  metadata: Record<string, unknown> | null;

  createdAt: string;                   // ISO
  updatedAt: string;                   // ISO
  deletedAt: string | null;
};

List rows (ContentPageSummary) carry every field above except content and schema.


Endpoints

GET /admin/content-pages — List pages — content: read

Offset-paginated summary list, ordered by sortOrder ASC by default.

Query

NameTypeNotes
limit / offsetnumber?pagination (default 100 / 0)
status"DRAFT" | "PUBLISHED"?filter by status
parentIdstring?parentId=<id> for children; parentId=null for top-level
includeDeletedboolean?include soft-deleted pages (to restore them)
searchValuestring?substring match over title + slug
sortBy / sortDirectiontitle / sortOrder / createdAt / updatedAt / publishedAt

GET /admin/content-pages/:id — Get one — content: read

Returns the full ContentPage (incl. HTML content), or 404.

POST /admin/content-pages — Create — content: create (+ content: manageRawHtml when template: "html")

Body (CreateContentPage)

FieldTypeNotes
titlestringrequired
contentstringrequired — raw HTML
slugstring?kebab-case; auto-derived from title when omitted
descriptionstring?subtitle / short description
status"DRAFT" | "PUBLISHED"default DRAFT
template"default" | "html"storefront render mode; default default (prose). html = raw HTML as-authored
layoutstring?free-form layout hint (preserved from the legacy CMS)
hideLowerMenuboolean?hide the storefront lower (category) nav on this page; default false
sortOrdernumber?default 0
parentIdstring?must reference a live page
metaTitle / metaDescription / metaKeywords / metaRobotsstring?SEO; metaRobots defaults to "index, follow"
ogTitle / ogDescription / ogImage / ogTypestring?Open Graph; ogImage is a storage key uploaded via the admin presigned flow
schemaobject?JSON-LD structured data
metadataobject?forward-extensibility escape hatch

Response 201 — the created ContentPage. 409 if the slug is already in use.

PUT /admin/content-pages/:id — Update — content: update (+ content: manageRawHtml to change template/content)

Partial update; all CreateContentPage fields optional. Setting status to PUBLISHED stamps publishedAt (once). Reparenting validates against cycles (400) and a missing/deleted parent (400). Changing slug to one held by another live page returns 409. Changing template or content without content: manageRawHtml returns 403 (re-sending the unchanged value is allowed).

DELETE /admin/content-pages/:id — Soft-delete — content: delete

Soft-deletes the page. 409 if the page has live children.

POST /admin/content-pages/:id/restore — Restore — content: update

Clears deletedAt. 404 if not a deleted page; 409 if the slug is now held by a live page.

POST /admin/content-pages/:id/duplicate — Duplicate — content: create (+ content: manageRawHtml if the subtree contains a raw-HTML page)

Deep-clones the page and its entire live subtree as DRAFTs. The new root's title is suffixed " (copy)"; every cloned slug is <slug>-copy (then -copy-2, -copy-3, … on collision). Returns 201 with the new subtree root.


  • content (storefront) — public read surface for published pages. See store/content.md.
  • admin-rbac — defines the content permission set. See admin/admin-rbac.md.

On this page