Next.js Integration
This guide explains how to use WP Block to HTML with Next.js to build high-performance websites with WordPress as a headless CMS.
Setting Up a Next.js Project
First, create a new Next.js project if you don't have one already:
npx create-next-app@latest my-wp-nextjs-app
cd my-wp-nextjs-app
Next, install WP Block to HTML:
npm install wp-block-to-html
Server-Side Rendering (SSR)
One of the biggest advantages of using Next.js is server-side rendering. Here's how to use WP Block to HTML with Next.js's getServerSideProps
:
// pages/posts/[slug].js
import Head from 'next/head';
import { convertBlocks } from 'wp-block-to-html';
export default function Post({ title, excerpt, content }) {
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={excerpt} />
</Head>
<main>
<article>
<h1>{title}</h1>
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
</main>
</>
);
}
export async function getServerSideProps({ params }) {
try {
// Fetch post data from WordPress
const res = await fetch(
`https://your-wordpress-site.com/wp-json/wp/v2/posts?slug=${params.slug}&_fields=id,title,excerpt,content,blocks`
);
if (!res.ok) {
return { notFound: true };
}
const posts = await res.json();
if (!posts.length) {
return { notFound: true };
}
const post = posts[0];
// Process content based on what's available
let content = '';
if (post.content) {
// Convert blocks to HTML
content = convertBlocks(post.content, {
cssFramework: 'tailwind', // Match your project's CSS framework
ssrOptions: {
enabled: true,
optimizationLevel: 'balanced',
lazyLoadMedia: true
}
});
} else if (post.content?.rendered) {
// Use pre-rendered content if blocks aren't available
content = post.content.rendered;
}
return {
props: {
title: post.title.rendered,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
content
}
};
} catch (error) {
console.error('Error fetching post:', error);
return { notFound: true };
}
}
Static Site Generation (SSG)
For even better performance, you can use Static Site Generation with getStaticProps
and getStaticPaths
:
// pages/posts/[slug].js
import Head from 'next/head';
import { convertBlocks } from 'wp-block-to-html';
export default function Post({ title, excerpt, content }) {
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={excerpt} />
</Head>
<main>
<article>
<h1>{title}</h1>
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
</main>
</>
);
}
export async function getStaticPaths() {
// Fetch all post slugs from WordPress
const res = await fetch(
'https://your-wordpress-site.com/wp-json/wp/v2/posts?_fields=slug'
);
const posts = await res.json();
// Create paths for each post
const paths = posts.map((post) => ({
params: { slug: post.slug }
}));
return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }) {
try {
// Fetch post data from WordPress
const res = await fetch(
`https://your-wordpress-site.com/wp-json/wp/v2/posts?slug=${params.slug}&_fields=id,title,excerpt,content,blocks`
);
if (!res.ok) {
return { notFound: true };
}
const posts = await res.json();
if (!posts.length) {
return { notFound: true };
}
const post = posts[0];
// Process content based on what's available
let content = '';
if (post.content) {
// Convert blocks to HTML
content = convertBlocks(post.content, {
cssFramework: 'tailwind',
ssrOptions: {
enabled: true,
optimizationLevel: 'maximum',
lazyLoadMedia: true
}
});
} else if (post.content?.rendered) {
// Use pre-rendered content if blocks aren't available
content = post.content.rendered;
}
return {
props: {
title: post.title.rendered,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
content
},
// Revalidate once per day (in seconds)
revalidate: 86400
};
} catch (error) {
console.error('Error fetching post:', error);
return { notFound: true };
}
}
Incremental Static Regeneration (ISR)
For the best of both worlds, you can use Incremental Static Regeneration:
// In getStaticProps, add the revalidate property
export async function getStaticProps({ params }) {
// ... (same as above)
return {
props: {
title: post.title.rendered,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
content
},
// Revalidate every hour (in seconds)
revalidate: 3600
};
}
Using React Components
If you prefer using React components instead of HTML strings, you can use the React integration:
// pages/posts/[slug].js
import Head from 'next/head';
import { createReactComponent } from 'wp-block-to-html/react';
export default function Post({ title, excerpt, blocks }) {
// Create a React component from the blocks
const BlockContent = blocks ? createReactComponent(blocks, {
cssFramework: 'tailwind',
ssrOptions: {
enabled: true,
optimizationLevel: 'balanced'
}
}) : null;
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={excerpt} />
</Head>
<main>
<article>
<h1>{title}</h1>
{BlockContent ? (
<BlockContent />
) : (
<div className="error">No content available</div>
)}
</article>
</main>
</>
);
}
export async function getStaticProps({ params }) {
try {
// Fetch post data from WordPress
const res = await fetch(
`https://your-wordpress-site.com/wp-json/wp/v2/posts?slug=${params.slug}&_fields=id,title,excerpt,blocks`
);
if (!res.ok) {
return { notFound: true };
}
const posts = await res.json();
if (!posts.length) {
return { notFound: true };
}
const post = posts[0];
// If no blocks are available, return rendered content
if (!post.content) {
return {
props: {
title: post.title.rendered,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
content: post.content?.rendered || '',
blocks: null
},
revalidate: 3600
};
}
return {
props: {
title: post.title.rendered,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
blocks: post.content
},
revalidate: 3600
};
} catch (error) {
console.error('Error fetching post:', error);
return { notFound: true };
}
}
CSS Framework Integration
Tailwind CSS
If you're using Tailwind CSS with Next.js, make sure to specify Tailwind in your WP Block to HTML configuration:
const content = convertBlocks(post.content, {
cssFramework: 'tailwind',
ssrOptions: {
enabled: true
}
});
Bootstrap
If you're using Bootstrap, update your configuration accordingly:
const content = convertBlocks(post.content, {
cssFramework: 'bootstrap',
ssrOptions: {
enabled: true
}
});
Optimizing for Core Web Vitals
To optimize your Next.js app for Core Web Vitals, use these WP Block to HTML features:
const content = convertBlocks(post.content, {
cssFramework: 'tailwind',
ssrOptions: {
enabled: true,
optimizationLevel: 'maximum',
lazyLoadMedia: true,
preconnect: true,
criticalPathOnly: true,
removeDuplicateStyles: true
}
});
API Routes for Preview Mode
You can create an API route to handle WordPress preview mode:
// pages/api/preview.js
export default async function handler(req, res) {
const { secret, id, slug } = req.query;
// Check the secret and next parameters
if (secret !== process.env.WORDPRESS_PREVIEW_SECRET || !id) {
return res.status(401).json({ message: 'Invalid token' });
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({});
// Redirect to the post
res.redirect(`/posts/${slug || id}`);
}
Then update your getStaticProps function to handle preview mode:
export async function getStaticProps({ params, preview = false, previewData }) {
// Fetch parameters to pass to the WordPress API
const apiParams = new URLSearchParams({
slug: params.slug,
_fields: 'id,title,excerpt,content,blocks'
});
// Add preview-specific parameters
if (preview) {
apiParams.append('status', 'any');
// Add authentication if needed
}
const res = await fetch(
`https://your-wordpress-site.com/wp-json/wp/v2/posts?${apiParams.toString()}`
);
// Rest of the function remains the same
}
Bundle Size Optimization
For optimal bundle size, use selective imports from the library:
// Import only what you need
import { convertBlocks } from 'wp-block-to-html/core';
import { paragraphHandler, headingHandler } from 'wp-block-to-html/blocks/text';
import { imageHandler } from 'wp-block-to-html/blocks/media';
import { tailwindMapping } from 'wp-block-to-html/frameworks/tailwind';
// Register only the block handlers you need
registerBlockHandler('core/paragraph', paragraphHandler);
registerBlockHandler('core/heading', headingHandler);
registerBlockHandler('core/image', imageHandler);
// Use with custom configuration
const content = convertBlocks(post.content, {
cssFramework: 'tailwind',
customClassMap: { tailwind: tailwindMapping }
});
Complete Example
Here's a complete example of a Next.js page that integrates with WordPress:
// pages/posts/[slug].js
import Head from 'next/head';
import Link from 'next/link';
import { convertBlocks } from 'wp-block-to-html';
export default function Post({ post, morePosts }) {
return (
<div className="container mx-auto px-4 py-8">
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
</Head>
<Link href="/" className="text-blue-500 hover:underline mb-8 block">
← Back to Home
</Link>
<main>
<article>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="prose lg:prose-xl max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
</main>
{morePosts.length > 0 && (
<div className="mt-12">
<h2 className="text-2xl font-bold mb-4">More Posts</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{morePosts.map((relatedPost) => (
<Link href={`/posts/${relatedPost.slug}`} key={relatedPost.id}
className="border p-4 rounded-lg hover:shadow-lg transition-shadow">
<h3 className="text-xl font-semibold">{relatedPost.title}</h3>
<p className="mt-2 text-gray-600">{relatedPost.excerpt}</p>
</Link>
))}
</div>
</div>
)}
</div>
);
}
export async function getStaticPaths() {
const res = await fetch(
'https://your-wordpress-site.com/wp-json/wp/v2/posts?_fields=slug'
);
const posts = await res.json();
return {
paths: posts.map((post) => ({
params: { slug: post.slug }
})),
fallback: 'blocking'
};
}
export async function getStaticProps({ params }) {
try {
// Fetch main post
const res = await fetch(
`https://your-wordpress-site.com/wp-json/wp/v2/posts?slug=${params.slug}&_fields=id,title,slug,excerpt,content,blocks`
);
if (!res.ok) {
return { notFound: true };
}
const posts = await res.json();
if (!posts.length) {
return { notFound: true };
}
const post = posts[0];
// Process content
let content = '';
if (post.content) {
content = convertBlocks(post.content, {
cssFramework: 'tailwind',
ssrOptions: {
enabled: true,
optimizationLevel: 'balanced',
lazyLoadMedia: true
}
});
} else if (post.content?.rendered) {
content = post.content.rendered;
}
// Fetch related posts
const relatedRes = await fetch(
`https://your-wordpress-site.com/wp-json/wp/v2/posts?exclude=${post.id}&_fields=id,title,slug,excerpt&per_page=3`
);
const relatedPosts = await relatedRes.json();
return {
props: {
post: {
title: post.title.rendered,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
content
},
morePosts: relatedPosts.map((p) => ({
id: p.id,
title: p.title.rendered,
slug: p.slug,
excerpt: p.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 100)
}))
},
revalidate: 3600
};
} catch (error) {
console.error('Error fetching post:', error);
return { notFound: true };
}
}
Next Steps
- Learn about Server-Side Rendering for more optimization techniques
- Explore Performance Optimization for high-traffic sites
- Check out CSS Framework Integration for styling options
- Read about React Integration for more React-specific features