Skip to content

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.

lib/feed.ts
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.

app/news/page.tsx
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 minute

Static Generation

For fully static sites, generate all pages at build time from the feed.

Static List Page

app/events/page.tsx
import { fetchAllItems } from '@/lib/feed';
export const dynamic = 'force-static'; // Never run server-side after build
export 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

app/events/[id]/page.tsx
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 time
export 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).

components/FeedList.tsx
'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>
)}
</>
);
}
app/news/page.tsx
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.

app/news/tagged/[tag]/page.tsx
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>
);
}

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.

app/api/revalidate/route.ts
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.