Laizy CMS
Integrations

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 generate

This 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-id

Variables 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

On this page