The browser never talks to Strapi: pages call Nitro endpoints under server/api/, which are the only place Strapi queries are built. That keeps the Strapi URL internal to the Docker network and avoids CORS entirely. Article bodies are rendered from Markdown to HTML in the endpoint, so the content is server-rendered and crawlable, marked stays out of the client bundle, and the meta description is derived from the opening of the body. Pages carry canonical URLs, Open Graph tags and BlogPosting structured data, and handle empty, missing and failed states.
55 lines
1.8 KiB
TypeScript
55 lines
1.8 KiB
TypeScript
import { marked } from 'marked'
|
|
|
|
/**
|
|
* Calls the Strapi REST API from the server only, so the internal URL and any
|
|
* future API token never reach the browser.
|
|
*/
|
|
export async function strapiFetch<T>(path: string): Promise<T> {
|
|
const { strapiUrl, strapiToken } = useRuntimeConfig()
|
|
|
|
try {
|
|
return (await $fetch(path, {
|
|
baseURL: strapiUrl,
|
|
headers: strapiToken ? { Authorization: `Bearer ${strapiToken}` } : undefined,
|
|
})) as T
|
|
} catch (error) {
|
|
console.error(`Strapi 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 StrapiList<T> {
|
|
data: T[]
|
|
meta: { pagination: { page: number; pageCount: number; total: number } }
|
|
}
|