Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
cms Submodule
pages
Page CRUD, publish/duplicate lifecycle, automatic revision snapshotting, and full-replace management of the component instances placed into a page's layout regions.
Source path: libraries/cms/src/cms-page.controller.ts, cms-page.service.ts, cms-page-component.service.ts
Module file: cms.module.ts
Introduction
The Pages submodule owns `cms_page`, the record a site visitor ultimately renders. Every page has a `type` (page, landing, blog_post, blog_index, custom) and a `status` (draft, review, scheduled, published, archived), and is scoped to a single locale via `locale_id` — the DTO accepts a locale *code* (e.g. "en", "pt") which CmsPageService resolves to an ID through LocaleService, throwing a 400 if the code is unknown. The same resolve-by-code pattern applies to `layout_slug` → `layout_id`: a page cannot be created or updated to reference a layout that doesn't exist. The slug is unique per locale, enforced by a partial unique index on `(slug, locale_id)` scoped `WHERE deleted_at IS NULL` — so a slug freed up by a soft-deleted page can be reused, but two live pages in the same locale can never collide.
Every mutating action on a page — create, update, publish, duplicate, and restore — unconditionally calls a private `createRevision()` helper that snapshots the entire mapped page payload (not a diff) into `cms_page_revision`, tagged with an `action` field and the acting user. The version number is simply the previous max version for that page plus one. This means the revision history is a full audit trail of every state the page has ever been in, generated automatically as a side effect of normal editing rather than through any explicit "save revision" endpoint — `GET /cms/pages/:id/revisions` lists them, most recent first. `DELETE /cms/pages/:id` is the one mutating action that does **not** create a revision: it only sets `deleted_at`, and both `list()` and `getById()` filter on `deleted_at: null`, so a soft-deleted page simply disappears from every read endpoint while its row and revision history remain in the database.
`POST /cms/pages/:id/publish` sets `status: published`, stamps `published_at`/`published_by_id`, and records a `publish` revision. `POST /cms/pages/:id/duplicate` clones the page into a new `draft`: it appends `-copy` to the slug and, if that collides within the same `locale_id`, keeps appending `-1`, `-2`, … until it finds a free one, but the cloned title always gets a hardcoded Portuguese suffix (`" (cópia)"`) regardless of the source page's own locale — a localization gap worth knowing about if the admin UI is used in other languages. `POST /cms/pages/:id/revisions/:revisionId/restore` reapplies a past snapshot's fields (title, description, type, locale, layout, SEO fields) but **always forces `status: draft`** no matter what status the snapshot captured — restoring a revision that was once `published` does not republish the page, it only stages that content as a draft that must go through `/publish` again explicitly.
`GET/POST /cms/pages/:pageId/components` and their `PATCH`/`DELETE` `:id` counterparts manage `cms_page_component`, the join between a page, one of its layout's regions, and a catalog component. `POST .../components` (`saveComponents`) is **full-replace, not incremental**: it deletes every existing `cms_page_component` row for the page and bulk-recreates the entire list from the submitted `instances` array in one call. A client that omits an existing instance from the payload will silently lose it — there is no merge/upsert semantics here, unlike the individual `PATCH .../components/:id` and `DELETE .../components/:id` endpoints, which do target a single instance by its own ID. Each instance's `region_code` is resolved to a `layout_region_id` scoped to the *page's own* `layout_id` (via `resolveRegionId(page.layout_id, region_code)`), so a region code that exists on a different layout is rejected with a 400 even if the code string itself is valid elsewhere; `component_slug` is resolved to `component_id` against the global component catalog with no such scoping.
The `cms_page_category` and `cms_page_tag` join tables are defined in `libraries/cms/hedhog/table/*.yaml` (page ↔ `category`, page ↔ `tag`) but have **no corresponding controller, service, or endpoint** anywhere in the module — the schema models a taxonomy/tagging layer for pages that the current API surface does not expose at all. Do not assume category or tag assignment is reachable through this or any other CMS endpoint.
HTTP Endpoints
13 endpoints
Paginated list of pages, excluding soft-deleted ones. search matches title or slug (case-insensitive). layout and locale filters are resolved to their internal IDs before querying.
- Query
- page (default 1), pageSize (default 20), search?, status? (draft | review | scheduled | published | archived), type? (page | landing | blog_post | blog_index | custom), layout? (layout slug), locale? (locale code)
Create a new page and record its initial revision (action: "create").
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| slug | string | yes | — |
| title | string | yes | — |
| description | string | no | — |
| type | CmsPageType (page | landing | blog_post | blog_index | custom) | no | Default: page |
| status | CmsPageStatus (draft | review | scheduled | published | archived) | no | Default: draft |
| locale | string | yes | Locale code (e.g. "en"), resolved to locale_id |
| layout_slug | string | yes | Resolved to layout_id; 400 if the layout does not exist |
| seo_title | string | no | — |
| seo_description | string | no | — |
| author_id | number | no | Falls back to the authenticated user ID if omitted |
| scheduled_at | date string | no | — |
Get a single (non-deleted) page by ID.
- Params
- id (int)
Partially update a page (same fields as create, all optional). Records a new revision (action: "update"). Editors can update pages but cannot delete them.
- Params
- id (int)
Soft-delete a page by setting deleted_at. Does not record a revision. Restricted to admin roles — cms-editor cannot delete pages.
- Params
- id (int)
Publish a page: sets status to published, stamps published_at/published_by_id, and records a revision (action: "publish").
- Params
- id (int)
Clone a page into a new draft with a "-copy" (and, on collision, "-1", "-2", …) slug suffix scoped to the same locale, and a hardcoded " (cópia)" title suffix. Records a revision (action: "duplicate") referencing the source page ID.
- Params
- id (int)
List all revisions for a page, most recent version first.
- Params
- id (int)
Reapply a past revision's snapshot fields to the page. Always sets status back to draft regardless of the snapshot's captured status — a restored "published" snapshot must be re-published explicitly. Records a new revision (action: "restore") referencing the source revision ID.
- Params
- id (int), revisionId (int)
List the component instances placed on a page, ordered by layout region then order.
- Params
- pageId (int)
Full-replace save of a page's component instances: deletes every existing instance for the page and bulk-recreates them from the submitted array in one call. Omitting an existing instance deletes it — this is not an incremental upsert. Each item: id? (number, existing instance to keep), region_code (string, required, resolved to layout_region_id scoped to the page's own layout), component_slug (string, required, resolved to component_id), name?, props? (object), content? (object), order? (number), is_visible? (boolean), parent_id? (number, for nested instances).
- Params
- pageId (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| instances | CmsPageComponentItemDto[] | yes | Full desired list of component instances for the page — see fields below |
Update a single component instance in place (unlike the bulk save endpoint, this targets one instance by ID).
- Params
- pageId (int), id (int) — component instance ID
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| props | object | no | — |
| content | object | no | — |
| name | string | no | — |
| order | number | no | — |
| is_visible | boolean | no | — |
Remove a single component instance from a page.
- Params
- pageId (int), id (int) — component instance ID