Next.js is one of the most popular frameworks suitable for building any sort of website, such as a web application, an eCommerce website, or a blog.
In this tutorial, we will learn how to build a blog with Next.js and WordPress and how to leverage AI to generate content for our blog.
For simplicity - all the snippets in this post are unstyled - with just enough Tailwind CSS classes for layout purposes. You can find the full source code of this minimal Next.js Wordpress Blog Starter on GitHub.
Why WordPress, though?
WordPress is the most popular CMS (Content Management System) in the world, powering a good part of the internet. There are various reasons why you may want to use Wordpress as your CMS, despite no longer being a particularly modern or cutting-edge technology.
For example, here's why you may want to choose WordPress as your CMS in 2023:
- Familiarity: Given its popularity, your content team may already be familiar with it.
- Documentation: It is easy to use and has tons of documentation available.
- Community: Has a large community of developers, agencies and freelancers available.
- Ecosystem: Has a large ecosystem of plugins and themes available.
- Hosting: Plenty of hosting options available.
- Migration: Easy to migrate to another CMS if needed.
- Saleability: If ever plan on selling your blog, know that WordPress is the de-facto standard for blogs and websites, so it will be easier to find a buyer that wants to acquire your blog.
Getting Started
Ok, let's get started. To create our application, we will leverage the following technologies:
- Next.js 13 with App Router
- Wordpress 6.0
- Tailwind CSS
- Shadcn UI for our UI components
Creating a new Next.js application
To install your Next.js application, you can use the following command:
npx create-next-app@latest nextjs-wordpress-blog --typescript --eslint --tailwind
When going through the prompts, I used the following options:
Ok to proceed? (y) y
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
Creating a new Next.js app in /Users/giancarlo/Code/nextjs-wordpress-blog.
Awesome, now that we have our Next.js application created, let's install the dependencies we need.
Installing ShadCN UI
ShadCN UI is a UI component library for React built with Tailwind CSS and Radix UI. It is a great choice for building any sort of UIs, as it has a lot of components available and it is easy to customize.
To install ShadCN UI, we can use the following command:
npx shadcn-ui@latest init
When going through the prompts, I used the following options:
✔ Would you like to use TypeScript (recommended)? … yes
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Where is your global CSS file? … app/globals.css
✔ Would you like to use CSS variables for colors? … yes
✔ Where is your tailwind.config.js located? … tailwind.config.js
✔ Configure the import alias for components: … @/components
✔ Configure the import alias for utils: … @/lib/utils
✔ Are you using React Server Components? … yes
✔ Write configuration to components.json. Proceed? … yes
Installing WordPress
To install WordPress we can use Docker - so that we don't have to install PHP and MySQL on our machine.
To run a WordPress instance with Docker, we create a docker-compose.yml
file with the following content:
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
WP_ENVIRONMENT_TYPE: local
volumes:
- wordpress:/var/www/html
db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
volumes:
wordpress:
db:
Installing Wordpress typescript definitions
To install WordPress typescript definitions, we can use the following command:
npm i wp-types -D
We will use these types to type our WordPress API client.
Running Wordpress
To run WordPress, we can use the following command from the root of our project:
docker-compose up
Of course, make sure you have Docker installed on your machine and that it is running.
Configuring WordPress
Once running, you should be able to access WordPress at http://localhost:8080. Follow the instructions to configure Wordpress and create your admin user.
Once done, you should be able to access the WordPress admin panel at http://localhost:8080/wp-admin 🎉.
Feel free to add some dummy content to Wordpress to test our Next.js application later.
Building the WordPress Blog with Next.js
Now that we have our Next.js application and our WordPress instance running, we can start building our blog with Next.js App Router, the new routing system for Next.js that allows us to build hyper-performant websites with the power of React Server Components.
Creating an API to interact with WordPress from Next.js
To interact with WordPress from Next.js, we will create an API that will fetch the data from WordPress and return it to our Next.js application. Since there aren't great options for SDKs to interact with WordPress from Next.js, we will use the REST API directly.
Our local WordPress instance is running at http://localhost:8080, so we will use that as our base URL for the API. To retrieve the posts from WordPress, we will use the following endpoint: http://localhost:8080/wp-json/wp/v2/posts. You can visit it with your browser to see the data that is returned from the API and how it is structured.
Let's create a new file called lib/wordpress/wp-client.ts
, where we can create a class called WpClient
that will be responsible for interacting with the WordPress REST API.
To begin with, we keep it simple and create only two methods: getPosts
and getPost
. The getPosts
method will fetch all the posts from WordPress, while the getPost
method will fetch a single post by its ID.
import { WP_REST_API_Post as Post } from "wp-types";
const API_BASE_URL =
process.env.WORDPRESS_API_BASE_URL ?? "http://localhost:8080/wp-json";
const API_VERSION = process.env.WORDPRESS_API_VERSION ?? "v2";
const BASE_URL = `${API_BASE_URL}/wp/${API_VERSION}/`;
const DEFAULT_POSTS_PARAMS = {
per_page: 10,
page: 1,
search: "",
slug: <string[]>[],
};
type GetPostsParams = Partial<typeof DEFAULT_POSTS_PARAMS>;
export default class WpClient {
constructor(
private readonly username: string,
private readonly password: string,
) {}
async getPosts(params: Partial<typeof DEFAULT_POSTS_PARAMS> = {}): Promise<{
posts: Post[];
totalPages: number;
}> {
const queryParams = this.queryString({
...DEFAULT_POSTS_PARAMS,
...params,
});
const url = `${BASE_URL}posts${queryParams}`;
const response = await fetch(url);
const totalPagesHeader = response.headers.get('X-WP-TotalPages');
const totalPages = totalPagesHeader ? Number(totalPagesHeader) : 0;
const posts = await response.json();
return {
posts,
totalPages,
};
}
async getPost(id: number): Promise<Post> {
const response = await fetch(`${BASE_URL}posts/${id}`);
return response.json();
}
private getHeaders() {
const auth = this.createWordpressBasicAuthHeader(
this.username,
this.password,
);
return {
Authorization: auth,
"Content-Type": "application/json",
Accept: "application/json",
};
}
private createWordpressBasicAuthHeader(username: string, password: string) {
const buffer = Buffer.from(`${username}:${password}`, "binary");
const encoded = buffer.toString("base64");
return `Basic ${encoded}`;
}
private queryString(params: Record<string, string | string[] | number>) {
const queryParams = Object.keys(params)
.map((key) => `${key}=${encodeURIComponent(params[key].toString())}`)
.join("&");
return queryParams ? `?${queryParams}` : "";
}
}
Now we can create an instance of the service with the username and password already provided - which we will get from environment variables - and export it from lib/wordpress/wp-service.ts
:
import WpClient from "./wp-client";
const WORDPRESS_USERNAME = process.env.WORDPRESS_USERNAME;
const WORDPRESS_PASSWORD = process.env.WORDPRESS_PASSWORD;
if (!WORDPRESS_USERNAME || !WORDPRESS_PASSWORD) {
throw new Error(
"Please provide a WORDPRESS_USERNAME and WORDPRESS_PASSWORD environment variables",
);
}
const wpService = new WpClient(WORDPRESS_USERNAME, WORDPRESS_PASSWORD);
export default wpService;
Now we can use this service to fetch the posts from Wordpress and return them to our Next.js application. For example, to retrieve all the posts, we can use the following code:
import wpService from 'lib/wordpress/wp-service';
export default async function getPosts() {
return wpService.getPosts();
}
Creating an Application Password
Creating a Password is not required to read posts, but we will need it for inserting posts later on using the ChatGPT API and the WordPress REST API.
If you want to create an Application Password, you can do it by going to the WordPress admin panel and clicking on the "Users" menu item. Then, click on the user itself and scroll down to the "Application Passwords" section. You will now be able to create a new password for your application.
We need to now insert the username and password in our .env.local
file:
WORDPRESS_USERNAME=your-username
WORDPRESS_PASSWORD=your-password
Creating the Home page
From the home page, we will display te list of the first 10 posts from WordPress. To do that, we can use the getPosts
function we created in the previous step.
Let's update the app/page.tsx
file with the following content:
import { use } from "react";
import Link from "next/link";
import wpService from "@/lib/wordpress/wp-service";
export default function Home() {
const { posts } = use(wpService.getPosts());
return (
<div className={"container mx-auto my-8"}>
<div className={"flex flex-col space-y-8"}>
<h1 className={"text-xl xl:text-2xl font-bold"}>Posts</h1>
<div
className={
"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 xl:gap-8"
}
>
{posts.map((post) => (
<div className={"flex flex-col space-y-5"} key={post.id}>
<Link href={`/posts/${post.slug}`}>
<h2>{post.title.rendered}</h2>
<span>{post.excerpt.protected}</span>
</Link>
</div>
))}
</div>
</div>
</div>
);
}
If you have added some posts to your WordPress instance, you should now see them on the home page! 🎉
Adding a Post page
Now that we have the home page, we can add a page to display a single post. To do that, we will create a new file called app/posts/[slug]/page.tsx
with the following content:
import { use } from "react";
import { notFound } from "next/navigation";
import wpService from "@/lib/wordpress/wp-service";
interface PostPageParams {
params: {
slug: string;
};
}
function PostPage({ params }: PostPageParams) {
const { posts } = use(
wpService.getPosts({
slug: [params.slug],
}),
);
const post = posts ? posts[0] : null;
if (!post) {
notFound();
}
return (
<div className={"my-8 container mx-auto"}>
<div className={"flex flex-col space-y-4"}>
<div className={"flex flex-col space-y-2"}>
<h1 className={"text-3xl font-semibold"}>{post.title.rendered}</h1>
<h2 className={"text-xl font-semibold"}>{post.excerpt.raw}</h2>
</div>
<div>
<div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</div>
</div>
</div>
);
}
export default PostPage;
We can now display our posts on their own pages! 🎉
This is pretty bare-bones, but you can now start adding some styles to make it look better.
Static Rendering
Assuming you have a limited set of pages, you could also choose to statically generate the pages at build time. In this way, you don't need to fetch the posts at runtime from your WordPress server, but you can fetch them at build time and generate the pages as static HTML.
To do that, we can use the function generateStaticParams
that we can export from the page. This function will be called at build time and will generate the pages statically.
For simplicity, we will generate the pages for the first 100 posts (as the WordPress API limits us to). If you want to generate more pages, you should likely use a recursive function to fetch all the pages.
If you want to generate the pages statically, you can export the function below from the app/posts/[slug]/page.tsx
file:
export async function generateStaticParams() {
const { posts } = await wpService.getPosts({
per_page: 100,
});
return posts.map((post) => ({
slug: post.slug,
}));
}
If you now run npm run build
, the pages will be generated statically - instead of being fetched a runtime. This can be great if you don't want to deploy a WordPress server and you want to use the WordPress API as a headless CMS locally.
Of course, there is a certain limit to this approach. If you have a ton of pages, you will likely want to use a WordPress server to fetch the pages at runtime.
Adding pagination to the home page
A blog can't scale without a solid pagination system. Let's add one to our home page!
To do that, we need to proceed with the following steps:
- Retrieve the total number of pages for the current query using the
X-WP-TotalPages
header - Add a
page
query parameter that we use to fetch the posts from WordPress so we can paginate them - Display the pages at the bottom of the home page and link to them using the
page
query parameter
Retrieving the total number of pages
We have already retrieved the total pages using the header X-WP-TotalPages
in the wp-service.ts
file. We can now use this value to display the pagination links.
Adding a "page" query parameter
We can use a page
query parameter for pagination. We will then use this parameter to link to the appropriate page when we display the pagination links.
The Next.js Page components provide an argument searchParams
that we can use to retrieve the query parameters. Let's update the app/page.tsx
file with the following content:
interface HomePageParams {
searchParams: {
page?: string
}
}
export default function Home({ searchParams }: HomePageParams) {
const page = searchParams.page ? parseInt(searchParams.page) : 1
const posts = use(wpService.getPosts({ page }));
// ...
}
Displaying the pagination links
We can now display the pagination links at the bottom of the home page. Let's update the app/page.tsx
file with the following content:
import Link from "next/link";
function PaginationLinks({
currentPage,
totalPages,
}: React.PropsWithChildren<{
currentPage: number;
totalPages: number;
}>) {
const pagesArray = Array(totalPages)
.fill(null)
.map((_, page) => page + 1);
return (
<div className={'flex space-x-4'}>
{pagesArray.map((page) => {
const isSelected = page === currentPage;
const className = isSelected ? 'font-bold' : 'hover:font-medium';
return (
<Link key={page} className={className} href={`/?page=${page}`}>
{page}
</Link>
);
})}
</div>
);
}
Putting it all together
Now let's update the home page to display the pagination links at the bottom of the page:
import { use } from 'react';
import Link from 'next/link';
import wpService from '@/lib/wordpress/wp-service';
interface HomePageParams {
searchParams: {
page?: string;
};
}
export default function Home({ searchParams }: HomePageParams) {
const page = searchParams.page ? parseInt(searchParams.page) : 1;
const { posts, totalPages } = use(wpService.getPosts({ page }));
return (
<div className={'container mx-auto my-8'}>
<div className={'flex flex-col space-y-8'}>
<h1 className={'text-xl xl:text-2xl font-bold'}>Posts</h1>
<div
className={
'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 xl:gap-8'
}
>
{posts.map((post) => (
<div className={'flex flex-col space-y-5'} key={post.id}>
<Link href={`/posts/${post.slug}`}>
<h2>{post.title.rendered}</h2>
<span>{post.excerpt.protected}</span>
</Link>
</div>
))}
</div>
<PaginationLinks currentPage={page} totalPages={totalPages} />
</div>
</div>
);
}
function PaginationLinks({
currentPage,
totalPages,
}: React.PropsWithChildren<{
currentPage: number;
totalPages: number;
}>) {
const pagesArray = Array(totalPages)
.fill(null)
.map((_, page) => page + 1);
return (
<div className={'flex space-x-4'}>
{pagesArray.map((page) => {
const isSelected = page === currentPage;
const className = isSelected ? 'font-bold' : 'hover:font-medium';
return (
<Link key={page} className={className} href={`/?page=${page}`}>
{page}
</Link>
);
})}
</div>
);
}
NB: remember to add enough posts to your WordPress site to see the pagination links.
It's all very bare-bones still, but hey, it works! 🎉
Using ChatGPT to generate content
Now that we have a blog, we need to write content for it. But writing content is hard and takes a lot of time. What if we could generate content automatically? The ChatGPT API can help us with that!
We will now create a small script that we can run so to generate content for our blog. If you know PHP and Wordpress, you can probably create a plugin that does the same thing - but it's not the focus of this tutorial. Instead, we write a CLI script that we can run locally with a Node.js runtime (but easily adapted to run on Cloudflare Workers, Deno, etc.)
When the content is generated from the OpenAI ChatGPT API, we will use the WordPress API to create a new post with the generated content. You can then use this content as a starting point for your blog posts.
Installing Dependencies
Now, we want to install some dependencies to help us with our script. Run the following command to install the dependencies:
npm i openai-edge dotenv
The command will install the following dependencies:
openai-edge
: a library to interact with the OpenAI API that works in edge environments (e.g. Cloudflare, Deno, etc.)dotenv
: a library to read environment variables from a.env
file
Retrieving the OpenAI API key
This post assumes you have an OpenAI account and you have created an API key. If you don't have one, you can create one for free on the OpenAI website.
With your key in hand, create a .env.local
file at the root of your project with the following content:
OPENAI_API_KEY=your-api-key
We will read the API key from this file in the next section.
Creating the OpenAI client
Let's create a lib/openai-client.ts
file with the following content:
import { Configuration, OpenAIApi } from 'openai-edge';
function getOpenAIClient() {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY env variable is not set');
}
const configuration = new Configuration({ apiKey });
return new OpenAIApi(configuration);
}
export default getOpenAIClient;
Creating a function to generate content
Let's create a function that we can use to generate content using the OpenAI API. Create a lib/generate-content.ts
file with the following content:
import type { CreateChatCompletionResponse } from 'openai-edge';
import getOpenAIClient from './openai-client';
interface GeneratePostParams {
title: string;
maxTokens?: number;
temperature?: number;
}
const MODEL = `gpt-3.5-turbo`;
export async function generatePostContent(
params: GeneratePostParams
) {
const { title, maxTokens, temperature } = params;
const client = getOpenAIClient();
const content = getCreatePostPrompt(title);
const response = await client.createChatCompletion({
model: MODEL,
temperature: temperature ?? 0.7,
max_tokens: maxTokens ?? 500,
messages: [
{
role: 'user',
content,
},
],
});
const json = (await response.json()) as CreateChatCompletionResponse;
const usage = json.usage?.total_tokens ?? 0;
const text = getResponseContent(json);
return {
text,
usage,
};
}
// make sure to use a better prompt than this one
// this is just an example to get you started :)
function getCreatePostPrompt(title: string) {
return `
Write a blog post under 500 words whose title is "${title}".
`;
}
function getResponseContent(response: CreateChatCompletionResponse) {
return (response.choices ?? []).reduce((acc, choice) => {
return acc + (choice.message?.content ?? '');
}, '');
}
The prompt is extremely basic - not great at all. For now, it's good enough to get us started and see some data in our blog.
Creating the Node.js Script to generate content
We can finally put everything together and create a script that we can run to generate content for our blog.
First, we need a script that loads the environment variables from the .env.local
file using the dotenv
library. Create a scripts/env.ts
file with the following content:
import { config } from 'dotenv';
// read .env.local variables
config({
path: '.env.local',
});
Then, create a scripts/generate.ts
file with the following content:
import './env';
import { generatePostContent } from '@/lib/generate-content';
async function main() {
const title = process.argv[2];
if (!title) {
throw new Error('Please provide a title');
}
const { text, usage } = await generatePostContent({ title });
console.log(`Usage: ${usage} tokens`);
console.log(text);
}
// run the script with `npx tsx scripts/generate.ts "My Blog Post"`
void main();
At the moment the text generated by the OpenAI API is simply printed to the console. We will improve this later and use the WordPress API to create a new post with the generated content.
Running the script
Let's add a script to our package.json
file so we can run the script easily. Add the following script to the scripts
section of your package.json
file:
{
"scripts": {
// ...
"generate": "npx tsx scripts/generate.ts"
}
}
NB: you can also choose to install tsx
globally and run the script with tsx scripts/generate.ts
.
Now, you can run the script with the following command:
npm run generate -- "My Blog Post"
The script will generate some content and print it to the console - but it's not very useful at the moment. Let's improve it in the next section, so that the content is automatically added to our WordPress site.
Creating the methods to insert posts into WordPress
Let's go back to our WordPress client and extend it with a new method that we can use to create a new post. Append the method to the WpClient
class in the lib/wordpress/wp-client.ts
file:
import {
WP_REST_API_Post as Post,
WP_Post_Status_Name as PostStatus,
} from 'wp-types';
async insertPost(params: {
title: string;
content: string;
status: PostStatus;
excerpt?: string;
}) {
const url = `${BASE_URL}posts`;
const headers = this.getHeaders();
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`Failed to insert post: ${response.statusText}`);
}
return response.json();
}
NB: there are many more parameters that we can pass to the insertPost
method, but we will keep it simple for now. If you want to learn more about the available parameters, check out the WordPress REST API documentation.
Inserting Posts into Wordpress from the ChatGPT API
We can now update our scripts/generate.ts
script to insert the generated content into our WordPress site.
Update the scripts/generate.ts
file with the following content:
import { WP_Post_Status_Name as PostStatus } from 'wp-types';
import './env';
import { generatePostContent } from '@/lib/generate-content';
import wpService from '@/lib/wordpress/wp-service';
async function main() {
const title = process.argv[2];
if (!title) {
throw new Error('Please provide a title');
}
console.log(`Generating content for post "${title}"...`);
const { text, usage } = await generatePostContent({ title });
console.log(`Usage: ${usage} tokens`);
await wpService.insertPost({
title,
content: text,
status: 'publish' as PostStatus,
});
console.log(`Post "${title}" successfully created`);
}
// run the script with `npx tsx scripts/generate.ts "My Blog Post"`
void main();
Our script is now complete and we can run it to generate content and insert it into our WordPress site. Run the script with the following command:
npm run generate -- "A good post title"
The script will generate some content and insert it into your WordPress site. You can now go to your WordPress site and see the new post.
Conclusion
That's it! We've finally reached the end of this tutorial.
We've learned how to build a small blog with Next.js App Router using Wordpress as a CMS, and use the OpenAI API to generate content for our blog posts and how to insert the generated content into our WordPress site.
Here's your homework:
- Improve the prompt that we send to the OpenAI API to generate better content
- Add styles to your blog and leverage Shadcn UI to make it look amazing
- Launch your blog and start writing content!
You can find the full source code for this Blog Starter on GitHub.
Hope this post was useful to you. If you have any questions, feel free to join our Discord server and ask away!