Astro Integration
Astro is a natural companion for FlareBuilder — both embrace an edge-first, content-driven philosophy. Use the Feed API to pull published content into your Astro site at build time for a fully static site, or on-demand with server-side rendering.
Prerequisites
- A FlareBuilder organization with published content
- An Astro project (
npm create astro@latest) - Your organization’s feed URL:
https://your-org.flarebuilder.com/feed
Understanding the Feed Structure
Every response from the Feed API shares the same shape. Items contain top-level metadata and a sections array — the sections are defined by your template and vary per content type.
{ "title": "Your Org Feed", "feed_url": "https://your-org.flarebuilder.com/feed", "items": [ { "id": "011a1262-8675-486f-a3e1-9c62ca273f5e", "title": "Quarterly Update", "tags": ["news", "featured"], "template_name": "Blog Post", "date_published": "2026-02-11T06:00:00.000Z", "date_expires": null, "date_created": "2026-02-12T15:53:52Z", "author": { "id": "e022..." }, "permalink": "https://your-org.flarebuilder.com/p/011a1262-8675-486f-a3e1-9c62ca273f5e", "sections": [ { "id": "content", "label": "Content", "data": { "description": "A brief summary of the update.", "main_content": "<h2>Details</h2><p>...</p>" } } ] } ], "pagination": { "next": "https://your-org.flarebuilder.com/feed?cursor=...&limit=20", "prev": null, "limit": 20, "has_more": false }}Sections are flexible — a “Blog Post” template might have a content section with description and main_content, while an “Event” template might have a details section with location, start_date, and registration_url. Check your FlareBuilder workspace templates to see the exact section IDs and field names.
Fetching All Content
For feeds with more than 20 items, you’ll need to follow pagination. This helper fetches every page:
const FEED_URL = 'https://your-org.flarebuilder.com/feed';
export async function fetchAllItems(params = {}) { const items = []; const query = new URLSearchParams({ limit: '100', ...params }); let url = `${FEED_URL}?${query}`;
while (url) { const res = await fetch(url); if (!res.ok) throw new Error(`Feed fetch failed: ${res.status}`); const data = await res.json(); items.push(...data.items); url = data.pagination.has_more ? data.pagination.next : null; }
return items;}
export async function fetchItem(permalink) { const res = await fetch(permalink); if (!res.ok) throw new Error(`Item fetch failed: ${res.status}`); return res.json();}Static Site Generation
Blog Index Page
Fetch all items and render a list at build time:
---import { fetchAllItems } from '../../lib/flarebuilder';
const items = await fetchAllItems({ type: 'Blog Post', sort: 'newest' });---
<html lang="en"> <head><title>Blog</title></head> <body> <main> {items.map(item => { const section = item.sections.find(s => s.id === 'content'); return ( <article> <a href={`/blog/${item.id}`}> <h2>{item.title}</h2> </a> {section?.data?.description && ( <p>{section.data.description}</p> )} <time datetime={item.date_published}> {new Date(item.date_published).toLocaleDateString()} </time> <ul> {item.tags.map(tag => <li>{tag}</li>)} </ul> </article> ); })} </main> </body></html>Dynamic Post Pages
Use getStaticPaths to generate one page per content item. The item id (UUID) serves as the route parameter — this matches the permalink pattern FlareBuilder uses.
---import { fetchAllItems, fetchItem } from '../../lib/flarebuilder';
export async function getStaticPaths() { const items = await fetchAllItems({ type: 'Blog Post' }); return items.map(item => ({ params: { id: item.id }, props: { item }, }));}
const { item } = Astro.props;const section = item.sections.find(s => s.id === 'content');---
<html lang="en"> <head> <title>{item.title}</title> {section?.data?.description && ( <meta name="description" content={section.data.description} /> )} </head> <body> <article> <h1>{item.title}</h1> <time datetime={item.date_published}> {new Date(item.date_published).toLocaleDateString()} </time> {section?.data?.main_content && ( <div class="prose"> <Fragment set:html={section.data.main_content} /> </div> )} </article> </body></html>Working with Sections
Sections let templates define different field structures. Always look up the section by id rather than relying on array position:
// Generic helper — use in any .astro componentfunction getSection(item, sectionId) { return item.sections.find(s => s.id === sectionId)?.data ?? {};}
// Example: Blog Postconst content = getSection(item, 'content');// content.description → string or null// content.main_content → HTML string or null
// Example: Eventconst details = getSection(item, 'details');// details.location → string or null// details.start_date → ISO date string or null// details.registration_url → URL string or nullFiltering Content by Type and Tag
Pass query parameters to the feed to retrieve only the content you need:
// Fetch only "Event" itemsconst events = await fetchAllItems({ type: 'Event', sort: 'oldest' });// Fetch items tagged "featured"const featured = await fetchAllItems({ tag: 'featured' });// Fetch items published in 2026const posts = await fetchAllItems({ after: '2026-01-01', before: '2026-12-31', tz: 'America/New_York',});---// Fetch blog posts and news separatelyconst [posts, news] = await Promise.all([ fetchAllItems({ type: 'Blog Post' }), fetchAllItems({ type: 'News' }),]);---Content Collections (Astro 2+)
For larger projects, define a typed content collection backed by FlareBuilder. Create a loader in src/content/config.ts:
import { defineCollection, z } from 'astro:content';
const feedItemSchema = z.object({ id: z.string(), title: z.string(), tags: z.array(z.string()), type: z.string(), date_published: z.string(), date_expires: z.string().nullable(), date_created: z.string(), author: z.object({ id: z.string(), name: z.string().optional() }), permalink: z.string(), sections: z.array( z.object({ id: z.string(), data: z.record(z.unknown()), }) ),});
export const collections = { blog: defineCollection({ loader: async () => { const items: unknown[] = []; let url: string | null = 'https://your-org.flarebuilder.com/feed?type=Blog+Post&limit=100'; while (url) { const res = await fetch(url); const data = await res.json() as any; items.push(...data.items); url = data.pagination.has_more ? data.pagination.next : null; } // Content collections require an `id` field return items.map((item: any) => ({ ...item, id: item.id })); }, schema: feedItemSchema, }),};Then query it from any page:
---import { getCollection } from 'astro:content';
const posts = await getCollection('blog');posts.sort((a, b) => new Date(b.data.date_published).getTime() - new Date(a.data.date_published).getTime());---Automatic Rebuilds via Webhooks
Connect FlareBuilder webhooks to your hosting provider’s deploy hook so your site rebuilds whenever content is published or updated.
- In Cloudflare Pages, go to your project → Settings > Builds & deployments > Deploy hooks
- Create a new deploy hook and copy the generated URL
- In FlareBuilder, go to Settings > Webhooks, add a new webhook with that URL
- Subscribe to
content.published,content.updated, andcontent.unpublished - Publish a content item to trigger a test build
- In Netlify, go to Site settings > Build & deploy > Build hooks
- Create a new build hook and copy the URL
- In FlareBuilder, go to Settings > Webhooks, add a new webhook with that URL
- Subscribe to
content.published,content.updated, andcontent.unpublished - Publish a content item to trigger a test build
Deploy a lightweight webhook receiver that triggers your CI/CD pipeline:
// webhook-handler.js (e.g. a Cloudflare Worker)import crypto from 'crypto';
export default { async fetch(request, env) { if (request.method !== 'POST') return new Response('Method not allowed', { status: 405 });
const body = await request.text(); const signature = request.headers.get('X-FlareBuilder-Signature'); const expected = `sha256=${crypto.createHmac('sha256', env.WEBHOOK_SECRET).update(body).digest('hex')}`; if (signature !== expected) return new Response('Invalid signature', { status: 401 });
const event = JSON.parse(body); if (['content.published', 'content.updated', 'content.unpublished'].includes(event.event)) { await fetch(env.DEPLOY_HOOK_URL, { method: 'POST' }); }
return new Response('OK'); }};Server-Side Rendering (On-Demand)
For content that changes frequently or when you need the absolute latest without a rebuild, switch to SSR mode:
export default defineConfig({ output: 'server', // or 'hybrid' to mix static + SSR adapter: ...,});Then fetch on each request instead of at build time:
---export const prerender = false; // required in hybrid mode
const { id } = Astro.params;const res = await fetch(`https://your-org.flarebuilder.com/p/${id}`);
if (!res.ok) return Astro.redirect('/404');
const item = await res.json();const section = item.sections.find(s => s.id === 'content');---
<article> <h1>{item.title}</h1> {section?.data?.main_content && ( <Fragment set:html={section.data.main_content} /> )}</article>Rich Text Styling
main_content (or equivalent HTML fields in your template) returns sanitized HTML. Wrap it in a styled container:
---const section = item.sections.find(s => s.id === 'content');---
<div class="prose"> <Fragment set:html={section?.data?.main_content ?? ''} /></div>
<style> .prose :global(h1), .prose :global(h2), .prose :global(h3) { font-weight: 700; line-height: 1.25; margin-top: 1.5em; } .prose :global(p) { line-height: 1.75; margin-bottom: 1em; } .prose :global(a) { color: var(--color-accent); text-decoration: underline; } .prose :global(img) { max-width: 100%; border-radius: 0.25rem; }</style>Channel Feeds
If your organization uses channels for unlisted content, pass a ?token= parameter to the channel feed endpoint:
// Authenticated channel feedconst res = await fetch( 'https://your-org.flarebuilder.com/feed/members-only?token=YOUR_CHANNEL_TOKEN');const data = await res.json();Complete Project Structure
A minimal Astro blog fully powered by FlareBuilder:
src/├── lib/│ └── flarebuilder.js # fetchAllItems, fetchItem helpers├── pages/│ ├── index.astro # Homepage│ └── blog/│ ├── index.astro # Post listing│ └── [id].astro # Individual posts via getStaticPaths└── content/ └── config.ts # Optional: typed content collection