mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
189 lines
9.5 KiB
Markdown
189 lines
9.5 KiB
Markdown
# 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](#overview)
|
|
- [Schema](#schema)
|
|
- [pages](#pages)
|
|
- [landing_pages](#landing_pages)
|
|
- [page_sections](#page_sections)
|
|
- [page_blocks](#page_blocks)
|
|
- [page_templates](#page_templates)
|
|
- [page_user_groups](#page_user_groups)
|
|
- [Relationships](#relationships)
|
|
- [Design Decisions](#design-decisions)
|
|
- [ENUM Types](#enum-types)
|
|
|
|
---
|
|
|
|
## 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` |
|