Public Pages (/p)
Overview
The /p route group holds the pages that are reachable without a CMS login. They exist so that end users — the LINE OA's own customers — can open content or menus through a token-bearing link.
Two things set this group apart from every other screen in the product:
- It is not wrapped in the CMS shell. No header, side menu, breadcrumb, or quick-access bar. The root layout detects the path and renders only the page body.
- It does not use the shared CMS API client, which attaches an administrator token and forces sign-out on a 401. These pages call the public API directly, because whoever opens them has no CMS account.
The group contains three pages: a content viewer (with LIFF login and password support), a public menu viewer, and a link-testing utility for the development team.
Business Flow
Public content page
- The token is read from the query string first. If absent, the page falls back to the
liff.statevalue that LINE supplies in some redirect scenarios. - The page first fetches the content's LIFF configuration to learn whether authentication is required and which LIFF ID to use.
- When authentication is required, the LIFF library is imported dynamically and initialised. If the user is already signed in, their ID token is captured for subsequent requests. If they are not signed in but are inside the LINE in-app browser, the page triggers login and reloads afterwards. If initialisation fails outright, the page proceeds and lets the server return an access denial.
- The content itself is then fetched, carrying the ID token when one exists and the password when the user has entered one.
- The response is interpreted four ways: a password is required (render the password card, with an error message when the password is wrong); content was returned (set the browser title from the content metadata); access was denied (render a denial card with the server's message); or another error occurred (render an alert).
- Once content is available, if both Thai and English translations exist the page shows a sticky language toggle and renders the body through a read-only viewer. If the selected language has no translation, an informational notice appears instead.
Public menu page
- The token is read from the query string and used to fetch the full menu configuration from the public API.
- The page renders one of four states: loading, error, configuration not found, or the menu itself through the public renderer.
- The renderer supports navigating into submenus, back and home controls once below the root, and a search box that scans the whole tree and shows the path of items whose names collide.
- Selecting a folder item navigates into its children; selecting an outbound link opens a new tab.
- Layout follows the configured template — grid or list — with button styling coming from the menu theme and then overridden by any per-item customisation.
Link testing page
A developer utility that displays example content and menu URLs with open and copy buttons, alongside a card summarising the testing steps and the relevant endpoints.
Key Screens & Components
Layout branching (src/app/layout.tsx) — detects paths beginning with /p/ and renders the page body alone, with none of the CMS chrome. The layout inside the /p group is a pass-through.
Content page (src/app/p/content/page.tsx) — owns the LIFF initialisation sequence, content fetching, the password gate, and language switching.
Menu page (src/app/p/menu/page.tsx) — loads the public menu configuration and hands it to the renderer.
Test page (src/app/p/test/page.tsx) — the development utility.
Components shared with the CMS — the read-only content viewer (TiptapViewer), which uses the same extension set as the authoring editor, and the public menu renderer (MenuPublicView), which is the same component the Menu Builder previews with.
Public endpoints actually called — the content LIFF configuration, the content read by token (accepting a password as a query parameter and an ID token as a header), and the menu read by token.
Dependencies
- No client-side permission checks — access control lives entirely on the server, enforced through the token, optional LIFF ID token verification, and the password gate.
- Content Management — issues the public token and owns the settings that decide whether authentication, a password, or audience restrictions apply.
- Menu Builder — issues the menu's public token.
- LINE OA Management — the source of the LIFF ID and OA hash the CMS uses to assemble these links before they are distributed.
- External libraries — LINE's LIFF SDK is imported dynamically, and only when the content actually requires authentication, so ordinary public content is not slowed down.
- Environment configuration — every request targets the API base URL defined by
NEXT_PUBLIC_BASE_CONSOLE_API_URL.
Backend Details (CMS API)
The endpoints these /p pages call live in internal/modules/publicmod/, where every route is declared public and no JWT is checked at all. What stands in for authorization is an unguessable token, a rate limit, and per-page conditions (password / LIFF).
Endpoints and how each is controlled
GET /api/public/contents— lists published content and returns pagination metadata (total, current page, page size, total pages). Rate limited to 10 requests per 60 seconds.GET /api/public/contents/categories— lists categories that actually contain content, takinglineOaIdas a query parameter. Also rate limited to 10 per 60 seconds.GET /api/public/content/:token/liff-config— returns the page's LIFF configuration so the frontend can initialize LIFF before loading content. Not rate limited.GET /api/public/content/:token— reads content by token. Not rate limited.GET /api/public/menu/:token— reads a menu structure by token. Not rate limited.
Why some routes are throttled and others are not — the listing routes can be called with no prior knowledge, so they carry a throttle. Token routes are considered protected by the token being unguessable. Security note: once a token leaks there is no rate ceiling slowing anything down, which makes token regeneration the only mechanism that genuinely cuts off access.
What the backend checks when reading content by token
- If the request carries an
x-liff-tokenheader, the backend uses it to identify the LINE user currently viewing, which makes attributed view tracking possible — this is the mechanism that lets audience restriction work at all. - If the page is password-protected, the correct
passwordmust be supplied as a query parameter or no content is returned. - If the page is not found, or is found but not published, the backend answers with error code
APP_007. It deliberately does not distinguish "missing" from "unpublished", so a caller cannot probe whether a given token exists.
Public routes living outside this module
Other modules expose public endpoints too, worth knowing when tracing a link:
GET /api/public/content-pages/:tokenandPOST /api/public/content-pages/:token/track— in the content management module (the latter records a view).GET /api/menu-builder/public/:token— in the menu builder module, running parallel toGET /api/public/menu/:token.
A soft-delete caveat that hits public pages directly
- The
content_page,content_category, andcontent_subcategorytables use adeleted_datecolumn that is not wired into the ORM's automatic soft-delete filtering. Every read must add the predicate by hand; miss it in one place and deleted content can resurface on a public page. - The
line_oatable does use the automatic mechanism, so it is filtered without extra code. - The legacy code contains a soft-delete filter pointing at a field that does not actually exist, making it a no-op — a concrete illustration of how easily hand-written predicates go wrong.
Route ordering
Static routes such as /api/public/contents must be registered before the :token parameter routes, otherwise the literal contents would be captured as a token.