Atlas CMS
Use Cases

Landing Page

Build a dynamic landing page in Next.js that renders components based on an Atlas CMS page block tree.

Dynamic Landing Page

This guide walks through building a dynamic landing page driven by the Atlas Pages API. Instead of hardcoding sections (like Hero, Feature Grid, Testimonials) in your React code, you will fetch a page's block tree and render the corresponding React components recursively.

Both pages are Server Components — the API key stays on the server and is never exposed to the browser.

What We're Building

app/
  [slug]/
    page.tsx          ← dynamically renders the block tree
components/
  blocks/
    HeroBlock.tsx     ← renders the 'hero' block type
    FeatureGrid.tsx   ← renders the 'feature-grid' block type
    BlockRenderer.tsx ← maps block types to React components
lib/
  atlas.ts            ← API helpers

1. API Helpers

We need a function to fetch a single page by its slug, which includes its block tree.

lib/atlas.ts
const BASE = process.env.ATLAS_BASE_URL + '/api/v1/public'

const headers = {
  'X-API-Key': process.env.ATLAS_API_KEY!,
}

export interface Block {
  id: string
  type: string
  position: number
  data: string // JSON string
  children: Block[]
}

export interface PageData {
  page: {
    slug: string
    seo: {
      title: string
      description?: string
      og_image?: string
    }
  }
  blocks: Block[]
}

export async function getPage(slug: string): Promise<PageData | null> {
  const res = await fetch(`${BASE}/pages/${slug}`, {
    headers,
    next: { revalidate: 60 },
  })
  if (!res.ok) return null
  const { data } = await res.json()
  return data
}

2. Block Renderer

The BlockRenderer takes an array of blocks and maps each block's type to a specific React component.

components/blocks/BlockRenderer.tsx
import { HeroBlock } from './HeroBlock'
import { FeatureGrid } from './FeatureGrid'
import type { Block } from '@/lib/atlas'

interface Props {
  blocks: Block[]
}

export function BlockRenderer({ blocks }: Props) {
  return (
    <>
      {blocks.sort((a, b) => a.position - b.position).map((block) => {
        // Parse the block data (which is a JSON string)
        const data = JSON.parse(block.data)

        switch (block.type) {
          case 'hero':
            return <HeroBlock key={block.id} data={data} />
          case 'feature-grid':
            return (
              <FeatureGrid key={block.id} data={data}>
                {/* Recursively render children if the block supports nested blocks */}
                <BlockRenderer blocks={block.children} />
              </FeatureGrid>
            )
          default:
            // Fallback for unknown blocks
            console.warn(`Unknown block type: ${block.type}`)
            return null
        }
      })}
    </>
  )
}

3. Block Components

Create the individual React components that correspond to your Atlas block types.

components/blocks/HeroBlock.tsx
interface HeroData {
  heading: string
  subtext?: string
  cta_label?: string
  cta_url?: string
}

export function HeroBlock({ data }: { data: HeroData }) {
  return (
    <section className="bg-slate-900 text-white py-20 text-center">
      <div className="container mx-auto px-4">
        <h1 className="text-5xl font-extrabold mb-6">{data.heading}</h1>
        {data.subtext && <p className="text-xl text-slate-300 mb-8 max-w-2xl mx-auto">{data.subtext}</p>}
        {data.cta_label && data.cta_url && (
          <a href={data.cta_url} className="bg-blue-600 hover:bg-blue-700 px-8 py-3 rounded-full font-semibold transition">
            {data.cta_label}
          </a>
        )}
      </div>
    </section>
  )
}

4. The Dynamic Page Route

Finally, create a dynamic catch-all route (or specific [slug] route) that fetches the page and renders its blocks.

app/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getPage } from '@/lib/atlas'
import { BlockRenderer } from '@/components/blocks/BlockRenderer'
import type { Metadata } from 'next'

type Props = { params: Promise<{ slug: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const pageData = await getPage(slug)
  if (!pageData) return {}

  const { seo } = pageData.page
  return {
    title: seo.title,
    description: seo.description,
    openGraph: seo.og_image ? { images: [seo.og_image] } : undefined,
  }
}

export default async function DynamicPage({ params }: Props) {
  const { slug } = await params
  const pageData = await getPage(slug)
  
  if (!pageData) notFound()

  return (
    <main>
      <BlockRenderer blocks={pageData.blocks} />
    </main>
  )
}

Home Page

To render the dynamic page on your site's root (/), you can reuse the same logic in app/page.tsx, but pass a hardcoded slug like 'home' to getPage('home').


Next Steps

On this page