Files
starr-philproperties/apps/api/controllers/admin/documentation/landing_pages.md
T

9.5 KiB

Landing Pages / Page Builder — Schema Documentation

Migrations: 41b, 42, 43, 44, 45, 49
Base URL (planned): /api/admin/pages
Guards: authenticate → requireAdmin() → adminLimiter

TODO: Controller, service, and routes for this module have not been created yet.


Table of Contents


Overview

The page builder uses a generalized pages root table as the single owner of all page_sections. This means sections and blocks are not locked to landing_pages — any future page type (lesson_page, course_page, etc.) can reuse the same section/block system by linking to pages.

Hierarchy:

pages
  ├── landing_pages  (1:1 via page_id)
  └── page_sections  (1:N via page_id)
        └── page_blocks  (1:N via page_section_id)

page_templates   — standalone reusable layout snapshots
page_user_groups — controls which groups can see a page

Schema

pages

Root identity table. Every page type gets a row here first.

Column Type Constraints Description
id BIGSERIAL PK
type page_type NOT NULL Discriminator for the page type
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Indexes: idx_pages_type on (type)


landing_pages

Landing-page-specific metadata. One-to-one with pages.

Column Type Constraints Description
id BIGSERIAL PK
page_id BIGINT NOT NULL, UNIQUE, FK → pages(id) Link to root pages table
title VARCHAR(255) NOT NULL
slug VARCHAR(255) NOT NULL, UNIQUE URL path segment
meta_title VARCHAR(255) SEO title override
meta_description TEXT SEO description
status landing_page_status NOT NULL DEFAULT 'draft'
published_at TIMESTAMPTZ Set when first published
created_by BIGINT FK → users(user_id) SET NULL Admin who created the page
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Indexes: idx_landing_pages_status on (status)


page_sections

Ordered sections within any page. References pages, not landing_pages.

Column Type Constraints Description
id BIGSERIAL PK
page_id BIGINT NOT NULL, FK → pages(id) Belongs to a page (any type)
type page_section_type NOT NULL Section layout type
label VARCHAR(255) Admin-facing label
position INT NOT NULL DEFAULT 0 Display order
settings JSONB Background, padding, layout, etc.
is_visible BOOLEAN NOT NULL DEFAULT true Toggle section visibility
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Indexes: idx_page_sections_page_id on (page_id), idx_page_sections_order on (page_id, position)


page_blocks

Content blocks within a section.

Column Type Constraints Description
id BIGSERIAL PK
page_section_id BIGINT NOT NULL, FK → page_sections(id)
type page_block_type NOT NULL Block content type
content JSONB Payload — varies by type (see below)
position INT NOT NULL DEFAULT 0 Display order within section
settings JSONB Block-level style overrides
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Indexes: idx_page_blocks_section_id on (page_section_id), idx_page_blocks_order on (page_section_id, position)

content shape by block type:

type content fields
text { body: string }
image { src: string, alt: string }
button { label: string, href: string, variant: string }
video { src: string, autoplay: boolean }
form { form_id: number }
spacer { height: number }

page_templates

Reusable layout snapshots. Stored as a full JSONB dump of sections + blocks — not live FK references.

Column Type Constraints Description
id BIGSERIAL PK
name VARCHAR(255) NOT NULL Template display name
thumbnail_url TEXT Preview image URL
structure JSONB Full snapshot of sections and blocks
created_at TIMESTAMPTZ NOT NULL DEFAULT now()

page_user_groups

Junction table — controls which user groups can access a page.

Column Type Constraints Description
page_id BIGINT PK, FK → pages(id) ON DELETE CASCADE
group_id BIGINT PK, FK → user_groups(group_id)
created_at TIMESTAMPTZ NOT NULL DEFAULT now()

Indexes: idx_page_user_groups_group_id on (group_id)
Primary Key: composite (page_id, group_id)


Relationships

pages 1 ──── 1 landing_pages
pages 1 ──── N page_sections
pages N ──── N user_groups  (via page_user_groups)
page_sections 1 ──── N page_blocks

Cascade deletes flow top-down: deleting a pages row removes its landing_pages record, all its page_sections, and all nested page_blocks automatically.


Design Decisions

  • pages as root — sections belong to pages, not directly to landing_pages. Adding a new page type (e.g. course_page) only requires a new detail table + adding its value to the page_type ENUM. No changes to page_sections or page_blocks.
  • page_templates.structure is a snapshot — templates store a JSONB copy of sections/blocks, not live FK references. This keeps templates stable when source pages are edited.
  • page_sections.page_id points to pages(id) directly, giving sections access to any page type without schema changes.

ENUM Types

Type Values
page_type landing_page, lesson_page, course_page
landing_page_status draft, published, archived
page_section_type hero, features, cta, testimonials, faq, pricing, gallery, custom
page_block_type text, image, button, video, form, spacer