Single Article Viewer (Content Page)
Overview
The endpoint that serves one article's content. It supports three modes with different response shapes — a metadata mode used to generate OG tags during server-rendering, a password-prompt mode, and full content — and it also handles audience restrictions, view counting, and the add-friend CTA block.
Two things worth knowing up front:
- The
:tokenpath parameter accepts either apublicTokenor aslug. - A wrong password returns HTTP 200 with a body explaining why, not 401 or 403. This is a quirk of the original controller, ported verbatim to preserve parity.
Business Flow
GET /api/public-content/content/:token
There is no per-route rate limit here; the global limiter applies. Query parameters are password and metadataOnly.
-
Look up the published
content_pagebypublic_token; if not found, try byslug; if still not found, return 404 withContent not found. -
Load every language translation for that page.
-
In
metadataOnly=truemode, skip the audience and password checks and do not count a view. Return{uuid, slug, translations, requirePassword}, where each language carries only meta/OG fields.This mode auto-generates an excerpt when
excerpt,ogDescription, andmetaDescriptionare all empty butcontentis present: strip HTML tags, replace with spaces, collapse repeated whitespace, trim, then take the first 200 characters (counted as runes), appending an ellipsis if the text was longer. -
Audience restriction — when
audience_idsis non-empty:- With no
x-liff-token, return 403 withAuthentication required for this content. - With a token, call
VerifyContentAccess, which uses the primary channel only — there is no form-channel fallback — and every error path returns 403. If the audiences do not overlap, return 403 withYou do not have access to this content.
- With no
-
Password check — when
password_protectedis set:- No password supplied returns 200 with
{requirePassword:true, message:"This content is password protected"}. - A wrong password returns 200 with
{requirePassword:true, message:"Invalid password", error:"INVALID_PASSWORD"}. - Comparison uses bcrypt. A malformed or empty hash simply fails to match rather than throwing, and hashes written by Node's
bcryptjsshare the same format so they verify correctly.
- No password supplied returns 200 with
-
Assemble the full translations, including
content, then callIncrementViewCount. -
The friendTrack block — if the page has a
friend_track_campaign_id, resolve the campaign token and return{campaignToken, buttonText, lineOaHash, lineLiffId}.buttonTextdefaults to the Thai label for "add friend", andlineLiffIdcomes fromline_login_info.lineLiffIdor.liffId, falling back to an empty string. -
Return
{uuid, slug, translations, requireAuth, friendTrack}with status 200.
Key Files & Functions
| Item | Value |
|---|---|
| Route | GET /api/public-content/content/:token |
| Handler | internal/publiccontent/handler.go → (*Handler).GetContentByToken |
| Service | internal/publiccontent/service.go → (*Service).GetContentByToken, which returns any because the response shape varies by branch |
| Helpers | autoExcerpt, comparePassword, liffIDFromLoginInfo, isEmptyPtr, jsonArrayLen, extractAudienceIds |
| Repository | internal/contentpage/repository.go → FindPublishedByPublicToken, FindPublishedBySlug, FindTranslationsByPageID, IncrementViewCount, FindLineOaRelationByID, FindFriendTrackCampaignTokenByID |
| Response shapes | detailTrans, metadataTrans, friendTrackBlock |
Connections to Other Services
- Database — tables
content_page(public_token,slug,password_protected,password_hash,audience_ids,require_auth,friend_track_campaign_id,friend_track_button_text,view_count),content_page_translation,friend_track_campaign, andline_oa - Library —
golang.org/x/crypto/bcryptfor password comparison - LIFF authentication — calls
VerifyContentAccess - Friend track — the friendTrack block feeds the friend-track feature; the web app takes
campaignTokenand callsPOST /friend-track/:token/visit - Related features — lives in the same package as the public content listing and the content link viewer
- client-web — corresponds to the
content-page-viewerfeature