Skip to main content

Public Article & Category Listing

Overview

Two endpoints that let the web app fetch an OA's published articles with filtering (category, subcategory including all descendants, free-text search, and publish-date range), plus the category and subcategory list used to build the filter bar.

Both are public endpoints requiring no LIFF token, so validation is strict and a rate limit of 10 requests per 60 seconds per IP applies.

Business Flow

Article listing — GET /api/public-content/contents

Query parameter validation

ParameterRule
lineOaIdRequired, integer, 1 or greater
categoryId / subcategoryIdOptional, integer, 1 or greater
searchOptional; sanitized before validation — trimmed, with special characters stripped — then must match ^[a-zA-Z0-9฀-๿\s\-_]*$ and be at most 100 characters
publishedDateFrom / publishedDateToOptional, must be ISO 8601
pageOptional, 1–100
limitOptional, 1–50

Business rules

  1. validateLineOa — the line_oa row must be active and not soft-deleted, otherwise respond 404 with Invalid or inactive channel.
  2. validateDateRange — when both bounds are supplied, a range longer than 365 days returns 400 with Date range cannot exceed 365 days, and a start date after the end date returns 400 with publishedDateFrom must be before publishedDateTo. If either bound fails to parse, the check is skipped.
  3. A search term shorter than two characters returns 400 with Search term must be at least 2 characters.
  4. Defaults are page=1 and limit=10, with skip = (page-1)*limit. A skip above 500 returns 400 with Page number exceeds maximum allowed, guarding against deep pagination.
  5. When subcategoryId is supplied it is expanded to every active descendant via a recursive CTE in FindActiveDescendantIDs, and the results are filtered against that id set — so picking a parent subcategory also surfaces its children's articles.
  6. Fetch one page of results, then hydrate: load all content_page_translation rows in a single batch via FindTranslationsByPageIDs, and load category/subcategory references (id, name, slug) cached per page to avoid N+1 queries. Those references are read unfiltered by active or soft-delete state to preserve the original behaviour, and a missing category simply has no key in the result.
  7. Return {data, total, page, limit, totalPages}, where totalPages is ceil(total/limit).

Category listing — GET /api/public-content/contents/categories

  • lineOaId is read raw without a DTO, so a missing or non-numeric value becomes 0 and validateLineOa responds 404.
  • Returns the OA's active categories along with their active subcategories grouped under category_id; the repository already sorts by sort_order.
  • A category with no subcategories returns an empty array, not null.

Key Files & Functions

RouteRate limitHandler
GET /api/public-content/contents10/60sinternal/publiccontent/handler.go(*Handler).ListPublicContents
GET /api/public-content/contents/categories10/60s(*Handler).ListPublicCategories
  • internal/publiccontent/register.goRegister(r, deps) mounts all four routes in this domain
  • internal/publiccontent/service.goListPublicContents, ListPublicCategories, validateLineOa, validateDateRange, buildListItems, mapToPublicListItem, parseJSDate
  • internal/publiccontent/handler.gocontentsDTO, byLinkDTO, numberTransform, searchTransform, isDateStringCheck
  • Repositories — internal/contentpage/repository.go (ListPublished, FindTranslationsByPageIDs), internal/contentcategory/repository.go, and internal/contentsubcategory/repository.go (FindActiveDescendantIDs)

Connections to Other Services

  • Database — tables content_page, content_page_translation, content_category, content_subcategory, and line_oa
  • Middlewaremiddleware.RateLimit(10), applied per route and per IP
  • Related features — shares repositories with the content link viewer and the content page viewer, since all three endpoints live in the same publiccontent package
  • client-web — corresponds to the content-link-listing feature (listing page with search and infinite scroll) and to content-page-viewer, which consumes the listing metadata