Skip to main content

Menu Builder

Overview

Menu Builder is used to design multi-level web or LIFF menus and publish them through a link carrying a public token. The defining trait is that the entire menu structure is stored as one JSON document inside a single record rather than as a table of individual items — so editing the structure and saving it are a single operation.

A menu item can be one of four kinds: a folder holding child items (node), an outbound link to an external site (linkout), a content page (content_page), or a content link collection (content_link). The last kind can either navigate to a new view or render its content inline, displayed as cards or as buttons.

Items can also be scoped to specific audiences individually, and appearance is customisable at two levels: the menu-wide theme (colours, corner radius, spacing, font size) and per-item overrides (colours, borders, images, text placement).

Business Flow

  1. Opening the menu list requires a selected LINE OA. Without one, the page shows a warning instead of the list.
  2. The filter bar uses a staged pattern — typed and selected values are held locally and only applied when the user presses Search, so no request fires per keystroke.
  3. The table shows the menu name with its template beneath it, the total item count across all levels, the status, the public token (copyable and reissuable directly from the column), the creation date, and the row actions: edit, copy link, clone, and delete.
  4. Copying a link first checks whether any item anywhere in the tree restricts audiences. If so and the OA has a LIFF ID, the CMS produces a LIFF URL so viewers can be identified; otherwise it produces a plain web URL.
  5. Cloning creates a new menu from the existing structure. Deleting performs a soft delete on the server.
  6. The create/edit screen is a three-column workspace. New menus start with default theme values and the list template; opening an existing menu loads its structure and public token.
  7. The menu name lives in a separate form but is synced back into the menu structure as the user types, because both the preview and the public view render the name from that structure.
  8. The left column (menu tree) is where items are added, edited, and removed. It offers expand-all, collapse-all, and a reset that clears every per-item style override across the tree. Adding an item opens a modal that asks for the item kind first; the remaining fields change to match. Drag-and-drop reordering is only permitted when the item stays under its original parent, guarding against accidental moves across submenus.
  9. The middle column (live preview) renders the menu in a phone frame with working navigation, breadcrumbs, a back/home control, and a search box. Selecting a content-link item configured for inline display fetches the real content and renders it as cards or buttons.
  10. The right column (customisers) splits into per-item settings (style, image, audience targeting) and menu-wide theme settings, which include five ready-made theme presets. A separate template selector chooses between list and grid layout, with a configurable column count for grid.
  11. Saving validates the name first. For a new menu the CMS creates the record and immediately switches into edit mode so the newly minted token is available. The preview button becomes usable once a token exists.

Key Screens & Components

List page (src/app/menu-builder/page.tsx) — table, staged filters, clone, token regeneration, link copying, and deletion.

Builder page (src/app/menu-builder/form/page.tsx) — the three-column workspace coordinating the tree, the live preview, and the customisers, plus save, preview, and copy-public-link actions.

Core components under src/components/menu-builder/

  • MenuTree — structure editing, the add/edit item modal, the item-kind selector, and drag-and-drop ordering
  • MenuPreviewEnhanced — the navigable live preview, including inline content-link previews
  • ThemeCustomizer and TemplateSelector — menu-wide theming and layout selection
  • ButtonCustomizer — per-item style, image, and audience targeting
  • ImageUploader — uploads images through POST /menu-builder/upload-image; only the resulting URL is stored in the menu config
  • ContentPagePicker and ContentLinkPicker — pick a published content page or an active content link, returning its token
  • MenuPublicView — the public-facing renderer, shared with the public pages
  • utils/menu.util.ts — in-memory tree helpers for finding, adding, removing, moving, and measuring the depth of items

Shared service (src/services/menu-builder.service.ts) — create, read, update, delete, clone, regenerate token, read by public token, and image upload.

Dependencies

  • Permissions — the subject menu-builder is checked by the side menu before the entry appears; it is unlocked by the backend line-oa module.
  • LINE OA Management — a selected OA is required before menus can be loaded or created, and the OA hash and LIFF ID come from here to build links.
  • Content Management — content-page items reference that page's public token.
  • Content Links — content-link items reference a link's token and use its preview endpoint to render content inside the live preview.
  • Audience — restricts per-item visibility, which in turn forces the menu link into LIFF form.
  • Public pages — published menus are opened through the public route, which uses the same renderer as the CMS.
  • Environment and storage — public URLs are built from NEXT_PUBLIC_BASE_APP_URL, and uploaded images live in object storage with only the URL persisted in the menu config.

Backend Details (CMS API)

The module lives in internal/modules/menubuilder/ and splits cleanly into two zones: the authenticated admin routes under /api/menu-builder, and a single public route with no guard at all.

Required permissions (and one important caveat)

  • Every admin route passes through the global JwtAuth.
  • This module's policy metadata is declared against the system_module module rather than its own. That is a direct carry-over from the previous system. If policy enforcement is ever switched on, access to the Menu Builder would hinge on system_module permissions instead of menu-builder — which differs from the menu-builder subject the side menu checks.
  • Today the policy metadata is not enforced and no ModuleGate guards the module, so any authenticated user can reach these endpoints.

What the backend does on create, clone, and token regeneration

  • POST /api/menu-builder — the backend generates the public token itself; it is never supplied by the form. That is why the UI must save first and enter edit mode before a token exists for links and preview.
  • POST /api/menu-builder/:id/clone — copies the whole menu structure into a new record and issues a fresh token for the copy, leaving the original's link untouched. Useful for seasonal variants or A/B trials.
  • POST /api/menu-builder/:id/regenerate-token — overwrites the token; the old link dies the instant the call succeeds, with no grace period. If that link was already embedded in a rich menu or a sent message, those places must be updated by hand.
  • PUT /api/menu-builder/:id — a full replace of the menu structure rather than a partial patch, matching the UI's model of holding the entire tree in memory and submitting it in one go.

Image upload

POST /api/menu-builder/upload-image validates that the uploaded file really is an image before accepting it, then stores it in object storage through the shared storage service. What comes back is a URL, which is embedded into the menu structure — the file itself is never stored on the menu record.

The public route

  • GET /api/menu-builder/public/:token has no guard whatsoever — anyone holding the token can read the menu structure.
  • A parallel public route exists in the public module: GET /api/public/menu/:token, behaving similarly. Both accept the same token, so blocking one path does not block the other.
  • Per-item audience restriction does not make the menu structure itself secret; it relies on LIFF identifying the viewer at render time.

Deletion and a soft-delete caveat

  • DELETE /api/menu-builder/:id is a soft delete that stamps a deleted_at column.
  • That column is not wired into the ORM's soft-delete mechanism, which has two consequences:
    • Every read must add deleted_at IS NULL by hand. Miss it and deleted menus resurface — including through the public route.
    • The delete itself must be written to bypass automatic filtering (unscoped) so it stamps the right record.
  • Tables involved: menu_builder and line_oa.