Logotipo Hedhog

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

FieldTypeRequiredNotes
slugstringyes
titlestringyes
descriptionstringno
typeCmsPageType (page | landing | blog_post | blog_index | custom)noDefault: page
statusCmsPageStatus (draft | review | scheduled | published | archived)noDefault: draft
localestringyesLocale code (e.g. "en"), resolved to locale_id
layout_slugstringyesResolved to layout_id; 400 if the layout does not exist
seo_titlestringno
seo_descriptionstringno
author_idnumbernoFalls back to the authenticated user ID if omitted
scheduled_atdate stringno

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

FieldTypeRequiredNotes
instancesCmsPageComponentItemDto[]yesFull 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

FieldTypeRequiredNotes
propsobjectno
contentobjectno
namestringno
ordernumberno
is_visiblebooleanno

Remove a single component instance from a page.

Params
pageId (int), id (int) — component instance ID