DEV Community

Cover image for A Detailed Guide on How to Build a Website with Next.js and Headless WordPress + SEO - 2023 Web Development
Jeffrey Nwankwo
Jeffrey Nwankwo

Posted on

A Detailed Guide on How to Build a Website with Next.js and Headless WordPress + SEO - 2023 Web Development

If you haven't heard about Next.js and WordPress, don't worry - you can check out the links I've included to learn more about them.

In this article, we'll together be exploring the power of combining Headless WordPress and Next.js, two technologies that have become increasingly popular in recent years. By using these together, we can create a website that is not only incredibly fast, but also SEO-friendly and provides an exceptional user experience. This guide is perfect for both experienced developers and those who are just starting out, as I'll be providing all the necessary knowledge and tools to help you create cutting-edge websites in 2023.

Throughout this post, I'll be working on building a simple blog using Next.js and Headless WordPress. You can check out the finished product at the link provided, and the code can be found in the GitHub repository.

While WordPress is a well-known content management system, not everyone is familiar with Headless WordPress - I'll be covering what it is and why it's important.

What is Headless WordPress?

In traditional WordPress setups, the content management system (CMS) and frontend are closely tied together (tightly coupled). Essentially, WordPress handles both the content management and presentation aspects of user interactions. However, with Headless WordPress, we've got a new way of using the platform that separates the CMS from the frontend presentation layers. This approach allows us developers to use the CMS as a content repository, similar to a backend, and use the provided API to fetch content as needed on the frontend. The best part is that we're no longer limited to using WordPress for the frontend, but can choose any technology we prefer, like React, Vue.js, Angular, or in our case, Next.js.

What about the benefits? Well, they are pretty amazing. As developers, this technology gives us the power to build applications with greater flexibility, improved performance, and better separation of concerns between the CMS and frontend. We can build custom, high-performance front-ends while still leveraging the powerful content management capabilities of WordPress.


In this setup, here are some of the things we will cover:

  • Next.js setup with TypeScript.
  • WordPress setup on Local by Flywheel, which will enable us establish a server for WordPress installation. WordPress installation. You may also utilize XAMPP, WAMP, or any other familiar tool.
  • Downloading and configuring plugins.
  • Connecting our WordPress and building our blog.
  • Deploying our blog.
  • Optimizing blog for SEO. (A live url is necessary).

Are you game? Let's begin!

Next.js setup with TypeScript

Option 1 (Recommended)

If you want to join me or already know about this, you can grab the starter template I made on GitHub. It's got useful stuff like React components and project styling. Just run npm install or yarn install to install the dependencies. Here's the link to the starter template: Grab it here.

Option 2

But if you prefer to go the extra mile and set up Next.js with TypeScript on your own, keep reading.

To begin, create a new folder for this project on your desktop or any location you prefer. I called mine "the-headless-blog". 🧐

The fastest way to start a Next.js project with TypeScript is to use the npx create-next-app command with the --typescript flag. Open your project folder in your favorite IDE and run this command on the terminal:

npx create-next-app@latest --typescript.

You'll be prompted to answer some questions about your project confiuration. To follow along, here are the options I chose:

  1. What is your project named? - . (".", specifying to use my folder name as the project name. You can enter a name of your choice)
  2. Would you like to use ESLint with this project? - Yes
  3. Would you like to use src/ directory with this project? - No
  4. Would you like to use experimental app/ directory with this project? - No
  5. What import alias would you like configured? - @/*

Let's keep it moving.

WordPress Installation and Setup on Local

Feel free to skip this part.

Local by Flywheel is an effortless way to develop WordPress sites locally. It's quite similar to tools like XAMPP and WAMP but it's my go-to for setting up WordPress locally. You can learn more and download here and it's free.

To begin creating a new site, open Local by Flywheel, navigate to the green plus icon located at the bottom left of the window. Clicking on it will prompt you to select an option for creating a new site. Choose the "Create a new site" option and click on continue.

Image description

After this, you will be taken to a new screen where you can enter a name for your site. Simply type in any name you prefer and click on the continue button.

You will then be given the option to select an environment. Generally, the preferred option should suffice, but if you wish to customize your environment, select the "Custom" option.

Image description

The next step is to enter your WordPress credentials, which include a username, password, and email address. Once you have entered these, click on the "Add site" button.

Image description

Allow some time for the site to deploy and once the process is complete, click on the "Start site" button if it hasn't already started.

Image description

Image description

To access the WordPress dashboard, click on the "WP Admin" button and log in using the credentials you created earlier. Congratulations! Your WordPress installation is now complete.

WordPress Dashboard

Downloading and Configuring Plugins

Recall that our plan is to use WordPress as a content respository only and then fetch content through APIs. By default, WordPress offers a basic REST API for retrieving and managing content and data. However, in this project, we will use the WPGraphQL plugin to enhance performance and align with personal preferences.

  • WPGraphQL: This is an open-source and free WordPress plugin that offers a user-friendly GraphQL API for your WordPress website. Rather than utilizing the standard WordPress REST API, we'll be using GraphQL, a query language for APIs that Facebook developed. It allows you to request only the data you require from an API, increasing the speed and efficiency of your applications. With the help of the plugin, we will be able to retrieve data from WordPress using GraphQL queries. Click here to learn more about the GraphQL query language.

To install, navigate to the plugins directory on your WordPress dashboard, and search for "graphql." Proceed to install and activate the WPGraphQL plugin. As of the time of writing, it boasts over 20,000 active installations.

Image description

Let's pause a minute to generate at least three to five dummy posts in WordPress. Each post should include a title, a featured image, and an excerpt. We will retrieve these posts on our frontend at a later stage.

Dummy posts

Connecting our WordPress and building our blog

In the Next.js setup section, I added a link to grab the starter template, here. The starter template includes useful code for the React components and styling that I'll be using, and I've also integrated TailwindCSS to reduce the amount of CSS code you need to write.

I'll be mostly focused on getting our project connected to WordPress, pulling posts from there, and displaying them on the screen. After that, I'll dive into the SEO stuff.

To link WordPress to our project, go to your WordPress dashboard. If you've installed and activated the WPGraphQL plugin, it should now show up on the dashboard with a label that says "GraphQL". Click on it and go to the GraphQL settings page.

GraphQL Settings Page

On the "WPGraphQL General Settings" page in the settings, you'll find the GraphQL Endpoint we need to connect WordPress to our project. This endpoint allows us to communicate with WordPress from the frontend and fetch content as required. Simply copy the endpoint, which should look like a URL beginning with either http or https.

WpGraphQL endpoint

Back in our project folder, create a .env file in the main directory. It's a file used to store environment variables such as API keys, database credentials or the endpoint we just copied.

Once the file is created, add a key for the endpoint and save the file. You can name it NEXT_PUBLIC_WORDPRESS_API_ENDPOINT. Whatever you name it, be sure it starts with "NEXT_PUBLIC_".

.env file

Next, let's create a new folder in the main directory and name it "lib". Inside this folder, create two new files: base.ts and service.ts. Copy and paste the following code into the base.ts file

const API_URL = <string>process.env.NEXT_PUBLIC_WORDPRESS_API_ENDPOINT;

export async function fetchAPI(
  query = "",
  { variables }: Record<string, any> = {}
) {
  const headers = { "Content-Type": "application/json" };

  const res = await fetch(API_URL, {
    headers,
    method: "POST",
    body: JSON.stringify({
      query,
      variables,
    }),
  });

  const json = await res.json();

  if (json.errors) {
    console.error(json.errors);
    throw new Error("Failed to fetch API");
  }
  return json.data;
}
Enter fullscreen mode Exit fullscreen mode

In the base.ts file, we are exporting the fetchAPI function which is used to fetch data from the WordPress endpoint we stored in the .env file. We can pass a query string and a variables object to it to make a request to the endpoint and retrieve the desired data.

Now let's proceed to write the query to fetch all blog posts from WordPress.

In your WordPress dashboard, navigate to the GraphQL IDE. The IDE allows us to experiment with GraphQL queries and see the structure of the data that is returned. This can help you understand how to structure your queries and align the data with the needs of your application. Click on the "Query Composer" button to open the composer. With the composer, you can create GraphQL queries by toggling the relevant data types and their available options. To learn more about making queries in WPGraphQL, you can refer to this resource.

Since we want to fetch posts, here's the query to fetch the first a number of blog posts from WordPress:

query FetchPosts($first: Int = 10) {
  posts(first: $first) {
    nodes {
      excerpt
      featuredImage {
        node {
          sourceUrl
        }
      }
      slug
      title
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The variable first is the number of posts to fetch per request. The default number is 10. When you run the query, this the structure of the response.

Returned posts

Moving along, back in your project folder, copy and paste this code in your service.ts file.

import { fetchAPI } from "./base";

export async function getPosts(first = 10) {
  const data = await fetchAPI(
    `query FetchPosts($first: Int = 10) {
        posts(first: $first) {
          nodes {
            excerpt
            featuredImage {
              node {
                sourceUrl
              }
            }
            slug
            title
          }
        }
      }`,
    {
      variables: {
        first,
      },
    }
  );

  return data?.posts?.nodes;
}
Enter fullscreen mode Exit fullscreen mode

See how we passed the FetchPosts query to the fetchAPI function. When the getPosts function is called, it makes a request and fetches the specified number of posts, returning the data. The service.ts file provides a space for defining additional functions/services to retrieve and manage data in WordPress.

We'll retrieve and display the posts on the home page which is the index.tsx file inside the pages folder. For static site generation, we will use the getStaticProps and getStaticPaths functions to pre-render the page. To learn more about data fetching and pre-rendering in Next.js, refer to this resource.

In the index.tsx file, we need to import the GetStaticProps type from the next module because we're using TypeScript. We also have to import the getPosts service from the lib directory.

import { GetStaticProps } from "next";
import { getPosts } from "@/lib/service";
Enter fullscreen mode Exit fullscreen mode

After that, we need to create a function called getStaticProps just below the HomePage function. Inside this function, we'll call the getPosts service to request data from WordPress. Once we have the posts, we'll return them as a prop which will be passed to the HomePage function (default export).

export const getStaticProps: GetStaticProps = async () => {
  const posts = await getPosts(100); // retrieve first 100 posts

  return {
    props: {
      posts,
    },
    revalidate: 3600,
  };
};
Enter fullscreen mode Exit fullscreen mode

When the getStaticProps function is called, it will return an array of posts, which will be passed to the HomePage function as a prop. We can access this prop inside HomePage.

Each post in the returned array is an object with properties like title, slug, excerpt, and featuredImage, as we can see by examining the structure of each post in the WPGraphQL IDE.

Post structure

To display each post on the HomePage, we'll need to map through the array of posts and create a PostBlock component for each one. The PostBlock component is located in the components folder.

We'll also need to modify the PostBlock component to work with the title, slug, excerpt, and featuredImage properties of each post and display the data appropriately.

Here's the updated index.tsx file:

import { GetStaticProps } from "next";

import { Hero } from "@/components/Hero";
import { PostBlock } from "@/components/PostBlock";
import { getPosts } from "@/lib/service";

export default function HomePage({ posts }: { posts: any }) {
  return (
    <>
      <Hero />
      <div className="container mx-auto py-8">
        <h3 className="text-xl">All my posts (5)</h3>
        <div className="my-6 grid grid-flow-row grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
          {posts.map((post: any) => {
            return <PostBlock key={post.slug} post={post} />;
          })}
        </div>
      </div>
    </>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const posts = await getPosts(100); // retrieve first 100 posts

  return {
    props: {
      posts,
    },
    revalidate: 3600,
  };
};

Enter fullscreen mode Exit fullscreen mode

To make sure that the PostBlock component can display the data for each post correctly, we need to modify it. You can find the PostBlock component inside the components folder.

We'll need to make sure that the PostBlock component can access the title, excerpt, slug, and featuredImage properties of each post that we're passing in. Then, we can use these properties to display the data appropriately.

For example, we might want to display the post's title as a heading, the excerpt as a short summary, and the featured image as a thumbnail. We'll also need to create a link that takes the user to the full post when they click on it.

import Link from "next/link";
import Image from "next/image";

import defaultImage from "@/assets/images/default.jpg";

export const PostBlock = ({ post }: { post: any }) => {
  return (
    <div className="post-block p-2 rounded-md">
      <Link href={`/blog/${post.slug}`}>
        <div className="relative h-80 transition-all duration-200 ease-linear hover:-translate-y-[3px]">
          <Image
            src={post.featuredImage.node.sourceUrl ?? defaultImage}
            fill
            alt={post.title}
            className="absolute rounded-md h-full w-full object-cover"
          />
        </div>
      </Link>
      <Link href={`/blog/${post.slug}`} className="post-content my-4">
        <h3 className="text-2xl py-4">{post.title}</h3>
        <p className="italic">{post.excerpt}</p>
      </Link>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

Make sure to test the PostBlock component after making the modifications to ensure that the data is being displayed correctly.

Once you've made all the necessary changes to your code, you can start your development server by running the npm run dev command in your terminal. Make sure that your local server for WordPress is still running. Next.js will typically start the server on port 3000, so navigate to http://localhost:3000 in your browser to view the site. If everything is working correctly, you should see the homepage of your site but with an error on the screen like so:

Image Error

This error is related to the usage of the next/image component in Next.js. The error message indicates that the src property of the next/image component is pointing to an image file with a URL that includes a hostname the-headless-blog.local which is not allowed by the next/image configuration.

If you want to use images from a source other than your Next.js project, you'll need to configure your next.config.js file properly. This includes creating a list of allowed domains from which images can be loaded. This security measure is in place to prevent cross-site scripting (XSS) attacks.

So to fix the error you're seeing, you can add the the-headless-blog.local hostname to the list of allowed domains in your next.config.js file. This should allow you to load images from your WordPress server. Learn more here.

next.config.js

After making changes to your code, you will need to restart your development server in order to see the changes take effect. To do this, stop the current instance of your development server by pressing Ctrl + C in your terminal.

Then, run the npm run dev command again to start the development server with your updated code. This should ensure that your changes have been properly propagated and are visible in your browser.

Retrieved posts

Congratulations on successfully retrieving posts from your WordPress server! 🎉🎉🎉

However, there is one more modification we need to make to the PostBlock component. The excerpt returned from WordPress is an HTML paragraph, so we need to make some adjustments to ensure that it is displayed properly.

To do this, we can change the p tag in the PostBlock component to a div tag and use the dangerouslySetInnerHTML attribute to display the excerpt as HTML. This will ensure that the excerpt is displayed properly and any HTML tags in the excerpt are rendered correctly.

Here's the updated code for the PostBlock component:

import Link from "next/link";
import Image from "next/image";

import defaultImage from "@/assets/images/default.jpg";

export const PostBlock = ({ post }: { post: any }) => {
  return (
    <div className="post-block p-2 rounded-md">
      <Link href={`/blog/${post.slug}`}>
        <div className="relative h-80 transition-all duration-200 ease-linear hover:-translate-y-[3px]">
          <Image
            src={post.featuredImage.node.sourceUrl ?? defaultImage}
            fill
            alt={post.title}
            className="absolute rounded-md h-full w-full object-cover"
          />
        </div>
      </Link>
      <Link href={`/blog/${post.slug}`} className="post-content my-4">
        <h3 className="text-2xl py-4">{post.title}</h3>
        <div
          className="italic"
          dangerouslySetInnerHTML={{ __html: post.excerpt }}
        ></div>
      </Link>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

After saving the changes, our posts should be rendered correctly.

Posts

To display the correct number of posts on the homepage, we need to modify the HomePage function to access the length property of the posts array.

Posts length
Posts length


Currently, clicking on any post on the homepage will take you to the 404 page. To fix this, we need to establish a dynamic route for the post details page.

To do this, we can create a new folder called posts inside the pages folder. Inside the posts folder, we can create a new file called [slug].tsx. This will be the template used to render the details of each post.

After creating the [slug].tsx file, we can update the link structure in the PostBlock component from "blog/${post.slug}" to "posts/${post.slug}" to match the new dynamic route we just created. This will ensure that clicking on a post on the homepage takes you to the correct post details page.

Updated link structure

Next, to fetch a single post by slug, we can create and export a new service in the service.ts file inside the lib folder. Here's the query we can use to fetch a single post by slug:

query GetPost($id: ID = "") {
  post(id: $id, idType: SLUG) {
    content
    featuredImage {
      node {
        sourceUrl
      }
    }
    slug
    title
  }
}
Enter fullscreen mode Exit fullscreen mode

And here's the service to get a single post by slug:

export async function getPostBySlug(slug: string) {
  const data = await fetchAPI(
    `query GetPost($id: ID = "") {
    post(id: $id, idType: SLUG) {
      content
      featuredImage {
        node {
          sourceUrl
        }
      }
      slug
      title
    }
  }`,
    {
      variables: {
        id: slug,
      },
    }
  );

  return data?.post;
}
Enter fullscreen mode Exit fullscreen mode

We'll call the getPostBySlug function inside the getStaticProps function in the [slug].tsx file. This function will return the post data, which we'll then pass as a prop to the page component for rendering. You can copy and paste the code provided below into the [slug].tsx file, and find the code explanation right after it.

import { GetStaticProps } from "next";
import { GetStaticPaths } from "next";

import { getPosts, getPostBySlug } from "@/lib/service";

export default function PostDetails({ post }: { post: any }) {
  return (
    <section className="container mx-auto py-12">
      <div
        className="post-header relative flex flex-col items-center justify-center w-full min-h-[200px] rounded-md"
        style={{
          backgroundImage: `url(${post.featuredImage.node.sourceUrl})`,
          backgroundSize: "cover",
          backgroundPosition: "center",
        }}
      >
        <div
          className="absolute w-full h-full z-10"
          style={{ backgroundColor: "rgba(0, 0, 0, .5)" }}
        ></div>
        <div className="z-20 text-center">
          <h1 className="text-2xl md:text-4xl mb-4">{post.title}</h1>
          <p className="italic">By Jeffrey</p>
        </div>
      </div>
      <div
        className="post-content w-full md:w-3/5 mx-auto mt-20 py-6 text-lg"
        dangerouslySetInnerHTML={{ __html: post.content }}
      ></div>
    </section>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getPosts(100); // retrieve first 100 posts

  return {
    paths: posts.map((post: any) => `/posts/${post.slug}`),
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const post = await getPostBySlug(params?.slug as string);

  return {
    props: { post },
  };
};

Enter fullscreen mode Exit fullscreen mode

The PostDetails function is the component that will render the post page. It receives a post object as a prop which contains the information about the post to be rendered. The function simply renders a header section for the post which includes the post's featured image, title, and author. We also render the post's content right below it.

In Next.js, if a page has dynamic routes and uses getStaticProps, it needs to define a list of paths to be statically generated. Next.js will generate the necessary pages for each route (each post) at build time. This means that when a user visits one of our posts, they will see a pre-rendered version of the page, rather than waiting for the page to load and render on the client. In this specific case, it retrieves the first 100 posts using the getPosts function from our service.ts file and maps over them to create an array of paths that corresponds to the slug of each post. The fallback property is set to false, which means that if a user navigates to a post that doesn't exist, Next.js will return a 404 page.

The getStaticProps function has been used before in the index.tsx file, but in this case, note that the slug parameter is obtained by destructuring the params object passed to the function. We use slug because it corresponds to the name of our file, which is [slug].tsx, and is enclosed in square brackets. You can learn more about Next.js routing here.

After saving all the changes, clicking on a post in the home page should take you to the corresponding post details page in your browser.

Post details

Deploying our Blog

It's time to share our work with the world! 🎉 To make a website or blog using the WordPress + Next.js combination go live, we need to first set up an online server for the WordPress backend, and then deploy the Next.js frontend on Vercel or on any other static hosting providers.

Important: Usually, you would get a domain name, like example.com, and point it to your Frontend deployed on Vercel. Then, you can create a subdomain, such as api.example.com, for the WordPress backend. This allows you to use your primary domain name for your website, while the subdomain serves as a server for your site's backend.

But in our case, we won't be creating a domain name or subdomain for our project. Instead, we'll use a free hosting service called 000webhost to host our WordPress backend and then deploy our Frontend on Vercel.

Deploying our WordPress backend.

  1. Go to https://www.000webhost.com/ and create a new account. Don't forget to verify your email address, and the best part, it's free!
  2. Login and navigate to https://www.000webhost.com/members/website/list. Click on the "Create New Site" button, and enter a website name and password in the modal form. Finally, click on the "Create" button, and you'll be redirected to the dashboard when the process is completed. Create website
  3. The next step is to install WordPress. From the dashboard, click on the "WordPress" card, and then fill out the form on the screen by entering your desired admin username and password. Once you've filled out the form, click on the "Install" button and wait for the WordPress installation process to complete. Host Dashbaord Set credentials for WordPress installation WordPress Installation
  4. After installation, click on the "Go to configuration page" button, and you'll be prompted to log in. Enter the admin username and password you created in the previous step. If you encounter a 404 page or are redirected to the website lists page, try step 3 again. WordPress Dashboard
  5. On the WordPress dashboard, we need to download and configure plugins, as well as create some posts just like we did earlier in this article. However, if you want to migrate your local WordPress installation to your live one, you can easily use a plugin called All-in-one WP Migration. You can learn more about migration with the plugin here. Since we don't have much content on our local WordPress installation, we can simply repeat the process.

Deploying our Frontend on Vercel

Before proceeding, ensure that you have created a GitHub account, and that you have knowledge of Git. If you don't have knowledge of Git, you can click here to learn more.

Next, open the next.config.js file in your working folder and add the new domain of your live WordPress hostname in the domains array. api-headless-blog.000webhostapp.com
Add the new hostname

  1. Create a new repository for your frontend on your GitHub account. You can name it "nextjs-headless-wordpress."
  2. In your working folder, run the following commands one after the other in your terminal to initialize Git and push your code:

    git init
    git commit -m "first commit"
    git branch -M main
    git remote add origin <your git repo url>
    git push -u origin main
    
  3. After pushing your code to the main branch, go to https://vercel.com and log in using your GitHub account. On the dashboard, add a new project and click on the "Import" button next to the repository you just created.

    New project

  4. On the project configuration page, in the "Environment Variables" section, enter the WordPress API endpoint as you did locally, but use the live WordPress GraphQL endpoint instead. Then click on the "Add" button. NEXT_PUBLIC_WORDPRESS_API_ENDPOINT = <live endpoint>

    Environment Variable

  5. Click on the "Deploy" button to start the deployment process.

If everything went smoothly, your project should now be deployed without any hiccups! If you run into any issues, double-check your steps and try again.

Now, here's the exciting part – you can share your newly created blog with your friends! Here's the link to mine https://nextjs-headless-wordpress-lac.vercel.app/

Optimizing Blog for SEO

SEO

If this article has been helpful so far, give me a follow on Twitter.

In a Twitter's Space, a developer expressed concerns about Next.js and React not being a suitable choice to build a blog due to the difficulty in optimizing for SEO. However, in 2023, optimizing for SEO on your website or blog is quite achievable.

In this section, we'll cover the following:

  • Creating a custom SEO component
  • Generating sitemap
  • Google site ownership verification

Creating a Custom SEO Component

We will implement a custom SEO component that can enhance the search engine optimization (SEO) of our blog by generating meta tags and other HTML markup. This component will accept multiple props, including the page's title, description, image, and type, to create relevant meta tags for both search engine indexing and social media sharing. Additionally, it will incorporate links to different icons, like the favicon, and set the canonical URL to avoid duplication issues. The utilization of this component will ensure that our blog has optimized meta tags, resulting in improved search engine rankings and an enhanced user experience when sharing the site on social media platforms.

Step 1
Add a new environment variable called NEXT_PUBLIC_ROOT_URL in the .env file with the value set to the root URL of our blog. As we are currently in the development phase, the root URL should be http://localhost:3000.

.env file

Step 2

Create favicons for your blog using an online generator like Favicon.io. These icons will be utilized in the upcoming SEO component. It is recommended to generate 5 types of icons for optimal optimization, including:

  • faviconIco: This is the favicon icon for the blog/website, which appears in the browser tab and bookmarks.
  • favicon180: This icon is used for Apple devices when a user adds a website to their home screen.
  • favicon32: This is another favicon icon, but with a size of 32x32 pixels.
  • favicon16: This favicon icon is even smaller, with a size of 16x16 pixels.
  • maskIcon: This is a special icon that is used as the mask for the pinned tab icon in Safari browser.

However, I won't be generating a maskIcon for this project.

After generating the favicons using the online generator, a zip file containing the icons will be downloaded. Alternatively, you can create these icons yourself in any way that you prefer.

In the assets folder, create a new folder for the icons. You can name it "favicons" and copy the downloaded icons inside the folder.

Favicons folder

In the components folder, create a new folder and name it "SEO". Inside the folder, create an index.tsx file which will hold the code for the component. Copy and paste the code below into your file.

import Head from "next/head";
import { useRouter } from "next/router";

import faviconIco from "@/assets/favicons/favicon.ico";
import appleTouchIcon from "@/assets/favicons/apple-touch-icon.png";
import favicon32 from "@/assets/favicons/favicon-32x32.png";
import favicon16 from "@/assets/favicons/favicon-16x16.png";
import defaultImage from "@/assets/images/default.jpg";

interface SEOProps {
  title: string;
  description?: string;
  image?: string;
  type?: "website" | "article";
}

const pageImage = process.env.NEXT_PUBLIC_ROOT_URL + defaultImage.src.slice(1);

const ROOT_URL = process.env.NEXT_PUBLIC_ROOT_URL as string;

export const SEO = ({
  title,
  description,
  image = pageImage,
  type = "website",
}: SEOProps) => {
  const router = useRouter();
  const url = `${ROOT_URL}/${router.asPath}`;

  return (
    <Head>
      <title>{title}</title>
      <meta charSet="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta content="IE=edge" httpEquiv="X-UA-Compatible" />
      <meta name="description" content={description} />
      <meta name="robots" content="follow, index" />

      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:site" content="@JeffreySunny1" />
      <meta name="twitter:image" content={image} />
      <meta name="twitter:title" content={title} />
      <meta name="twitter:description" content={description} />

      <meta property="og:site_name" content="Jeffrey's Blog" />
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:url" content={url} />
      <meta property="og:type" content={type} />
      <meta property="og:image" content={image} />

      <link rel="shortcut icon" href={faviconIco.src} />
      <link rel="apple-touch-icon" sizes="180x180" href={appleTouchIcon.src} />
      <link rel="icon" type="image/png" sizes="32x32" href={favicon32.src} />
      <link rel="icon" type="image/png" sizes="16x16" href={favicon16.src} />
      {/* <link rel="mask-icon" href="" color="#5bbad5" /> Add mask icon */}
      <meta name="msapplication-TileColor" content="#da532c" />
      <meta name="theme-color" content="#ffffff" />

      <link rel="canonical" href={url} />
    </Head>
  );
};
Enter fullscreen mode Exit fullscreen mode

The components generates meta tags and other HTML markup to improve search engine optimization (SEO) for our blog. It takes four props - title, description, image, and type - to generate appropriate meta tags for social media sharing and search engine indexing. I included various meta tags, including those for Twitter and Open Graph, as well as link tags for the different sizes of favicons. The canonical link is also set to ensure that search engines index the correct URL and to avoid duplicates.

Let's modify our pages/index.tsx and posts/[slug].tsx files to use the new SEO component.

pages/index.tsx

export default function HomePage({ posts }: { posts: any }) {
  return (
    <>
      <SEO
        title="Welcome to Jeffrey's Blog"
        description="Access all tech content and beyond"
      />
      <Hero />
      <div className="container mx-auto py-8">
        <h3 className="text-xl">All my posts ({posts.length})</h3>
        <div className="my-6 grid grid-flow-row grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
          {posts.map((post: any) => {
            return <PostBlock key={post.slug} post={post} />;
          })}
        </div>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

And here's the result:

SEO

posts/[slug].tsx

export default function PostDetails({ post }: { post: any }) {
  return (
    <>
      <SEO
        title={`${post.title} - Posts`}
        description={post.excerpt}
        image={post.featuredImage.node.sourceUrl}
        type="article"
      />
      <section className="container mx-auto py-12">
        <div
          className="post-header relative flex flex-col items-center justify-center w-full min-h-[200px] rounded-md"
          style={{
            backgroundImage: `url(${post.featuredImage.node.sourceUrl})`,
            backgroundSize: "cover",
            backgroundPosition: "center",
          }}
        >
          <div
            className="absolute w-full h-full z-10"
            style={{ backgroundColor: "rgba(0, 0, 0, .5)" }}
          ></div>
          <div className="z-20 text-center">
            <h1 className="text-2xl md:text-4xl mb-4">{post.title}</h1>
            <p className="italic">By Jeffrey</p>
          </div>
        </div>
        <div
          className="post-content w-full md:w-3/5 mx-auto mt-20 py-6 text-lg"
          dangerouslySetInnerHTML={{ __html: post.content }}
        ></div>
      </section>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

On the post details page, the post's title, excerpt, and featured image are passed to the SEO component. The page's type is set to "article" to indicate the type of content it represents. If you inspect the head element using developer tools, you will see all the SEO tags that we included, which are necessary for optimizing the page's search engine ranking.

Developer tools

Generating Sitemap

Why are we generating a sitemap?

Sitemaps help search engines like Google to crawl and index a website more effectively, making it easier for users to discover the content on the site and they can be submitted to search engines as part of the website's search engine optimization (SEO) strategy.

To generate a sitemap for our blog,

To install the sitemap generator package for Next.js, run npm install next-sitemap. Then create a basic config file named next-sitemap.config.js in your root directory and add the following code into it:

/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_ROOT_URL,
  generateRobotsTxt: true
};
Enter fullscreen mode Exit fullscreen mode

Ensure you use the correct environment variable that points to your root url.

Make sure to use the right root url environment variable if yours is different. Then, add a "postbuild" script with the value "next-sitemap" in package.json. This script will generate a sitemap for our blog right after the "build" script is run.
Package.json postbuild

Run the npm run build command on your terminal and after which start your development server. A sitemap for your blog should have been generated and you can access it on this link: http://localhost:3000/sitemap-0.xml
Generated sitemap
Generated Sitemap

Google Site Ownership Verification

A key part of search engine optimization (SEO) is verifying ownership of your domain/website and there are several benefits of verfying your domain or website or in this case, our blog with Google including:

  • Indexing of your website: Google uses your site ownership verification as a way to confirm that you are the owner of a website, which can help to ensure that your website is properly indexed in Google search results.
  • Security: It helps to prevent unauthorized access to your website and to ensure that you have control over your website's presence on Google search.
  • SEO: By accessing Google Search Console, you can monitor your website's or blog's performance on Google search results and identify opportunities to improve your search engine optimization (SEO) efforts.

To verify ownership of your blog on Google, follow these steps:

  1. Sign into your Google account and access Google Search Console.
  2. Click "Add property" and enter your blog's homepage URL, which should be the live link of your blog (e.g. https://nextjs-headless-wordpress-lac.vercel.app/). Select the appropriate property type when prompted. Google Search Console
  3. If you have a custom domain, choose the "Domain" tab and enter your domain name. Otherwise, choose the "URL prefix" tab and click "Continue".
  4. Select the "HTML tag" verification method and copy the provided meta tag. Do not close the modal or click anything.
  5. Open the SEO component file in the _components_folder and paste the meta tag code within the Head tag, like this:

Site ownership verification

Important: You need to add the NEXT_PUBLIC_ROOT_URL environment variable to the project settings of your project on Vercel. This variable should be set to the live URL of your blog, such as https://nextjs-headless-wordpress-lac.vercel.app. Don't forget to hit the "Save" button.
Updated environment variable

Pushing our updated code to GitHub allows us to redeploy our blog. The following commands should be executed sequentially:

git add .
git commit -m "sitemap generation and adjustment"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Our Vercel setup is configured to monitor changes made on the main branch and will automatically redeploy the blog once an update is detected.

After successfully deploying, go back to the Google Search Console page and click on the "Verify" button. If the meta tag has been added correctly, a message reading "Ownership verified" should appear on the screen.
Image description

In addition, you need to add the sitemap to Google Search Console. On the sidebar, click on the "Sitemaps" link. In the "Add a new sitemap" box, enter "sitemap.xml," which is the location of our sitemap (https://nextjs-headless-wordpress-lac.vercel.app/sitemap.xml), and click "Submit."

Congratulations! Your blog is now SEO optimized and ready to be crawled by search engines though it may take some time for the sitemap to fully propagate.


Summary

Thank you for staying with me until this point folks 😎. I want to emphasize that this comprehensive guide is not limited to just blogs. It can also be used to develop robust web applications such as e-commerce sites, real estate listings, online directories, and more. By utilizing plugins such as Advanced Custom Fields and WooCommerce, you can create custom fields and layouts tailored to your application's needs. With authentication and authorization features, WordPress can serve as your backend while Next.js handles your frontend, resulting in a highly responsive and cost-effective website.

Thank you for staying with me. If you found this guide helpful, please consider following me on Twitter.

Feel free to leave your comments, thoughts, and questions in the comments section below. I would be delighted to respond to them!

Image sources: https://freepik.com, https://canva.com

Top comments (10)

Collapse
 
beatrice_wambui profile image
Beatrice Wambui Mbugua

This particular post has made me sign up on Dev.to so that I can comment and say thank you very much for how detailed it was.

Although Next has fully embraced the app router, this was a life saver. Thank you.

Collapse
 
jeffsalive profile image
Jeffrey Nwankwo

You’re welcome. I’m glad it helped

Collapse
 
mezieb profile image
Okoro chimezie bright

Nice work thanks for throwing light in this direction💪

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Very nice article, thank you.

Collapse
 
sip profile image
Dom Sipowicz

@jeffsalive, do you have a plug and play template version that you could submit here as a Vercel community template?
vercel.com/templates?cms=wordpress

Collapse
 
jeffsalive profile image
Jeffrey Nwankwo

Hi Dom, not at the moment but I'd be glad to spin one up.

Collapse
 
rloodev profile image
rloodev

nice artisan

Collapse
 
alaswad profile image
ASWAD

Really amazing

Collapse
 
akinladesolomon profile image
Solex

Great post. Is there a reason why I cannot get featuredImage from the qraphql query?

Collapse
 
mitsuru17 profile image
Eddye Ríos

Any plan to update it with APP folder?
And why do you not use ApolloClient? fetch API is enougth? Thanks in advance :)