Building an AI-powered Blog with Next.js and WordPress

Learn how to build a blog with Next.js 13 and WordPress and how to leverage AI to generate content.

ยท17 min read
Cover Image for Building an AI-powered Blog with Next.js and WordPress

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:

  1. Familiarity: Given its popularity, your content team may already be familiar with it.
  2. Documentation: It is easy to use and has tons of documentation available.
  3. Community: Has a large community of developers, agencies and freelancers available.
  4. Ecosystem: Has a large ecosystem of plugins and themes available.
  5. Hosting: Plenty of hosting options available.
  6. Migration: Easy to migrate to another CMS if needed.
  7. 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:

  1. Next.js 13 with App Router
  2. Wordpress 6.0
  3. Tailwind CSS
  4. 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.

lib/wordpress/wp-client.ts
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:

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:

app/page.tsx
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:

app/posts/[slug]/page.tsx
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:

app/posts/[slug]/page.tsx
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:

  1. Retrieve the total number of pages for the current query using the X-WP-TotalPages header
  2. Add a page query parameter that we use to fetch the posts from WordPress so we can paginate them
  3. 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:

app/page.tsx
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 })); // ... }

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:

app/page.tsx
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:

app/page.tsx
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:

.env.local
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:

lib/openai-client.ts
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:

lib/generate-content.ts
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 ?? ''); }, ''); }

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:

scripts/env.ts
import { config } from 'dotenv'; // read .env.local variables config({ path: '.env.local', });

Then, create a scripts/generate.ts file with the following content:

scripts/generate.ts
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:

package.json
{ "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:

lib/wordpress/wp-client.ts
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:

scripts/generate.ts
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:

  1. Improve the prompt that we send to the OpenAI API to generate better content
  2. Add styles to your blog and leverage Shadcn UI to make it look amazing
  3. 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!



Read more about Tutorials

Cover Image for Building an AI Writer SaaS with Next.js and Supabase

Building an AI Writer SaaS with Next.js and Supabase

ยท57 min read
Learn how to build an AI Writer SaaS with Next.js and Supabase - from writing SEO optimized blog posts to managing subscriptions and billing.
Cover Image for Announcing the Data Loader SDK for Supabase

Announcing the Data Loader SDK for Supabase

ยท8 min read
We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.
Cover Image for Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

Adding AI capabilities to your Next.js SaaS with Supabase and HuggingFace

ยท20 min read
In this tutorial, we will learn how to use add AI capabilities to your SaaS using Supabase Vector, HuggingFace models and Next.js Server Components.
Cover Image for Using Supabase Vault to store secrets

Using Supabase Vault to store secrets

ยท6 min read
Supabase Vault is a Postgres extension that allows you to store secrets in your database. This is a great way to store API keys, tokens, and other sensitive information. In this tutorial, we'll use Supabase Vault to store our API keys
Cover Image for Introduction to Next.js Server Actions

Introduction to Next.js Server Actions

ยท9 min read
Next.js Server Actions are a new feature introduced in Next.js 13 that allows you to run server code without having to create an API endpoint. In this article, we'll learn how to use them.
Cover Image for Next.js 13: complete guide to Server Components and the App Directory

Next.js 13: complete guide to Server Components and the App Directory

ยท19 min read
Unlock the full potential of Next.js 13 with our most complete and definitive tutorial on using server components and the app directory.