ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:06:58 +08:00
parent bf48c95467
commit 439bb33f77
189 changed files with 17559 additions and 686 deletions
@@ -0,0 +1,188 @@
# 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` |