Skip to main content

Content Link Listing Page

Overview

A content link is a link admins create in the CMS with filter conditions defined up front — category, subcategory, and which audiences may see it. When a user opens that link, this page lists every article matching those conditions, with a search box and infinite scroll for loading more.

Its best fit is an "article library" attached to a rich menu button or a link sent in chat, because admins can change the filter conditions later without touching links that have already been published.

Business Flow

  1. The user lands on /:hash/pages/:linkToken.
  2. The app loads the OA data from the hash. If the OA has LIFF configured, it performs a LIFF login first and waits for the user id, since that is required to filter the list by audience.
  3. It requests the article list from GET /public-content/links/:token/contents with page, limit, and search parameters, attaching x-liff-token and using an infinite query that loads 12 items per page.
  4. Link metadata is read from the first page's response to render the name, description, and category breadcrumb, and the document title is set from the link name.
  5. The search box debounces for 500 ms before issuing a request. When the search term changes, pagination resets back to the first page.
  6. Loading more uses an IntersectionObserver with a 100-pixel prefetch margin. When the user scrolls to the sentinel at the end of the list, the next page loads, continuing until every page has been fetched.
  7. Tapping an article card navigates to the article viewer for that token.

Screen states

The page handles every state a user might encounter: loading the OA, OA not found, login in progress, requested link not found, a search with no results, and all items loaded.

Key Screens & Components

Page

  • The listing page (src/app/[hash]/pages/[linkToken]/page.tsx) manages the infinite scroll observer, search debouncing, and flattening multi-page results into a single list.

Hook and components

  • The list hook (hooks/useFetchContentsByLink.ts) takes the token, search term, page size, and an enabled flag, deriving the next page by comparing the current page number against the total page count.
  • The article card (components/ContentCard.tsx) renders the cover image through the Next.js image component, formats dates with dayjs, and shows category and subcategory badges.

Service and types

  • The content link service (src/service/content-link.service.ts) fetches articles by link token.
  • Response, query parameter, and list item types live in src/service/types/content-link.type.ts.

Endpoint used

  • GET /public-content/links/:token/contents, accepting page, limit, and search parameters.

Dependencies

  • TanStack Query in infinite query form drives pagination and load-more behaviour.
  • Next.js Image in fill mode with responsive sizing handles card cover images.
  • dayjs formats publication dates.
  • Tapping a card leads to the Article Viewer.
  • Reached from the Public Menu when a menu item is a content link configured to navigate to a new page.
  • A parallel service exists under the menu folder, which the menu uses to render an article list inline within the menu screen itself, without navigating the user away.

Backend Details (Client API)

The heart of this feature is that every filter (category, subcategory, date range, audience) is read from values embedded in the link on the backend, never from parameters the web app sends. The web app may only supply a page number, a page size, and a search term.

The consequence is that an admin can change the filtering at any time while already-distributed links keep working, and conversely the web app cannot widen the result set beyond what the link defines, no matter how the query is manipulated — an intentional security property.

The three-step audience ladder

The backend decides which articles to show in this order, stopping at the first matching case:

  1. The link has an audience configured → filter by the link's audience (regardless of who the user is).
  2. The link has none, but the user has an audience → filter by the user's audience.
  3. Neither applies → show only articles with no audience restriction (public only).

This explains a common observation: a logged-out user sees a shorter list than a logged-in one, with no error anywhere.

User verification is best-effort — it never returns 401

  • When x-liff-token is present, the backend tries to verify it to learn the user's audience, but every failure is swallowed and the request proceeds without user-audience filtering (falling to rung 3).
  • So an expired token or a wrong channel does not produce an error; it silently shortens the list. That is precisely why this page waits for login to finish before fetching — otherwise users would see an incomplete list without realising it.
  • When a user reports "some articles are missing", check the token state first, not the link's filters.

What the backend checks and records

  • A missing or disabled link → 404 Content link not found or inactive.
  • The link's OA must be active, otherwise 404 Invalid or inactive channel.
  • The link's click count is incremented on every call to this endpoint, before the data is fetched. The important side effect: infinite-scroll page loads also count as clicks, since they hit the same endpoint. The click number in the CMS therefore means "times the list was fetched", not "people who opened the link".
  • A selected subcategory is automatically expanded to include all of its active descendants, so a link bound to a parent subcategory also surfaces articles from its children.

Validation rules for the parameters the web app can send

  • This route has its own rate limit of 10 requests per 60 seconds per IP, stricter than the general application limiter — worth watching with fast infinite scrolling or continuous typing in the search box (the 500 ms search debounce addresses exactly this).
  • Search terms are sanitized before validation: the characters <>'"%;()& are stripped, then only Thai/Latin letters, digits, spaces, hyphens, and underscores may remain, up to 100 characters.
  • Page numbers accept 1–100 and page size accepts 1–50 — this page's 12 per page sits comfortably inside that range.
  • Deep-pagination protection: an offset beyond 500 items returns 400 Page number exceeds maximum allowed. At 12 per page that means scrolling hits an error around page 42 rather than reaching "all pages loaded" as the web app expects. Links with many articles should rely on filters or search.

Performance and data shape

  • The backend fetches translations for every article on a page in a single batch and caches taxonomy data per result page, avoiding N+1 queries.
  • Category data attached to each article is read without status filtering (parity with the legacy system) — a disabled or deleted category can still appear as a badge on a card.
  • A category that cannot be resolved is absent from the result entirely (not null), so the web app must check for the key's presence before using it.
  • Link information (name, description, categories for the breadcrumb) accompanies every page of results, not just the first — reading it from the first page is simply sufficient.