Next.js Integration
FlareBuilder’s Feed API pairs naturally with the Next.js App Router. Server Components can fetch content directly — no client-side JavaScript, no loading states, no API routes needed. Use ISR or full static generation depending on how frequently your content changes.
Prerequisites
- A FlareBuilder organization with published content
- A Next.js project using the App Router (
app/directory) - Your feed URL:
https://your-org.flarebuilder.com/feed
Fetching Helpers
Create a shared module for all Feed API calls. This keeps your feed URL in one place and gives you typed responses.
const FEED_BASE = 'https://your-org.flarebuilder.com/feed';
export interface FeedItem { id: string; title: string; type: string; tags: string[]; date_published: string; date_expires: string | null; date_created: string; author: { id: string; name?: string }; permalink: string; sections: Array<{ id: string; data: Record<string, unknown>; }>;}
export interface FeedResponse { title: string; description?: string; feed_url: string; items: FeedItem[]; pagination: { next: string | null; limit: number; has_more: boolean; };}
interface FetchFeedOptions { tags?: string; types?: string; status?: 'active' | 'scheduled' | 'expired'; limit?: number; cursor?: string; revalidate?: number;}
export async function fetchFeed(options: FetchFeedOptions = {}): Promise<FeedResponse> { const { revalidate = 60, ...params } = options; const qs = new URLSearchParams();
if (params.limit) qs.set('limit', String(params.limit)); if (params.cursor) qs.set('cursor', params.cursor); if (params.tags) qs.set('tags', params.tags); if (params.types) qs.set('types', params.types); if (params.status) qs.set('status', params.status);
const url = qs.size ? `${FEED_BASE}?${qs}` : FEED_BASE; const res = await fetch(url, { next: { revalidate } });
if (!res.ok) throw new Error(`Feed API error: ${res.status}`); return res.json();}
export async function fetchItem(id: string, revalidate = 60): Promise<FeedItem> { const res = await fetch( `https://your-org.flarebuilder.com/p/${id}`, { next: { revalidate } } ); if (!res.ok) throw new Error(`Item not found: ${id}`); return res.json();}
/** Fetch all pages of the feed (for static generation) */export async function fetchAllItems(options: Omit<FetchFeedOptions, 'cursor'> = {}): Promise<FeedItem[]> { const items: FeedItem[] = []; let cursor: string | null = null;
do { const page = await fetchFeed({ ...options, cursor: cursor ?? undefined, revalidate: 3600 }); items.push(...page.items); cursor = page.pagination.has_more && page.pagination.next ? new URL(page.pagination.next).searchParams.get('cursor') : null; } while (cursor);
return items;}Display a Content List
Server Components fetch directly — no useEffect, no loading spinners on the server side.
import { fetchFeed } from '@/lib/feed';
export default async function NewsPage() { const feed = await fetchFeed({ types: 'news', limit: 20 });
return ( <main> <h1>{feed.title}</h1> <ul> {feed.items.map(item => { const content = item.sections.find(s => s.id === 'content')?.data ?? {}; return ( <li key={item.id}> <a href={`/news/${item.id}`}> <h2>{item.title}</h2> {content.summary && <p>{content.summary as string}</p>} <time dateTime={item.date_published}> {new Date(item.date_published).toLocaleDateString()} </time> </a> </li> ); })} </ul> </main> );}
export const revalidate = 60; // ISR: regenerate at most once per minuteStatic Generation
For fully static sites, generate all pages at build time from the feed.
Static List Page
import { fetchAllItems } from '@/lib/feed';
export const dynamic = 'force-static'; // Never run server-side after buildexport const revalidate = false;
export default async function EventsPage() { const items = await fetchAllItems({ types: 'event' });
return ( <main> <h1>Events</h1> {items.map(item => { const schedule = item.sections.find(s => s.id === 'schedule')?.data ?? {}; return ( <article key={item.id}> <h2><a href={`/events/${item.id}`}>{item.title}</a></h2> {schedule.event_start_date && ( <time dateTime={schedule.event_start_date as string}> {new Date(schedule.event_start_date as string).toLocaleDateString()} </time> )} </article> ); })} </main> );}Static Detail Page
import { fetchAllItems, fetchItem, type FeedItem } from '@/lib/feed';import { notFound } from 'next/navigation';
interface Props { params: Promise<{ id: string }>;}
// Pre-render all known event pages at build timeexport async function generateStaticParams() { const items = await fetchAllItems({ types: 'event' }); return items.map(item => ({ id: item.id }));}
export async function generateMetadata({ params }: Props) { const { id } = await params; const item = await fetchItem(id).catch(() => null); if (!item) return {}; return { title: item.title, description: (item.sections[0]?.data?.summary as string) ?? undefined, };}
export default async function EventPage({ params }: Props) { const { id } = await params; const item = await fetchItem(id).catch(() => null); if (!item) notFound();
const schedule = item.sections.find(s => s.id === 'schedule')?.data ?? {}; const content = item.sections.find(s => s.id === 'content')?.data ?? {}; const speakers = item.sections.find(s => s.id === 'speakers')?.data ?? {};
return ( <article> <h1>{item.title}</h1>
{schedule.event_start_date && ( <p> <time dateTime={schedule.event_start_date as string}> {new Date(schedule.event_start_date as string).toLocaleString()} </time> {schedule.location_name && ` · ${schedule.location_name}`} </p> )}
{content.body && ( <div dangerouslySetInnerHTML={{ __html: content.body as string }} /> )}
{Array.isArray(speakers.speaker_list) && speakers.speaker_list.length > 0 && ( <section> <h2>Speakers</h2> {(speakers.speaker_list as Array<Record<string, unknown>>).map((s, i) => ( <div key={i}> {s.photo && <img src={s.photo as string} alt={s.name as string} />} <strong>{s.name as string}</strong> {s.company && <span> · {s.company as string}</span>} {s.bio && <p>{s.bio as string}</p>} </div> ))} </section> )} </article> );}Pagination with a Client Component
Static and ISR pages show the first page of results. For user-driven “Load More” pagination, mix a Server Component (for the initial fetch) with a Client Component (for the interaction).
'use client';
import { useState, useTransition } from 'react';import type { FeedItem } from '@/lib/feed';
interface Props { initialItems: FeedItem[]; initialNext: string | null;}
export function FeedList({ initialItems, initialNext }: Props) { const [items, setItems] = useState(initialItems); const [nextUrl, setNextUrl] = useState(initialNext); const [isPending, startTransition] = useTransition();
async function loadMore() { if (!nextUrl) return; startTransition(async () => { const res = await fetch(nextUrl); const data = await res.json(); setItems(prev => [...prev, ...data.items]); setNextUrl(data.pagination.has_more ? data.pagination.next : null); }); }
return ( <> <ul> {items.map(item => ( <li key={item.id}> <a href={`/news/${item.id}`}>{item.title}</a> </li> ))} </ul>
{nextUrl && ( <button onClick={loadMore} disabled={isPending}> {isPending ? 'Loading…' : 'Load more'} </button> )} </> );}import { fetchFeed } from '@/lib/feed';import { FeedList } from '@/components/FeedList';
export default async function NewsPage() { const feed = await fetchFeed({ types: 'news', limit: 10 });
return ( <main> <h1>News</h1> <FeedList initialItems={feed.items} initialNext={feed.pagination.has_more ? feed.pagination.next : null} /> </main> );}
export const revalidate = 60;Filtering by Tag
Pass tags to fetchFeed to scope the feed. Tag filters support AND/OR syntax — see the Feed API reference for details.
import { fetchFeed } from '@/lib/feed';
interface Props { params: Promise<{ tag: string }>;}
export default async function TagPage({ params }: Props) { const { tag } = await params; const feed = await fetchFeed({ tags: tag, limit: 20 });
return ( <main> <h1>Tagged: {tag}</h1> {/* render items */} </main> );}ICS Calendar Link
Render a subscribe link directly — no API call needed:
export function CalendarSubscribeLink({ type = 'event' }: { type?: string }) { const url = `https://your-org.flarebuilder.com/feed?format=ics&templates=${type}`; return ( <a href={url} download> Subscribe to calendar (.ics) </a> );}On-Demand Revalidation (optional)
If you want content to go live in Next.js as soon as it’s published in FlareBuilder, set up a webhook that calls Next.js’s on-demand revalidation endpoint.
import { revalidatePath, revalidateTag } from 'next/cache';import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) { const secret = req.headers.get('x-webhook-secret'); if (secret !== process.env.WEBHOOK_SECRET) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
// Revalidate the pages that show feed content revalidatePath('/news'); revalidatePath('/events');
return NextResponse.json({ revalidated: true });}Point your FlareBuilder webhook at https://your-site.com/api/revalidate and set the matching secret.