Skip to main content

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:

  1. The :token path parameter accepts either a publicToken or a slug.
  2. 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.

  1. Look up the published content_page by public_token; if not found, try by slug; if still not found, return 404 with Content not found.

  2. Load every language translation for that page.

  3. In metadataOnly=true mode, 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, and metaDescription are all empty but content is 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.

  4. Audience restriction — when audience_ids is non-empty:

    • With no x-liff-token, return 403 with Authentication 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 with You do not have access to this content.
  5. Password check — when password_protected is 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 bcryptjs share the same format so they verify correctly.
  6. Assemble the full translations, including content, then call IncrementViewCount.

  7. The friendTrack block — if the page has a friend_track_campaign_id, resolve the campaign token and return {campaignToken, buttonText, lineOaHash, lineLiffId}. buttonText defaults to the Thai label for "add friend", and lineLiffId comes from line_login_info.lineLiffId or .liffId, falling back to an empty string.

  8. Return {uuid, slug, translations, requireAuth, friendTrack} with status 200.

Key Files & Functions

ItemValue
RouteGET /api/public-content/content/:token
Handlerinternal/publiccontent/handler.go(*Handler).GetContentByToken
Serviceinternal/publiccontent/service.go(*Service).GetContentByToken, which returns any because the response shape varies by branch
HelpersautoExcerpt, comparePassword, liffIDFromLoginInfo, isEmptyPtr, jsonArrayLen, extractAudienceIds
Repositoryinternal/contentpage/repository.goFindPublishedByPublicToken, FindPublishedBySlug, FindTranslationsByPageID, IncrementViewCount, FindLineOaRelationByID, FindFriendTrackCampaignTokenByID
Response shapesdetailTrans, 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, and line_oa
  • Librarygolang.org/x/crypto/bcrypt for password comparison
  • LIFF authentication — calls VerifyContentAccess
  • Friend track — the friendTrack block feeds the friend-track feature; the web app takes campaignToken and calls POST /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-viewer feature