import { marked } from 'marked' import type { MediaImage } from '#shared/types/blog' /** * Calls the PocketBase REST API from the server only, so the internal URL * never reaches the browser. */ export async function pbFetch(path: string): Promise { const { pocketbaseUrl } = useRuntimeConfig() try { return (await $fetch(path, { baseURL: pocketbaseUrl })) as T } catch (error) { console.error(`PocketBase request failed: ${path}`, error) throw createError({ statusCode: 502, statusMessage: 'Content service unavailable' }) } } /** * Renders the Markdown body server-side, so the HTML is crawlable and `marked` * stays out of the client bundle. * * ponytail: no HTML sanitisation — article bodies come from authenticated * editors only. Add DOMPurify here if authoring is ever opened up further. */ export function renderMarkdown(source: string | null | undefined): string { return source ? (marked.parse(source, { async: false }) as string) : '' } /** * There is no excerpt field: the meta description is the opening of the body, * stripped of Markdown syntax and clipped on a word boundary. */ export function summarise(source: string | null | undefined, maxLength = 155): string { const text = (source ?? '') .replace(/```[\s\S]*?```/g, ' ') .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') .replace(/[#>*_`~-]/g, ' ') .replace(/\s+/g, ' ') .trim() if (text.length <= maxLength) return text const clipped = text.slice(0, maxLength) return `${clipped.slice(0, clipped.lastIndexOf(' ')).trimEnd()}…` } export interface PbList { page: number perPage: number totalItems: number totalPages: number items: T[] } /** * Builds a PocketBase file URL, relative to its own origin — PocketBase file * fields store only a filename, not a full path or dimensions. */ export function toMediaImage( collection: string, id: string, filename: string | null | undefined, alt: string | null | undefined ): MediaImage | null { if (!filename) return null return { url: `/api/files/${collection}/${id}/${filename}`, alt: alt || null } }