Get CMS Page (by slug)
Public read of a single CMS page by slug. Optional locale selector (default 'en'). HTML body server-side sanitized (script / style / on* / iframe stripped). 404 on missing or DRAFT.
GET /api/v1/public/pages/:slug โ ๐ Public ยท Rate limit: 60 req / minute
Returns a single CMS page by slug. PUBLISHED-only โ DRAFT / ARCHIVED slugs return 404. The HTML body is server-side sanitized before return: <script>, <style>, <iframe> tags + on* event handlers are stripped (defense-in-depth โ admin authors are trusted, but the sanitizer protects against operator mistakes).
Sanitization is defensive, not exhaustive. The server strips obvious XSS vectors (<script>, <style>, <iframe>, inline event handlers like onclick). Other dangerous patterns (CSS expressions, data URIs in <img src>, etc) are NOT stripped โ render the content with dangerouslySetInnerHTML only if you trust the admin authors. For belt-and-suspenders safety, run client-side sanitization (e.g. DOMPurify) on top.
Locale-keyed lookup: each (slug, locale) pair is a separate row. Default locale is en. Pass ?locale=tr to fetch the Turkish version. Missing locale โ 404, NOT fallback to English โ you must list available locales via GET /public/pages and pick from there.
Request
Path parameters
| Param | Type | Notes |
|---|---|---|
slug | string | Page slug (e.g. about-us, careers) |
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
locale | string | en | Locale code |
No headers required.
Response headers
| Header | Value |
|---|---|
Cache-Control | public, s-maxage=300, stale-while-revalidate=600 |
Response
200 OK โ ApiResponseOf<PublicCmsPageDto>
{
"success": true,
"data": {
"slug": "about-us",
"title": "About Us",
"content": "<p>Sanitized HTML content</p>",
"locale": "en",
"publishedAt": "2026-04-29T20:00:00.000Z"
}
}| Field | Type | Notes |
|---|---|---|
slug | string | Echoes the path param |
title | string | Page title |
content | string | Server-side sanitized HTML body |
locale | string | Locale of the matched row (matches the ?locale filter) |
publishedAt | string (ISO 8601) | null | When the page transitioned to PUBLISHED |
Stripped fields
id, status, publishedBy, authorId, createdAt, updatedAt โ all internal-only, excluded from this response.
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
404 | error.content.page_not_found | No CMSPage matches (slug, locale) AND status = PUBLISHED |
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
prisma.cMSPage.findFirst({ where: { slug, locale: locale ?? 'en', status: PUBLISHED } }).- Missing โ throw
page_not_found(404). - HTML sanitization โ
sanitizeHtml(page.content)strips:<script>...</script>blocks (regex with greedy minimum match).<style>...</style>blocks (same pattern).- Inline event handlers โ any attribute starting with
onfollowed by a word (e.g.onclick="...",onerror='...'). <iframe>opening AND closing tags (kept content between, but tags stripped โ effectively unwraps).
- Return
{ slug, title, content: <sanitized>, locale, publishedAt }. No mutations.
Code samples
# Default locale (en)
curl https://api.bio.re/api/v1/public/pages/about-us
# Specific locale
curl 'https://api.bio.re/api/v1/public/pages/about-us?locale=tr'type PublicCmsPage = {
slug: string;
title: string;
content: string;
locale: string;
publishedAt: string | null;
};
async function getCmsPage(slug: string, locale = 'en'): Promise<PublicCmsPage> {
const url = new URL(`https://api.bio.re/api/v1/public/pages/${encodeURIComponent(slug)}`);
if (locale !== 'en') url.searchParams.set('locale', locale);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Page fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery } from '@tanstack/react-query';
export const contentKeys = {
page: (slug: string, locale: string) => ['content', 'pages', slug, locale] as const,
};
export function useCmsPage(slug: string, locale = 'en') {
return useQuery({
queryKey: contentKeys.page(slug, locale),
queryFn: async () => {
const url = new URL(`/api/v1/public/pages/${encodeURIComponent(slug)}`, window.location.origin);
if (locale !== 'en') url.searchParams.set('locale', locale);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Page fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as PublicCmsPage;
},
enabled: Boolean(slug),
staleTime: 5 * 60_000,
});
}Try it
Path Parameters
Query Parameters
Locale code (default: en)
Response Body
application/json
application/json
curl -X GET "https://loading/api/v1/public/pages/string"{
"success": true,
"data": {
"slug": "about-us",
"title": "About Us",
"content": "<p>Sanitized HTML content</p>",
"locale": "en",
"publishedAt": "2019-08-24T14:15:22Z"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/content/public-content.controller.ts | 72โ95 (getPage), 29โ36 (sanitizeHtml) |
| DTO (response) | apps/api-core/src/modules/content/dto/content-public-response.dto.ts | 17โ32 (PublicCmsPageDto) |
| Service | apps/api-core/src/modules/content/content.service.ts | 73โ77 (getPage) |
| Prisma model | packages/prisma/prisma/schema.prisma | CMSPage (filter status = PUBLISHED, (slug, locale) lookup) |
List CMS Pages
Public list of admin-managed CMS pages (slug + title + locale + publishedAt). PUBLISHED-only filter โ DRAFT and ARCHIVED never leak. CDN-cached 5min/10min SWR.
Get Active Announcements
Public list of currently-active announcements. Filtered by active flag + time window. Cap 50. Cache 60s. HTML body server-side sanitized.