Get Active Announcements
Public list of currently-active announcements. Filtered by active flag + time window. Cap 50. Cache 60s. HTML body server-side sanitized.
GET /api/v1/public/announcements โ ๐ Public ยท Rate limit: 60 req / minute
Returns active announcements โ site-wide banners / notices admin-managed. Filter is active: true AND startsAt <= now (or null) AND endsAt >= now (or null). Cap is 50 (server-side take: 50). Each item's content is sanitized via the same helper as CMS pages (<script>, <style>, <iframe>, inline on* event handlers stripped). Cache window is shorter than other content endpoints (60s s-maxage, no SWR) โ announcements need fresher reads (e.g. urgent maintenance notices).
Time window is open-ended. startsAt: null means "active from forever ago", endsAt: null means "active forever". Both sides nullable, both sides matched. A row with both null is "active until admin disables it".
Sanitization is defensive, not exhaustive. Same caveat as GET /public/pages/:slug โ strips obvious XSS but admin authors are still ultimately trusted. Use client-side sanitization for additional defense.
Request
No body, no params, no headers required.
Response headers
| Header | Value |
|---|---|
Cache-Control | public, s-maxage=60 |
Note: shorter cache than other content endpoints (60s, no stale-while-revalidate). Urgent announcements should propagate quickly.
Response
200 OK โ ArrayApiResponseOf<PublicAnnouncementDto>
{
"success": true,
"data": [
{
"title": "Scheduled Maintenance",
"content": "<p>We will be down for maintenance...</p>",
"type": "MAINTENANCE",
"locale": "en",
"startsAt": "2026-05-01T00:00:00.000Z",
"endsAt": "2026-05-01T02:00:00.000Z"
},
{
"title": "Welcome to BIO.RE",
"content": "<p>Thanks for joining...</p>",
"type": "INFO",
"locale": null,
"startsAt": null,
"endsAt": null
}
]
}Item fields
| Field | Type | Notes |
|---|---|---|
title | string | Announcement title |
content | string | Server-side sanitized HTML body |
type | string | INFO / WARNING / CRITICAL / MAINTENANCE (admin-set; clients branch on this for color/icon) |
locale | string | null | Locale code; null means the announcement is locale-agnostic (shown to all locales) |
startsAt | string (ISO 8601) | null | When the announcement becomes active. null = active from beginning. |
endsAt | string (ISO 8601) | null | When it stops being active. null = no end date. |
Stripped fields
id, active (always true here, filter excludes false), publishedBy, createdAt, updatedAt, internal metadata โ all excluded.
Ordering
Server returns up to 50 rows ordered by createdAt DESC (newest first). There is no priority field exposed; admins control display order via timing.
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
prisma.announcement.findMany({ where: { active: true, OR: [startsAt null OR <= now], AND: [endsAt null OR >= now] }, orderBy: createdAt desc, take: 50 }).- Map each row โ strip internal fields, sanitize
contentviasanitizeHtml(). - Return the array. No mutations.
Code samples
curl https://api.bio.re/api/v1/public/announcementstype AnnouncementType = 'INFO' | 'WARNING' | 'CRITICAL' | 'MAINTENANCE';
type PublicAnnouncement = {
title: string;
content: string;
type: AnnouncementType | string;
locale: string | null;
startsAt: string | null;
endsAt: string | null;
};
async function getActiveAnnouncements(): Promise<PublicAnnouncement[]> {
const res = await fetch('https://api.bio.re/api/v1/public/announcements');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Announcements fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery } from '@tanstack/react-query';
export const contentKeys = {
announcements: () => ['content', 'announcements'] as const,
};
export function useActiveAnnouncements() {
return useQuery({
queryKey: contentKeys.announcements(),
queryFn: async () => {
const res = await fetch('/api/v1/public/announcements');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Announcements fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as PublicAnnouncement[];
},
// Match the shorter cache window โ announcements need fresh reads
staleTime: 60_000,
refetchOnWindowFocus: true,
});
}Try it
Response Body
application/json
curl -X GET "https://loading/api/v1/public/announcements"{
"success": true,
"data": [
{
"title": "Scheduled Maintenance",
"content": "<p>We will be down for maintenance...</p>",
"type": "INFO",
"locale": "en",
"startsAt": "2019-08-24T14:15:22Z",
"endsAt": "2019-08-24T14:15:22Z"
}
]
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/content/public-content.controller.ts | 269โ285 (getActiveAnnouncements), 29โ36 (sanitizeHtml) |
| DTO (response item) | apps/api-core/src/modules/content/dto/content-public-response.dto.ts | 146โ164 (PublicAnnouncementDto) |
| Service | apps/api-core/src/modules/content/content.service.ts | 389โ400 (getActiveAnnouncements) |
| Prisma model | packages/prisma/prisma/schema.prisma | Announcement (filter active = true + time window) |
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.
List Blog Posts
Public paginated list of published blog posts. Optional category filter (by slug). Lightweight items (no body, just excerpt). Categories embedded as { name, slug } pairs. CDN 5min/10min SWR.