Skip to main content

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;
}
FieldNotes
nameRequired on create. Minimum length 1.
url_keyRequired 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.
statusAccepts true, false, 0, 1, '0', '1'. Normalised to a boolean before the write.
publish_start / publish_endTimestamps 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

ParameterTypeDescription
dataLandingPageDataThe page. name and url_key are required here.
contextobjectHook context, bound as this inside hook callbacks. Pass {} when you have nothing to add.

Return Value

The inserted landing_page row.

What it does

  1. Runs the inbound data through the landingPageDataBeforeCreate registry value, then normalises it.
  2. Validates against the landing-page JSON schema with name and url_key required.
  3. Asserts the url_key is not already taken by another URL owner.
  4. Inserts the row.
  5. Writes the url_rewrite row mapping /<url_key>/landing/<url_key>.

Throws

ConditionMessage
context is a non-object truthy valueContext must be an object
Schema validation failsThe first AJV error message
url_key collides with an existing URLRaised 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

  1. Runs the data through landingPageDataBeforeUpdate, normalises and validates it.
  2. Loads the current row.
  3. If url_key changed, asserts the new slug is free.
  4. Updates the row.
  5. Re-syncs the url_rewrite entry to the (possibly new) url_key.
  6. On a rename, records a 302 redirect /<oldKey>/<newKey>, keyed by the page's URN, so existing links keep working.

Throws

ConditionMessage
No such pageRequested landing page not found
context is a non-object truthy valueContext must be an object
Schema validation failsThe first AJV error message
The new url_key is takenRaised 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

  1. Purges historical redirect aliases for the page's URN, so old URLs stop 302ing.
  2. Removes the url_rewrite row, so /<url_key> stops resolving.
  3. Deletes the page body — every widget_placement row whose entity_urn is this page.
  4. Deletes the landing_page row.
The body does not cascade

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

FieldValue on the copy
statusfalse — 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_endCopied 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.

ParameterTypeDescription
connectionPoolClientRequired. 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
  • URNPromotionUrn.landingPage(uuid), the key placements are scoped by