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
PARALLELE_API_KEY=prl_your_api_key_here2. Create API Helpers
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
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
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
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
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
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">
"{item.data.quote}"
</blockquote>
<p className="mt-4 font-medium">{item.data.name}</p>
</div>
))}
</div>
</main>
);
}Blog
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
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
"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>
);
}