Content Pages
Overview
This domain has 5 tables: content_page is the main table holding page data (slug, status,
access control, statistics), while the other 4 are extensions keyed on content_page_id:
content_page_translation (per-language content and SEO), content_page_utm (UTM parameters,
1:1), content_page_analytics (tracking scripts, 1:1) and content_page_view (per-visit view
log)
Table content_page
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | SERIAL | NO | nextval(...) | Primary key |
uuid | UUID | NO | gen_random_uuid() | External reference id for the page (unique) |
line_oa_id | INTEGER | NO | - | LINE OA the page belongs to (FK to line_oa.id) |
user_id | INTEGER | NO | - | User who owns the page |
organization_id | INTEGER | NO | - | Organization that owns the page |
slug | VARCHAR(255) | NO | - | Page slug used in the URL |
status | ContentPageStatus (ENUM) | NO | draft | Page status: draft, published, archived |
version | INTEGER | NO | 1 | Page version number |
public_token | VARCHAR(255) | YES | - | Token for opening the page publicly (unique) |
require_auth | BOOLEAN | NO | false | Whether login is required to view the page |
password_protected | BOOLEAN | NO | false | Whether the page is password protected |
password_hash | VARCHAR(255) | YES | - | Hash of the password protecting the page |
published_at | TIMESTAMPTZ(3) | YES | - | Publication timestamp |
published_by | INTEGER | YES | - | Publisher (user id) |
archived_at | TIMESTAMPTZ(3) | YES | - | Archive timestamp |
archived_by | INTEGER | YES | - | User who archived the page (user id) |
view_count | BIGINT | NO | 0 | Total number of views |
unique_view_count | BIGINT | NO | 0 | Number of unique viewers |
category_id | INTEGER | YES | - | Page category (FK to content_category.id) |
subcategory_id | INTEGER | YES | - | Page subcategory (FK to content_subcategory.id) |
audience_ids | JSONB | YES | '[]' | Audiences allowed to see this page |
friend_track_campaign_id | INTEGER | YES | - | Friend Track campaign linked to this page (FK to friend_track_campaign.id) |
friend_track_button_text | VARCHAR(255) | YES | - | Label shown on the Friend Track button in the page |
created_date | TIMESTAMPTZ(3) | NO | now() | Creation timestamp |
created_by | INTEGER | NO | - | Creator (user id) |
updated_date | TIMESTAMPTZ(3) | NO | now() | Last update timestamp |
updated_by | INTEGER | NO | - | Last updater (user id) |
deleted_date | TIMESTAMPTZ(3) | YES | - | Deletion timestamp (soft delete) |
deleted_by | INTEGER | YES | - | User who deleted the page (user id) |
Table content_page_translation
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | SERIAL | NO | nextval(...) | Primary key |
content_page_id | INTEGER | NO | - | Content page this translation belongs to (FK to content_page.id) |
language | VARCHAR(10) | NO | - | Translation language code, e.g. th, en |
title | VARCHAR(500) | NO | - | Page title in this language |
content | TEXT | YES | - | Page content in this language |
excerpt | TEXT | YES | - | Page excerpt / teaser text |
meta_title | VARCHAR(160) | YES | - | SEO meta title |
meta_description | VARCHAR(320) | YES | - | SEO meta description |
meta_keywords | TEXT | YES | - | SEO meta keywords |
og_title | VARCHAR(160) | YES | - | Open Graph title used when sharing |
og_description | VARCHAR(320) | YES | - | Open Graph description used when sharing |
og_image_url | TEXT | YES | - | Open Graph image URL |
twitter_card | VARCHAR(50) | YES | - | Twitter card type |
created_date | TIMESTAMPTZ(3) | NO | now() | Creation timestamp |
created_by | INTEGER | NO | - | Creator (user id) |
updated_date | TIMESTAMPTZ(3) | NO | now() | Last update timestamp |
updated_by | INTEGER | NO | - | Last updater (user id) |
Table content_page_utm
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | SERIAL | NO | nextval(...) | Primary key |
content_page_id | INTEGER | NO | - | Content page this row belongs to (unique — 1:1 relation) |
utm_source | VARCHAR(255) | YES | - | Default UTM source for the page |
utm_medium | VARCHAR(255) | YES | - | Default UTM medium for the page |
utm_campaign | VARCHAR(255) | YES | - | Default UTM campaign for the page |
utm_term | VARCHAR(255) | YES | - | Default UTM term for the page |
utm_content | VARCHAR(255) | YES | - | Default UTM content for the page |
created_date | TIMESTAMPTZ(3) | NO | now() | Creation timestamp |
created_by | INTEGER | NO | - | Creator (user id) |
updated_date | TIMESTAMPTZ(3) | NO | now() | Last update timestamp |
updated_by | INTEGER | NO | - | Last updater (user id) |
Table content_page_analytics
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | SERIAL | NO | nextval(...) | Primary key |
content_page_id | INTEGER | NO | - | Content page this row belongs to (unique — 1:1 relation) |
ga_events | JSONB | YES | - | Google Analytics event configuration |
fb_pixel_events | TEXT | YES | - | Facebook Pixel event configuration |
line_tag_events | TEXT | YES | - | LINE Tag event configuration |
custom_head_scripts | TEXT | YES | - | Extra scripts injected into the page head |
custom_body_scripts | TEXT | YES | - | Extra scripts injected into the page body |
enable_ga | BOOLEAN | NO | true | Whether Google Analytics is enabled |
enable_fb_pixel | BOOLEAN | NO | false | Whether Facebook Pixel is enabled |
enable_line_tag | BOOLEAN | NO | false | Whether LINE Tag is enabled |
created_date | TIMESTAMPTZ(3) | NO | now() | Creation timestamp |
created_by | INTEGER | NO | - | Creator (user id) |
updated_date | TIMESTAMPTZ(3) | NO | now() | Last update timestamp |
updated_by | INTEGER | NO | - | Last updater (user id) |
Table content_page_view
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | SERIAL | NO | nextval(...) | Primary key |
content_page_id | INTEGER | NO | - | Content page that was viewed (FK to content_page.id) |
viewer_ip | VARCHAR(45) | YES | - | Viewer IP address (IPv6 capable) |
viewer_user_agent | TEXT | YES | - | Viewer user agent |
viewer_referrer | TEXT | YES | - | Referrer URL the viewer arrived from |
viewer_line_uid | VARCHAR(255) | YES | - | Viewer LINE user id (when identifiable) |
utm_source | VARCHAR(255) | YES | - | UTM source carried by the visit |
utm_medium | VARCHAR(255) | YES | - | UTM medium carried by the visit |
utm_campaign | VARCHAR(255) | YES | - | UTM campaign carried by the visit |
utm_term | VARCHAR(255) | YES | - | UTM term carried by the visit |
utm_content | VARCHAR(255) | YES | - | UTM content carried by the visit |
session_id | VARCHAR(255) | YES | - | Viewer session id (used to count unique views) |
language | VARCHAR(10) | YES | - | Language the viewer opened the page in |
device_type | VARCHAR(50) | YES | - | Viewer device type |
viewed_at | TIMESTAMPTZ(3) | NO | now() | Visit timestamp |
Notes
content_pagehas a unique constraint on (line_oa_id,slug) — a slug must be unique within one OA, but different OAs may reuse the same slug- Both
content_page.public_tokenandcontent_page.uuidhave unique constraints - All 4 child tables have a FK to
content_page.idwithON DELETE CASCADE— deleting a page removes its translations, UTM row, analytics row and view logs content_page_utm.content_page_idandcontent_page_analytics.content_page_idare unique, making them 1:1 relations withcontent_pagecontent_page_translationhas a unique constraint on (content_page_id,language) — one translation per language per pagecontent_page.friend_track_campaign_idis a FK tofriend_track_campaign.idwithON DELETE SET NULL— deleting the campaign keeps the page and nulls the referencecontent_page.category_id/subcategory_idare FKs tocontent_category.idandcontent_subcategory.id(see Content Categories and Link Collections)content_page.view_countandunique_view_countare counters aggregated fromcontent_page_view, which stores per-visit logs and is indexed oncontent_page_id,session_idandviewed_at