Skip to content

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:

src/lib/flarebuilder.js
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:

src/pages/blog/index.astro
---
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.

src/pages/blog/[id].astro
---
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 component
function getSection(item, sectionId) {
return item.sections.find(s => s.id === sectionId)?.data ?? {};
}
// Example: Blog Post
const content = getSection(item, 'content');
// content.description → string or null
// content.main_content → HTML string or null
// Example: Event
const details = getSection(item, 'details');
// details.location → string or null
// details.start_date → ISO date string or null
// details.registration_url → URL string or null

Filtering Content by Type and Tag

Pass query parameters to the feed to retrieve only the content you need:

// Fetch only "Event" items
const events = await fetchAllItems({ type: 'Event', sort: 'oldest' });

Content Collections (Astro 2+)

For larger projects, define a typed content collection backed by FlareBuilder. Create a loader in src/content/config.ts:

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:

src/pages/blog/index.astro
---
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.

  1. In Cloudflare Pages, go to your project → Settings > Builds & deployments > Deploy hooks
  2. Create a new deploy hook and copy the generated URL
  3. In FlareBuilder, go to Settings > Webhooks, add a new webhook with that URL
  4. Subscribe to content.published, content.updated, and content.unpublished
  5. Publish a content item to trigger a test build

Server-Side Rendering (On-Demand)

For content that changes frequently or when you need the absolute latest without a rebuild, switch to SSR mode:

astro.config.mjs
export default defineConfig({
output: 'server', // or 'hybrid' to mix static + SSR
adapter: ...,
});

Then fetch on each request instead of at build time:

src/pages/blog/[id].astro
---
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 feed
const 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