Next.js
Integrate the Laizy CMS generated client with Next.js App Router for server components, API routes, and static generation.
Next.js Integration
Laizy CMS works naturally with Next.js App Router. The generated client runs in React Server Components for server-side rendering, in API routes for backend operations, and with frontend tokens for client-side dynamic content.
Setup
Initialize and generate the client
If you have not already, initialize your Laizy project and generate the TypeScript client:
pnpm laizy init
pnpm laizy sync
pnpm laizy generateThis creates the generated client in generated/laizy/ and sets up your .env.local with the necessary environment variables.
Create a client singleton
Create a shared client instance that can be imported anywhere in your application:
// lib/cms.ts
import { LaizyClient } from '@/generated/laizy';
import { ManagementClient } from '@/lib/management-client';
const managementClient = new ManagementClient({
baseUrl: process.env.NEXT_PUBLIC_LAIZY_BASE_URL!,
apiToken: process.env.LAIZY_API_TOKEN!,
projectId: process.env.LAIZY_PROJECT_ID!,
});
export const cms = new LaizyClient(managementClient);Add environment variables
Ensure these are set in your .env.local:
NEXT_PUBLIC_LAIZY_BASE_URL=https://laizycms.com
LAIZY_API_TOKEN=laizy_eyJhbG... # Admin token (server-side only)
NEXT_PUBLIC_LAIZY_TOKEN=laizy_eyJhbG... # Frontend token (safe for client)
LAIZY_PROJECT_ID=your-project-idVariables prefixed with NEXT_PUBLIC_ are safe for client-side use. The LAIZY_API_TOKEN without the prefix is server-only.
Server Components
The most common pattern. Fetch content directly in React Server Components with full type safety:
// app/blog/page.tsx
import { cms } from '@/lib/cms';
import type { BlogPost } from '@/generated/laizy';
export default async function BlogPage() {
const posts = await cms.blogPost.findMany({
limit: 20,
});
return (
<div>
<h1>Blog</h1>
<ul>
{posts.map((post: BlogPost) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}Dynamic Route with findById
// app/blog/[id]/page.tsx
import { cms } from '@/lib/cms';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function BlogPostPage({ params }: PageProps) {
const { id } = await params;
const post = await cms.blogPost.findById(id);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<time>{post.createdAt.toLocaleDateString()}</time>
</article>
);
}API Routes
Use the admin client in API routes for server-side operations:
// app/api/posts/route.ts
import { cms } from '@/lib/cms';
import { NextResponse } from 'next/server';
export async function GET() {
const posts = await cms.blogPost.findMany();
return NextResponse.json(posts);
}Client Components
For dynamic client-side content, create a separate client instance with the frontend token:
// lib/cms-client.ts
'use client';
import { LaizyClient } from '@/generated/laizy';
import { ManagementClient } from '@/lib/management-client';
const managementClient = new ManagementClient({
baseUrl: process.env.NEXT_PUBLIC_LAIZY_BASE_URL!,
apiToken: process.env.NEXT_PUBLIC_LAIZY_TOKEN!, // Frontend token only
projectId: process.env.NEXT_PUBLIC_LAIZY_PROJECT_ID!,
});
export const cmsClient = new LaizyClient(managementClient);Only use frontend tokens (content:read scope) in client components. These tokens can only read published content and are safe to expose in the browser.
Use in a client component with React hooks:
// components/post-list.tsx
'use client';
import { useEffect, useState } from 'react';
import type { BlogPost } from '@/generated/laizy';
import { cmsClient } from '@/lib/cms-client';
export function PostList() {
const [posts, setPosts] = useState<BlogPost[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
cmsClient.blogPost.findMany({ limit: 10 })
.then(setPosts)
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Static Site Generation
Use generateStaticParams to pre-render content pages at build time:
// app/blog/[id]/page.tsx
import { cms } from '@/lib/cms';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const posts = await cms.blogPost.findMany();
return posts.map((post) => ({ id: post.id }));
}
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function BlogPostPage({ params }: PageProps) {
const { id } = await params;
const post = await cms.blogPost.findById(id);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}Landing Page Pattern
A common pattern is using Laizy to manage marketing content sections:
// app/page.tsx
import { cms } from '@/lib/cms';
export default async function HomePage() {
const [hero, footer, newsletter] = await Promise.all([
cms.heroSection.findMany({ limit: 1 }),
cms.footerContent.findMany({ limit: 1 }),
cms.newsletterSection.findMany({ limit: 1 }),
]);
const heroContent = hero[0];
const footerContent = footer[0];
const newsletterContent = newsletter[0];
return (
<main>
{heroContent && (
<section>
{heroContent.badge && <span>{heroContent.badge}</span>}
<h1>{heroContent.headline}</h1>
<p>{heroContent.subheading}</p>
</section>
)}
{newsletterContent && (
<section>
<h2>{newsletterContent.title}</h2>
<p>{newsletterContent.description}</p>
</section>
)}
{footerContent && (
<footer>
<p>{footerContent.companyDescription}</p>
<p>{footerContent.copyrightText}</p>
</footer>
)}
</main>
);
}Error Handling
Wrap CMS calls with error boundaries for production resilience:
// app/blog/page.tsx
import { cms } from '@/lib/cms';
export default async function BlogPage() {
let posts;
try {
posts = await cms.blogPost.findMany({ limit: 20 });
} catch (error) {
console.error('Failed to fetch posts:', error);
posts = [];
}
if (posts.length === 0) {
return <p>No posts available.</p>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Next Steps
- React Integration -- For non-Next.js React applications
- Queries Reference -- Full API for
findMany(),findById(), andcount() - Authentication -- Token types and security