// View shaping for SSR payloads. Pages run API rows through these in // useAsyncData's transform, so the serialized payload carries only what the // page renders: no raw `content` source, no `translations`, and never an HTML // body that failed the Journal gate. import { gateJournalHtml } from './htmlGate.ts' import type { JournalPost, JournalPostDetail } from './types.ts' /** A post row as a list or card renders it: without body, source or translations. */ export function toListPost(post: JournalPost): JournalPost { return { ...post, content: null, content_html: null, translations: null } } /** A post page's data: the gated body, with `content_blocked` set when the gate refused it. */ export interface JournalPostView extends JournalPostDetail { content_blocked: boolean } /** * Formats an API timestamp as a long date for `language` (a BCP 47 tag such * as "pl-PL"). UTC on both server and browser, so hydration matches. Returns * "" for a missing or invalid value. */ export function formatJournalDate(iso: string | null | undefined, language: string): string { if (!iso) return '' const d = new Date(iso) if (Number.isNaN(d.getTime())) return '' try { return new Intl.DateTimeFormat(language || 'en', { dateStyle: 'long', timeZone: 'UTC' }).format(d) } catch { return new Intl.DateTimeFormat('en', { dateStyle: 'long', timeZone: 'UTC' }).format(d) } } export function toPostView(post: JournalPostDetail): JournalPostView { const gated = gateJournalHtml(post.content_html) return { ...post, content: null, translations: null, content_html: gated.html, content_blocked: !gated.safe, related_posts: (post.related_posts ?? []).map(toListPost), } }