Skip to main content

Content Page Management

Overview

Content Management is the module for creating and editing content pages for a LINE OA. Page bodies are stored as HTML authored through a rich text editor (TipTap), and a single record carries both languages side by side (Thai and English) as one set of translations.

The feature is aimed at marketing and content teams that need to publish articles, promotion pages, or service information for users to open via a link or QR code from the LINE OA, without requiring additional web development.

Each content page holds the following data:

GroupDescription
Core fieldsThe slug used to build the URL, and the page status (draft / published / archived)
Per-language contentTitle, HTML body, excerpt, and the meta/OG tag set for each language
CategorizationPrimary category and subcategory
Access controlAllowed audiences and optional password protection
Friend TrackThe linked add-friend campaign and its CTA button label
Campaign settingsUTM parameters for source tracking and analytics configuration (GA, Facebook Pixel, LINE Tag, custom scripts)

A content page can be in one of three states:

StatusMeaning
DraftSaved but not yet visible to users — used for work in progress
PublishedReachable through the public link or QR code
ArchivedNo longer reachable, but retained in the system for reference

Key points to be aware of:

  • After saving, the backend issues a publicToken for each page, used to build the public URL served by client-web.
  • The list table shows total and unique view counters for every page, so page performance can be compared without opening an external analytics tool.
  • Audience restriction and Friend Track campaigns are mutually exclusive — configuring one disables the other, since a page already limited to a known audience has no need for an add-friend call to action.
  • Publication state is changed through the save actions in the form (save as draft, or publish); there is no separate status toggle on the list page.
  • Both language translations live in the same record, so editing a page always edits both languages together rather than maintaining two separate entries.

Business Flow

Browsing the content page list

  1. Opening /content-management loads a paginated list with a search term and a status filter (all / published / draft / archived).
  2. The system reads the current LINE OA to obtain its LIFF ID and OA hash, which are needed to build the public URLs behind the copy-link and QR code actions.
  3. The copy-link action picks a URL shape based on the page's configuration. If the page is audience-restricted and the LINE OA has a LIFF ID, a LIFF URL is produced (https://liff.line.me/<liffId>/<lineOaHash>/content/<slug>); otherwise a standard web URL is used (<BASE_APP_URL>/<lineOaHash>/content/<slug>). Pages without a slug fall back to the publicToken.
  4. The QR action opens a dialog showing the QR code for that same URL, with a PNG download.
  5. Deletion requires confirmation; on success the table refreshes automatically.

Creating a content page

  1. The create action opens the multi-language form (/content-management/create-multilang), a two-column screen with the editor on the left and a live preview on the right.
  2. The preview panel simulates three viewport widths (full-width desktop, 768px tablet, 375px mobile) and follows the language tab currently being edited.
  3. Content tab — enter the title per language; the slug is generated automatically from the title and can still be overridden. The body is written in the rich text editor, and a collapsible SEO section covers meta title, meta description, excerpt, OG title, OG description, and OG image.
  4. Choose a category from the primary category list; selecting one loads that category's hierarchical subcategories for further selection.
  5. Access Control tab — select one or more audiences to restrict viewers, and/or enable password protection with a password.
  6. Friends Track tab — pick a campaign from the list of active campaigns and set the CTA button label, with a preview of the LINE-styled button.
  7. Campaign Settings tab — enter UTM values, where source and medium can be chosen from presets or typed freely while campaign, term, and content are free-text fields. Analytics settings cover enabling Google Analytics with its event list, Facebook Pixel events, LINE Tag events, and custom scripts injected into the page head. A preview of the resulting URL is shown for each language.
  8. Save as draft or publish. The system validates that at least one language has a title and that a slug is present, then creates the record and returns to the list.

Editing a content page

  1. The edit screen at /content-management/edit takes the content page ID from the query string.
  2. The existing record is loaded and mapped back into the full form structure: both language translations, category, audiences, Friend Track, password protection, UTM, and analytics.
  3. Saving works the same way as creation, except that the status is only submitted when the user presses an action that explicitly sets it. A plain save leaves the current publication state untouched.
  4. Published pages can be edited and saved over at any time; visitors using the existing link see the updated content, and neither the link nor the QR code changes.

Publishing and tracking

  1. Once a page is published, copy its link or download the QR code from the list and distribute it through the desired channel — a rich menu, a broadcast message, or offline media.
  2. If UTM values are configured, the generated link carries the tracking parameters automatically, allowing traffic sources to be distinguished in analytics tools.
  3. When a visitor opens the page, the configured analytics scripts are injected and begin recording the specified events, while the system also records the page view that feeds the counters on the list page.
  4. If password protection is enabled, the visitor must supply the correct password before the content is shown; if the page is audience-restricted, eligibility is checked against the LINE OA context before rendering.

Key Screens & Components

List page (/content-management)

  • Filter bar — a search field and a status selector, alongside pagination.
  • Content table — title, slug, category, status, view counts, and last-updated timestamp.
  • Copy link and QR actions — build the public URL in the shape appropriate to the page's configuration and render a downloadable QR code.
  • Delete action — guarded by an inline confirmation popover.

The system builds public URLs in two shapes:

Page configurationLink format
Audience-restricted and the LINE OA has a LIFF IDhttps://liff.line.me/<liffId>/<lineOaHash>/content/<slug>
Not audience-restricted, or no LIFF ID available<BASE_APP_URL>/<lineOaHash>/content/<slug>

In both shapes the publicToken takes the place of the slug when a page has none.

Primary file: src/app/content-management/page.tsx

Create page (/content-management/create-multilang) and edit page (/content-management/edit)

  • Multi-language form (src/components/content-management/MultiLanguageForm.tsx) — Thai/English tabs covering title, slug, body, and the collapsible SEO section.
  • Rich text editor (src/components/content-management/TiptapEditorSimple.tsx) — the module's main authoring surface, covering:
    • Basic formatting such as headings, bold, italic, ordered and unordered lists, and blockquotes.
    • Links, text alignment, highlighting, line height, and text background colour.
    • Direct image upload into the body, via file picker or by dragging a file onto the editing area.
    • In-place resizing of inserted images; the saved result is HTML that renders as-is on the public page.
  • Content viewer (src/components/content-management/TiptapViewer.tsx) — renders the stored HTML read-only; used both in the preview panel and on the public page.
  • Category selector (src/components/content-management/CategoryCascader.tsx) — a cascader that loads top-level categories first and fetches hierarchical subcategories on expand, supporting up to four levels.
  • Live preview panel — renders the page at the chosen viewport width in the language currently being edited, making it easy to check the mobile layout before publishing.

The four tabs in the left-hand column divide the configuration as follows:

TabCovers
ContentTitle, slug, body, and SEO fields per language, plus category selection
Access ControlPermitted audiences and password protection
Friends TrackThe add-friend campaign and its CTA button label
Campaign SettingsUTM values and analytics configuration, with a preview of the resulting URL

API service

All calls live in src/services/content-page.service.ts under the content-pages base path.

CapabilityEndpoint
List content pagesGET /content-pages
Get a single pageGET /content-pages/{id}
Create a pagePOST /content-pages
Update a pagePATCH /content-pages/{id}
Delete a pageDELETE /content-pages/{id}
Upload an image into the bodyPOST /content-pages/upload-image

The service also exposes additional operations reserved for future use — publish, unpublish, archive, duplicate, standalone UTM and analytics updates, regenerating the publicToken, plus fetching public content and recording views. No screen in the CMS currently calls this group.

Dependencies

  • Permissions — the parent menu is gated by the content-management module permission and the child menu by content-page; both are unlocked by the backend line-oa module.
  • Content Category / Subcategory — the source of the category and subcategory options in the form.
  • Audience — defines who may open the page and determines whether the public link is issued as a LIFF URL or a plain web URL.
  • Friend Track — supplies the campaigns bound to the add-friend CTA button on the page.
  • LINE OA Management — provides the LIFF ID and OA hash used to assemble public links and QR codes.
  • Content Links — aggregates content pages into filtered link lists.
  • Menu Builder — offers published content pages as menu targets, referenced by the page token.
  • Public pages on client-web — the actual destination users reach from a link or QR code, and where the configured analytics scripts execute.
  • Shared infrastructure — the CMS HTTP client (automatic bearer token and sign-out on expiry), the breadcrumb and side-menu system, and the shared translation namespaces used across the CMS.

Backend Details (CMS API)

The backend lives in internal/modules/contentpage/. It splits cleanly into two route groups: the administrative routes, which require a login, and the public routes, which readers reach without one.

Required permissions

  • The administrative routes only require the shared authentication check (JWT). They are not wrapped in a module gate, and the declared policy references the line-oa module as metadata that is not yet enforced.
  • As a result, per-menu access control for this feature is enforced mainly on the front end; any signed-in user can still call the content management endpoints directly.
  • The public routes carry no guard at all, by design, since general readers must be able to reach them.

What the backend generates, and how pages are protected

  • On creation the backend generates the public token using a cryptographically secure 32-byte random value, making it unguessable — which is what allows the public link to be distributed safely.
  • New pages always start in draft status; saving never publishes on its own.
  • Page passwords are hashed with bcrypt before storage rather than being kept in plain text, and opening a locked page compares against that hash.
  • The body image upload endpoint checks the declared content type before accepting the file.

What gets stored, and the side effects

  • Data spans several tables: content_page (the page), content_page_translation (per-language content), content_page_view and content_page_analytics (visit data), and content_page_utm (source tracking values), with references into content_category and content_subcategory.
  • View counting happens through a separate public endpoint that the reader-facing page calls; it is not a side effect of serving the page. That call increments the counter and records an event for the statistics.
  • Body images live on object storage, while the saved content itself is directly renderable HTML.
  • This module publishes no queue work and writes no cache, so content edits are visible on the public page immediately.

Backend capabilities the UI does not yet call

The API supports more than the current screens use:

  • POST /api/content-pages/:id/publish and POST /api/content-pages/:id/archive for changing status independently of saving the content.
  • POST /api/content-pages/:id/duplicate for copying a whole page, images included.
  • GET /api/content-pages/:id/analytics for per-page visit analytics that go well beyond the counters shown in the list table.

How the public routes behave

  • GET /api/public/content-pages/:token is the route readers use to open a page; locked pages must supply the password as well.
  • Every failure case collapses into the same "content not found" error — an invalid token, an unpublished page, or a wrong password all look identical. That is good for security, because someone guessing tokens cannot tell whether a page exists.
  • Security note — the page password is passed in the query string, where it can end up in server logs, proxy logs, and browser history. Users should not reuse a password they use elsewhere.
  • A second, similar public route exists in the public API module with additional LIFF support, so any change to public page behaviour has to account for both paths.

Edge cases worth knowing

  • The content_page table uses a plain deletion-timestamp column rather than the ORM's automatic soft-delete mechanism. Every read query has to spell out the "not deleted" condition itself; forgetting it makes deleted pages reappear in results — including on the public route.
  • The two language translations live in a separate translations table rather than in the page row, so adding a third language later would not require changing the core structure.
  • Because the public token is not regenerated when content is edited, links and QR codes already distributed keep working and always point at the latest version.
  • The view counts shown in the table depend on the reader-facing page successfully calling the tracking endpoint, so they can under-report when a visitor leaves quickly or the browser blocks the call.