Paragraphs
Next.js

Feeds next-intl. Doesn't replace it.

Paragraphs is the backend. next-intl stays your runtime. Install in two minutes; works with App Router and server components.

Install

Two minutes.

terminal bash
pnpm add @paragraphs/next next-intl
# or: npm install / yarn add

# Get your API key from the dashboard
echo "PARAGRAPHS_API_KEY=pg_live_..." >> .env.local
echo "PARAGRAPHS_PROJECT_ID=prj_..." >> .env.local
Postbuild sync

Pull translations into messages/.

scripts/sync-translations.ts ts
import { syncToNextIntl } from "@paragraphs/next";

await syncToNextIntl({
  apiKey: process.env.PARAGRAPHS_API_KEY!,
  projectId: process.env.PARAGRAPHS_PROJECT_ID!,
  outputDir: "./messages",
  locales: ["en-GB", "es-ES", "fr-FR", "de-DE"],
  // Optional: only export approved translations
  status: "approved",
});

Wire it into package.json:

package.json json
{
  "scripts": {
    "build": "next build && tsx scripts/sync-translations.ts",
    "translations:sync": "tsx scripts/sync-translations.ts"
  }
}
CMS content

For dynamic content, use the server fetcher.

If your content lives in Sanity, headless WP, or Payload, `messages/` won't cover it. The Paragraphs server fetcher handles locale-aware reads and substitutes translatable fields inline.

app/[locale]/blog/[slug]/page.tsx tsx
import { fetchPost } from "@paragraphs/next/sanity";
import { getTranslations } from "next-intl/server";

export default async function Post({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>;
}) {
  const { locale, slug } = await params;
  const t = await getTranslations({ locale, namespace: "Post" });
  const post = await fetchPost({ slug, locale });

  return (
    <article>
      <h1>{post.title}</h1>
      <PortableText value={post.body} />
      <p>{t("publishedAt", { date: post.publishedAt })}</p>
    </article>
  );
}
Webhook updates

Real-time without a rebuild.

Paragraphs can POST to your app whenever translations are approved. Pair with Next.js on-demand revalidation to keep static pages fresh without a full rebuild.

app/api/paragraphs-webhook/route.ts ts
import { verifyWebhook } from "@paragraphs/next";
import { revalidateTag } from "next/cache";

export async function POST(req: Request) {
  const event = await verifyWebhook(req, {
    secret: process.env.PARAGRAPHS_WEBHOOK_SECRET!,
  });

  if (event.type === "translation.approved") {
    revalidateTag(`post:${event.data.unitId}`);
  }

  return Response.json({ ok: true });
}
Edge runtime alternative

No build step, no postbuild.

If you prefer to skip the sync step entirely, the @paragraphs/next middleware can fetch the latest approved translations at request time from Cloudflare KV. Great for content that changes often.

middleware.ts ts
import createMiddleware from "next-intl/middleware";
import { paragraphsEdge } from "@paragraphs/next/edge";

export default paragraphsEdge(
  createMiddleware({
    locales: ["en-GB", "es-ES", "fr-FR"],
    defaultLocale: "en-GB",
  })
);

export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)"],
};

Want a working example?

The docs include a complete Next.js + next-intl starter on GitHub.