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
| Parameter | Rule |
|---|---|
lineOaId | Required, integer, 1 or greater |
categoryId / subcategoryId | Optional, integer, 1 or greater |
search | Optional; sanitized before validation — trimmed, with special characters stripped — then must match ^[a-zA-Z0-9-\s\-_]*$ and be at most 100 characters |
publishedDateFrom / publishedDateTo | Optional, must be ISO 8601 |
page | Optional, 1–100 |
limit | Optional, 1–50 |
Business rules
validateLineOa— theline_oarow must be active and not soft-deleted, otherwise respond 404 withInvalid or inactive channel.validateDateRange— when both bounds are supplied, a range longer than 365 days returns 400 withDate range cannot exceed 365 days, and a start date after the end date returns 400 withpublishedDateFrom must be before publishedDateTo. If either bound fails to parse, the check is skipped.- A search term shorter than two characters returns 400 with
Search term must be at least 2 characters. - Defaults are
page=1andlimit=10, withskip = (page-1)*limit. Askipabove 500 returns 400 withPage number exceeds maximum allowed, guarding against deep pagination. - When
subcategoryIdis supplied it is expanded to every active descendant via a recursive CTE inFindActiveDescendantIDs, and the results are filtered against that id set — so picking a parent subcategory also surfaces its children's articles. - Fetch one page of results, then hydrate: load all
content_page_translationrows in a single batch viaFindTranslationsByPageIDs, 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. - Return
{data, total, page, limit, totalPages}, wheretotalPagesisceil(total/limit).
Category listing — GET /api/public-content/contents/categories
lineOaIdis read raw without a DTO, so a missing or non-numeric value becomes 0 andvalidateLineOaresponds 404.- Returns the OA's active categories along with their active subcategories grouped under
category_id; the repository already sorts bysort_order. - A category with no subcategories returns an empty array, not null.
Key Files & Functions
| Route | Rate limit | Handler |
|---|---|---|
GET /api/public-content/contents | 10/60s | internal/publiccontent/handler.go → (*Handler).ListPublicContents |
GET /api/public-content/contents/categories | 10/60s | (*Handler).ListPublicCategories |
internal/publiccontent/register.go—Register(r, deps)mounts all four routes in this domaininternal/publiccontent/service.go—ListPublicContents,ListPublicCategories,validateLineOa,validateDateRange,buildListItems,mapToPublicListItem,parseJSDateinternal/publiccontent/handler.go—contentsDTO,byLinkDTO,numberTransform,searchTransform,isDateStringCheck- Repositories —
internal/contentpage/repository.go(ListPublished,FindTranslationsByPageIDs),internal/contentcategory/repository.go, andinternal/contentsubcategory/repository.go(FindActiveDescendantIDs)
Connections to Other Services
- Database — tables
content_page,content_page_translation,content_category,content_subcategory, andline_oa - Middleware —
middleware.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
publiccontentpackage - client-web — corresponds to the
content-link-listingfeature (listing page with search and infinite scroll) and tocontent-page-viewer, which consumes the listing metadata