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 helpers1. API Helpers
We need a function to fetch a single page by its slug, which includes its block tree.
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.
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.
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.
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
- Pages API Reference — explore the full payload structure of the pages API.
- Localization — learn how to fetch translated block data.