Public Menu (Menu Builder)
Overview
The public menu page renders a menu designed by admins in the CMS menu builder. It behaves much like a small menu app: users can drill down and back up through multiple levels. Supported capabilities include four button kinds, audience-based filtering, per-button theming and styling, background images and icons, and cross-menu search that also reaches into the articles inside inline content links.
At roughly 1,170 lines this is the largest file in the project, because it combines navigation logic, style rendering, and the search mechanism in one place.
Business Flow
- The user lands on
/:hash/menu/:token. - The app fetches the menu without attaching a token first, to read the menu configuration and the LIFF ID it needs.
- It then checks recursively whether any menu item defines audience conditions:
- If none do, LIFF is skipped entirely — the app does not need to know who the user is, so the menu opens fast.
- If any do, it performs a LIFF login and retrieves the ID token.
- The menu is fetched again with
x-liff-tokenattached, so the API filters menu items by audience before returning them. - The document title is set from the menu name.
- Buttons are rendered according to the configured template (grid or list), combined with the menu theme and each button's own style overrides, covering colours, gradients, corner radius, shadow, height, text position, and images used as backgrounds, icons, or full-button artwork.
- Tapping a button triggers behaviour based on the item type, as described below.
- The back and home buttons manage the current navigation path and the inline content list state.
Button behaviour by type
| Type | Behaviour |
|---|---|
| Submenu | Descends into the next menu level within the same screen |
| External link | Opens the configured URL in a new tab |
| Article page | Navigates to the article viewer for that token |
| Content link (navigate) | Navigates to that link's content listing page |
| Content link (inline) | Loads the article list and renders it as a submenu in place, either as cards or as buttons |
Search mechanism
Once the user types two or more characters, the app searches menu item names and URLs recursively across every level.
In parallel, it fetches the contents of every inline content link into a cache so that users can also search article titles and excerpts. Article results are converted into temporary menu item structures, letting them appear seamlessly alongside real menu items in the same result list.
Key Screens & Components
Page
- The menu page (
src/app/[hash]/menu/[token]/page.tsx) holds all the logic: handling button taps, computing which items belong to the current level, assembling button styles, rendering button contents, discovering every inline content link, fetching content into the search cache, filtering items, converting articles into menu items, and handling the back and home actions. - Sub-components for rendering articles as cards and as buttons are defined in the same file.
Hooks, services, and types
- The menu fetch hook (
src/app/menu/[token]/hooks/useFetchMenuByToken.ts) uses only a boolean "has token" flag in its query key rather than the token value itself, avoiding unnecessary refetches. - The menu-side content service (
src/app/menu/[token]/services/content.service.ts) fetches articles by link token and maps API data into content items, preferring the Thai translation, falling back to English, and finally taking the first translation that has a title. - The menu builder service (
src/service/menu-builder.service.ts) is the API access layer for menu retrieval. - Types for menu items, action types, menu configuration, themes, templates, and content link display styles live in
src/app/menu/types/menu.type.ts.
Endpoints used
GET /menu-builder/:token, with thex-liff-tokenheader optional.GET /public-content/links/:linkToken/contentsfor inline content lists.
The src/app/menu/ folder now contains only hooks, services, and types — no page file remains, because the real routes were moved entirely under the hash segment.
Dependencies
- Ant Design provides the search input, buttons, grid system, and loading spinner, alongside the Ant Design icon set.
- LINE Login via LIFF is invoked conditionally, only when the menu defines audience filtering.
- Navigation destinations are the Article Viewer and the Content Link Listing Page.
- All theming and configuration come from the API; nothing is hard-coded on the client beyond the defaults used when the API supplies no value.
Backend Details (Client API)
Audience filtering is a real gate, not hidden UI
This is the most important property of the endpoint: menu items the user is not entitled to see never leave the backend at all — not their name, type, or destination URL. Nothing is sent and then hidden by the web app.
The consequences:
- Opening DevTools reveals nothing about restricted menus; the data never leaves the source.
- Conversely, the web app has no way to know how many items were filtered out, so it cannot display anything like "there are menus you cannot see".
- Comparing the results before and after login (which this page already fetches twice) is the only way to observe the difference.
The recursive filtering rules
The backend walks the menu tree bottom-up:
- Children are always filtered first, then the parent is judged.
- An item with no audience configured is visible to everyone; one with an audience is visible when at least one audience overlaps (OR, not AND).
- A folder (submenu) whose children are all filtered away is itself removed — users never hit an empty folder that opens onto nothing. This is deliberate and worth knowing, because it makes the number of buttons on a level vary per user.
- The backend distinguishes "an item with no children property at all" (the key is absent from the JSON) from "an item whose children is an empty array" (an empty array is returned). The web app must handle both and must not assume children always exists.
- Surviving items are reassembled with a fixed key set (18 fields covering name, type, URL, article and content-link tokens, content-link behaviour and display style, icon, level, ordering, custom style, image, and text display), so the shape is entirely predictable.
Token verification is optional and swallows every error
- The
x-liff-tokenheader is optional; the endpoint works without it. - If a token is supplied but fails verification (expired, wrong channel, and so on), the backend swallows the error entirely and treats the caller as logged out, showing only public items — it never returns 401.
- This is why the initial token-less fetch that this page performs is both safe and genuinely useful.
- Watch out: if a token silently breaks, users see an incomplete menu with no error to catch. When someone reports "menu items disappeared", check the token state before inspecting the menu configuration.
- A token that resolves to no menu → 404
Menu not found.
Response details that matter to the web app
- The backend returns
lineOaHashandlineLiffIdalongside the menu structure, so the web app does not need a separate hash-resolution call to start login. lineLiffIddeliberately distinguishes an empty string from null — a value the admin set to an empty string stays an empty string rather than becoming null; it is null only when the OA has no LINE Login configuration or genuinely lacks that key. The web app should handle both before initializing LIFF.templateandthemeare passed through as raw stored JSON. The backend neither validates them nor fills in defaults — interpreting missing values and applying defaults is entirely the web app's job, which is exactly what this page does.
Edge cases worth knowing
- There is no caching on this endpoint. Every call, including the refetch after login, is a real database read — so a single page open costs two reads, and both count against the application-level rate limit.
- This endpoint records no menu-open statistics, unlike content links, which increment a click count on every call.
- The inline content lists this page renders inside the menu use the content-link endpoint, which has a stricter rate limit (10 requests per 60 seconds per IP) and increments a click count on every call. Prefetching every link's content to build the search cache therefore inflates those links' click numbers and can hit the rate limit on menus with many content links (see Content Link Listing Page).
- Audience filtering relies on the user record tied to the menu's OA. A brand-new user opening it for the first time is auto-provisioned as a guest with no audiences, so they see only public items until they are segmented.