Parallèle Docs
Examples

Next.js Integration

Complete guide to integrating Parallèle with Next.js.

Next.js Integration

This guide covers integrating Parallèle CMS with a Next.js 15+ App Router project.

Project Setup

1. Environment Variables

.env.local
PARALLELE_API_KEY=prl_your_api_key_here

2. Create API Helpers

lib/parallele.ts
const API_URL = "https://parallele.ai";
 
async function fetchAPI(endpoint: string) {
  const res = await fetch(`${API_URL}${endpoint}`, {
    headers: { "x-api-key": process.env.PARALLELE_API_KEY! },
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}
 
export async function getContent(locale: string = "fr") {
  return fetchAPI(`/api/public/content?locale=${locale}`);
}
 
export async function getPages(locale?: string) {
  const params = locale ? `?locale=${locale}` : "";
  return fetchAPI(`/api/public/pages${params}`);
}
 
export async function getPage(slug: string, locale: string = "fr") {
  return fetchAPI(`/api/public/pages/${slug}?locale=${locale}`);
}
 
export async function getMenu(slug: string) {
  return fetchAPI(`/api/public/menus/${slug}`);
}
 
export async function getCollectionItems(slug: string, locale: string = "fr") {
  return fetchAPI(`/api/public/collections/${slug}?locale=${locale}`);
}
 
export async function getBlogPosts(locale: string = "fr", limit?: number) {
  const params = new URLSearchParams({ locale });
  if (limit) params.set("limit", String(limit));
  return fetchAPI(`/api/public/blog?${params}`);
}
 
export async function getBlogPost(slug: string, locale: string = "fr") {
  return fetchAPI(`/api/public/blog/${slug}?locale=${locale}`);
}

Basic Usage

Server Components

app/page.tsx
import { getContent } from "@/lib/parallele";
 
export default async function HomePage() {
  const content = await getContent("fr");
 
  return (
    <main>
      <section className="hero">
        <h1>{content["hero.title"]}</h1>
        <p>{content["hero.subtitle"]}</p>
        <a href="/signup">{content["cta.button"]}</a>
      </section>
    </main>
  );
}

With Locale Parameter

app/[locale]/page.tsx
import { getContent } from "@/lib/parallele";
 
type Props = {
  params: Promise<{ locale: string }>;
};
 
export default async function HomePage({ params }: Props) {
  const { locale } = await params;
  const content = await getContent(locale);
 
  return (
    <main>
      <h1>{content["hero.title"]}</h1>
    </main>
  );
}

Layout with Navigation

app/[locale]/layout.tsx
import { getMenu, getContent } from "@/lib/parallele";
 
export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
 
  const [mainMenu, footerMenu, content] = await Promise.all([
    getMenu("main"),
    getMenu("footer"),
    getContent(locale),
  ]);
 
  return (
    <>
      <header>
        <nav>
          {mainMenu.items?.map((item: any) => (
            <a key={item.id} href={item.url}>
              {item.label}
            </a>
          ))}
        </nav>
      </header>
 
      {children}
 
      <footer>
        <nav>
          {footerMenu.items?.map((item: any) => (
            <a key={item.id} href={item.url}>
              {item.label}
            </a>
          ))}
        </nav>
        <p>{content["footer.copyright"]}</p>
      </footer>
    </>
  );
}

Dynamic Pages

app/[locale]/p/[slug]/page.tsx
import { getPage, getPages } from "@/lib/parallele";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
 
type Props = {
  params: Promise<{ locale: string; slug: string }>;
};
 
export async function generateStaticParams() {
  const { pages } = await getPages();
  return pages.map((page: any) => ({
    locale: page.locale,
    slug: page.slug,
  }));
}
 
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale, slug } = await params;
  try {
    const page = await getPage(slug, locale);
    return {
      title: page.metaTitle || page.title,
      description: page.metaDescription,
    };
  } catch {
    return { title: "Page Not Found" };
  }
}
 
export default async function CMSPage({ params }: Props) {
  const { locale, slug } = await params;
 
  try {
    const page = await getPage(slug, locale);
    return (
      <article>
        <h1>{page.title}</h1>
        {/* Render page.content based on your structure */}
      </article>
    );
  } catch {
    notFound();
  }
}

Collections

app/[locale]/testimonials/page.tsx
import { getCollectionItems } from "@/lib/parallele";
 
type Props = {
  params: Promise<{ locale: string }>;
};
 
export default async function TestimonialsPage({ params }: Props) {
  const { locale } = await params;
  const { items } = await getCollectionItems("testimonials", locale);
 
  return (
    <main>
      <h1>Testimonials</h1>
      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {items.map((item: any) => (
          <div key={item.slug} className="rounded-xl border p-6">
            <blockquote className="text-lg italic">
              &quot;{item.data.quote}&quot;
            </blockquote>
            <p className="mt-4 font-medium">{item.data.name}</p>
          </div>
        ))}
      </div>
    </main>
  );
}

Blog

app/[locale]/blog/page.tsx
import { getBlogPosts } from "@/lib/parallele";
import Link from "next/link";
 
export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  const { posts } = await getBlogPosts(locale, 20);
 
  return (
    <main>
      <h1>Blog</h1>
      <div className="grid gap-8">
        {posts.map((post: any) => (
          <article key={post.slug}>
            <h2>
              <Link href={`/${locale}/blog/${post.slug}`}>{post.title}</Link>
            </h2>
            <p>{post.excerpt}</p>
            <time>{new Date(post.publishedAt).toLocaleDateString(locale)}</time>
          </article>
        ))}
      </div>
    </main>
  );
}

Client-Side Content

API Proxy Route

app/api/content/route.ts
import { NextResponse } from "next/server";
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const locale = searchParams.get("locale") || "fr";
 
  const res = await fetch(
    `https://parallele.ai/api/public/content?locale=${locale}`,
    { headers: { "x-api-key": process.env.PARALLELE_API_KEY! } }
  );
 
  const content = await res.json();
  return NextResponse.json(content);
}

Client Component

components/dynamic-hero.tsx
"use client";
 
import { useEffect, useState } from "react";
 
export function DynamicHero({ locale }: { locale: string }) {
  const [content, setContent] = useState<Record<string, string>>({});
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetch(`/api/content?locale=${locale}`)
      .then((res) => res.json())
      .then(setContent)
      .finally(() => setLoading(false));
  }, [locale]);
 
  if (loading) {
    return <div className="animate-pulse h-32 bg-slate-200 rounded" />;
  }
 
  return (
    <section>
      <h1>{content["hero.title"]}</h1>
      <p>{content["hero.subtitle"]}</p>
    </section>
  );
}

On this page