@rankfirst/next
v0.1.0
Published
Next.js adapter for RankFirst content delivery.
Readme
@rankfirst/next
Next.js adapter for pulling RankFirst articles into your site, rendering them with sane MDX defaults, and revalidating them seconds after publish.
Quickstart
If you want the setup applied for you inside an existing Next.js App Router repo:
npx rankfirstThat CLI will:
- optionally open a browser link flow against your logged-in RankFirst account
- fetch the API key + webhook secret directly into
.env.local - create the server-only RankFirst client helper
- add the blog list/detail routes
- add the signed revalidation endpoint
- add sitemap support
- patch
next.config.*for RankFirst images - append the required env vars to
.env.example
If you want to do it manually or customize every file yourself, keep reading.
What you get
- pull published RankFirst articles into your Next.js app at build time
- render them with a ready-made article component
- survive RankFirst outages with a last-known-good snapshot cache
- revalidate the cache within seconds after publish via a signed webhook
Setup flow
- In RankFirst, open your site's Integration page and generate:
- an API key
- a webhook secret
- the webhook URL pointing at your Next.js app
- Install
@rankfirst/nextin your Next.js repo. - Add the env vars below.
- Add a server-only RankFirst client helper.
- Add blog list/detail routes plus the revalidation route.
- Add sitemap entries and
next/imageremote patterns.
1. Install
npm install @rankfirst/next2. Add env vars
RANKFIRST_API_KEY=rf_live_...
RANKFIRST_WEBHOOK_SECRET=...
RANKFIRST_BASE_URL=https://rankfirst.co
NEXT_PUBLIC_SITE_URL=https://your-site.comSecurity note: RANKFIRST_API_KEY is a bearer secret. Only read it in server components, route handlers, and server actions. Never prefix it with NEXT_PUBLIC_, never pass it into a client component, and rotate it immediately if it ever appears in browser devtools.
RANKFIRST_BASE_URL is optional. Leave it alone for production. Point it at your local RankFirst app when you want to test against an unshipped API, for example http://localhost:3000.
3. Create a server-only client
Put this in lib/rankfirst.server.ts:
import "server-only";
import { cache } from "react";
import { createResilientClient } from "@rankfirst/next";
export const getRankFirstClient = cache(() =>
createResilientClient({
apiKey: process.env.RANKFIRST_API_KEY!,
baseUrl: process.env.RANKFIRST_BASE_URL,
}),
);That gives you one shared server-only helper instead of repeating the client config in every route.
4. Add blog routes
app/blog/page.tsx
import Link from "next/link";
import { getRankFirstClient } from "@/lib/rankfirst.server";
export default async function BlogPage() {
const client = getRankFirstClient();
const snapshot = await client.getSnapshot();
return (
<main className="mx-auto max-w-3xl space-y-8 px-6 py-12">
<h1 className="text-4xl font-semibold">Blog</h1>
<ul className="space-y-6">
{snapshot.articles.map((article) => (
<li key={article.id}>
<Link href={`/blog/${article.slug}`} className="block space-y-2 hover:underline">
<h2 className="text-2xl font-semibold">{article.headline}</h2>
<p className="text-sm text-neutral-600">{article.excerpt}</p>
</Link>
</li>
))}
</ul>
</main>
);
}app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import {
generateRankFirstMetadata,
generateRankFirstStaticParams,
RankFirstArticle,
} from "@rankfirst/next";
import { getRankFirstClient } from "@/lib/rankfirst.server";
export const dynamicParams = true;
export async function generateStaticParams() {
const client = getRankFirstClient();
return generateRankFirstStaticParams(client);
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const client = getRankFirstClient();
const { slug } = await params;
const snapshot = await client.getSnapshot();
const article = snapshot.articles.find((entry) => entry.slug === slug);
if (!article) {
return {};
}
return generateRankFirstMetadata(article, {
baseUrl: process.env.NEXT_PUBLIC_SITE_URL!,
});
}
export default async function BlogArticlePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const client = getRankFirstClient();
const { slug } = await params;
const snapshot = await client.getSnapshot();
const article = snapshot.articles.find((entry) => entry.slug === slug);
if (!article) {
notFound();
}
return (
<main className="mx-auto max-w-3xl px-6 py-12">
<RankFirstArticle article={article} tokens={snapshot.site?.designTokens ?? null} />
</main>
);
}If you want a path other than /blog, that's fine. Just keep the same route shape and pass basePath into the sitemap / metadata helpers if needed.
5. Add the revalidation webhook route
app/api/rankfirst/revalidate/route.ts
import { createRevalidationHandler } from "@rankfirst/next";
export const { POST } = createRevalidationHandler({
secret: process.env.RANKFIRST_WEBHOOK_SECRET!,
});Then go back to RankFirst and set your webhook URL to:
https://your-site.com/api/rankfirst/revalidate6. Add sitemap support
app/sitemap.ts
import type { MetadataRoute } from "next";
import { getRankFirstSitemapEntries } from "@rankfirst/next";
import { getRankFirstClient } from "@/lib/rankfirst.server";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const client = getRankFirstClient();
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL!;
const articles = await getRankFirstSitemapEntries(client, { baseUrl });
return [{ url: baseUrl }, ...articles];
}7. Allow RankFirst images
Add RankFirst storage to next.config.ts so next/image can optimize article images:
import type { NextConfig } from "next";
const config: NextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "r2.rankfirst.co" }],
},
};
export default config;8. Optional styling inheritance
If the site owner fills in the RankFirst integration form, article typography and colors inherit those values. If not, RankFirstArticle falls back to normal Tailwind prose styling.
9. Build-time resilience
Every successful fetch writes .rankfirst-snapshot.json at the project root. On the next build, if RankFirst is down or returns an error, the adapter serves that last-known-good snapshot instead of dropping your blog routes.
You can:
- add
.rankfirst-snapshot.jsonto.gitignoreand keep it local, or - commit it if you want new infra to keep serving the previous article set during an outage.
10. Verify locally
Once your env vars are in place:
npm run devThen check:
/blogshows your published RankFirst posts/blog/[slug]renders the full articlePOST /api/rankfirst/revalidateis reachable from the public internet in your preview / prod environmentsitemap.xmlincludes the RankFirst URLs
Troubleshooting
- First-ever build with RankFirst down and no snapshot yet: the resilient client returns an empty article list and logs a warning. Set
onCompleteFailure: "throw"if you prefer a hard failure. - Secret rotation: rotate the API key or webhook secret in the RankFirst integration page, then update your env vars and redeploy.
- Verify webhook signatures manually:
curl -X POST https://your-site.com/api/rankfirst/revalidate \
-H "content-type: application/json" \
-H "x-rankfirst-signature: sha256=..." \
-H "x-rankfirst-timestamp: 1712345678901" \
-H "x-rankfirst-delivery-id: 00000000-0000-0000-0000-000000000000" \
-d '{"event":"article.published","articleId":"123","slug":"hello","publishedAt":"2026-04-24T00:00:00.000Z"}'