Landing Page Functions
A landing page is a promotion-owned page whose body is built entirely from page-builder widgets. It lives in the landing_page table, is served at the root-level friendly URL /<url_key> (mapped to the internal landingPageView route), and its content is a set of widget_placement rows scoped by the page's URN.
Import
import {
createLandingPage,
updateLandingPage,
deleteLandingPage,
duplicateLandingPage,
getLandingPagesBaseQuery,
syncLandingPageUrlRewrite,
deleteLandingPageUrlRewrite
} from '@evershop/evershop/promotion/services';
import type { LandingPageData } from '@evershop/evershop/promotion/services';
The data shape
interface LandingPageData {
name: string;
url_key: string;
status?: boolean | number | string;
description?: string | null;
meta_title?: string | null;
meta_description?: string | null;
publish_start?: string | null;
publish_end?: string | null;
}
| Field | Notes |
|---|---|
name | Required on create. Minimum length 1. |
url_key | Required on create. Must match ^[a-z0-9]+(?:-[a-z0-9]+)*$ — lowercase, digits, single hyphens. Unique across the table, and checked for collisions against other URL owners. |
status | Accepts true, false, 0, 1, '0', '1'. Normalised to a boolean before the write. |
publish_start / publish_end | Timestamps bounding the publish window. An empty string is normalised to null (a cleared datetime field posts '', which TIMESTAMPTZ rejects). null bounds mean open-ended. |
Every write runs inside its own transaction: it opens a connection, startTransaction, does the work, and commits — or rolls back and rethrows.
createLandingPage
createLandingPage(data: LandingPageData, context: object): Promise<LandingPage>
Parameters
| Parameter | Type | Description |
|---|---|---|
data | LandingPageData | The page. name and url_key are required here. |
context | object | Hook context, bound as this inside hook callbacks. Pass {} when you have nothing to add. |
Return Value
The inserted landing_page row.
What it does
- Runs the inbound data through the
landingPageDataBeforeCreateregistry value, then normalises it. - Validates against the landing-page JSON schema with
nameandurl_keyrequired. - Asserts the
url_keyis not already taken by another URL owner. - Inserts the row.
- Writes the
url_rewriterow mapping/<url_key>→/landing/<url_key>.
Throws
| Condition | Message |
|---|---|
context is a non-object truthy value | Context must be an object |
| Schema validation fails | The first AJV error message |
url_key collides with an existing URL | Raised by the URL-key availability check |
Example
import { createLandingPage } from '@evershop/evershop/promotion/services';
const page = await createLandingPage(
{
name: 'Black Friday 2026',
url_key: 'black-friday-2026',
status: true,
meta_title: 'Black Friday — up to 50% off',
publish_start: '2026-11-25T00:00:00Z',
publish_end: '2026-12-02T00:00:00Z'
},
{}
);
console.log(page.uuid);
Hooks
createLandingPage (whole call) and insertLandingPageData (just the insert). Helpers: hookBeforeCreateLandingPage, hookAfterCreateLandingPage, hookBeforeInsertLandingPageData, hookAfterInsertLandingPageData.
updateLandingPage
updateLandingPage(uuid: string, data: Partial<LandingPageData>, context: object): Promise<LandingPage>
Patch a landing page. Every field is optional — an update with no changed columns is tolerated rather than throwing.
What it does
- Runs the data through
landingPageDataBeforeUpdate, normalises and validates it. - Loads the current row.
- If
url_keychanged, asserts the new slug is free. - Updates the row.
- Re-syncs the
url_rewriteentry to the (possibly new)url_key. - On a rename, records a 302 redirect
/<oldKey>→/<newKey>, keyed by the page's URN, so existing links keep working.
Throws
| Condition | Message |
|---|---|
| No such page | Requested landing page not found |
context is a non-object truthy value | Context must be an object |
| Schema validation fails | The first AJV error message |
The new url_key is taken | Raised by the URL-key availability check |
Example
import { updateLandingPage } from '@evershop/evershop/promotion/services';
// Renaming keeps the old URL alive as a 302.
await updateLandingPage(
pageUuid,
{ url_key: 'black-friday', status: true },
{}
);
Hooks
updateLandingPage and updateLandingPageData, with the four matching helpers.
deleteLandingPage
deleteLandingPage(uuid: string, context: object): Promise<LandingPage>
Delete a landing page and everything hanging off it. Returns the row as it was before deletion.
What it does, in one transaction
- Purges historical redirect aliases for the page's URN, so old URLs stop 302ing.
- Removes the
url_rewriterow, so/<url_key>stops resolving. - Deletes the page body — every
widget_placementrow whoseentity_urnis this page. - Deletes the
landing_pagerow.
widget_placement.entity_urn is a plain varchar with no foreign key to landing_page. Dropping the entity row alone would orphan every placement, which is why step 3 is explicit. If you write your own landing-page teardown, you must delete placements by URN yourself.
Throws
Invalid landing page id when no row matches the UUID, plus Context must be an object.
Hooks
deleteLandingPage and deleteLandingPageData.
duplicateLandingPage
duplicateLandingPage(uuid: string, context: object): Promise<LandingPage>
Deep-clone a landing page. Returns the new row.
What the copy looks like
| Field | Value on the copy |
|---|---|
status | false — the copy is always an unpublished draft |
name | <source name> (copy) |
url_key | <source key>-copy, or -copy-2, -copy-3, … until free (url_key is UNIQUE) |
description, meta_title, meta_description, publish_start, publish_end | Copied verbatim |
Deep clone, not shallow
Widget settings live on widget_instance, not on widget_placement. A placement-only copy would leave both pages pointing at the same instances, so editing the copy would silently mutate the original. duplicateLandingPage therefore clones each referenced widget_instance and repoints the cloned placements at the new instances and the new page URN.
Throws
Invalid landing page id when the source does not exist, plus Context must be an object.
Example
import { duplicateLandingPage } from '@evershop/evershop/promotion/services';
const draft = await duplicateLandingPage(pageUuid, {});
// draft.status === false, draft.url_key === 'black-friday-copy'
Hooks
duplicateLandingPageData, with hookBeforeDuplicateLandingPageData / hookAfterDuplicateLandingPageData.
getLandingPagesBaseQuery
getLandingPagesBaseQuery(): SelectQuery
A fresh select().from('landing_page') query. This is the base every landing-page listing builds on — GraphQL collections, admin grids, your own filters. Each call returns a new query object, so it is safe to mutate.
import { getLandingPagesBaseQuery } from '@evershop/evershop/promotion/services';
import { pool } from '@evershop/evershop/lib/postgres';
const query = getLandingPagesBaseQuery();
query.where('status', '=', true);
query.orderBy('landing_page_id', 'DESC');
const rows = await query.execute(pool);
syncLandingPageUrlRewrite
syncLandingPageUrlRewrite(
connection: PoolClient,
landingPage: { uuid: string; url_key: string }
): Promise<void>
Keep a landing page's url_rewrite row in sync with its current url_key. Writes request_path: '/<url_key>' → target_path: '/landing/<url_key>', keyed on entity_uuid so a rename overwrites the single row in place.
The CRUD services above already call it; you need it only when you write landing_page rows directly — a data migration, an importer.
| Parameter | Type | Description |
|---|---|---|
connection | PoolClient | Required. There is no default — pass the transaction client doing the write. |
landingPage | { uuid, url_key } | The page whose rewrite should be written. |
The companion deleteLandingPageUrlRewrite(connection, uuid) removes the row.
import { syncLandingPageUrlRewrite } from '@evershop/evershop/promotion/services';
await syncLandingPageUrlRewrite(connection, {
uuid: page.uuid,
url_key: page.url_key
});
See Also
- Landing Page API — The REST endpoints behind the admin UI
- Page Builder — How the page body is composed from widgets
- URL Redirects — The rename-redirect machinery
- URN —
PromotionUrn.landingPage(uuid), the key placements are scoped by