As a full-stack developer specializing in Next.js and WordPress integrations, I’ve recently implemented a robust SEO strategy for my portfolio website using Rank Math SEO and Advanced Custom Fields (ACF). In this article, I’ll share my approach to creating a high-performing, SEO-optimized Next.js website that leverages WordPress as a headless CMS.
Before we dive in, ensure you have the following set up:
Let’s start by installing the necessary dependencies in your Next.js project:
npm install @apollo/client graphql next-seo
First, we’ll set up our WordPress backend. Create a new file called lib/apollo-client.ts
:
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: `${process.env.WORDPRESS_API_URL}/graphql`,
cache: new InMemoryCache(),
});
export default client;
Next, create your environment variables in .env.local
:
WORDPRESS_API_URL=https://your-wordpress-site.com
NEXT_PUBLIC_SITE_URL=https://your-nextjs-site.com
Let’s create our homepage with SEO implementation. Here’s the complete code for pages/index.tsx
:
import { GetStaticProps } from 'next';
import { gql } from '@apollo/client';
import Head from 'next/head';
import client from '../lib/apollo-client';
import Image from 'next/image';
import { Suspense } from 'react';
// Types
interface HomePageProps {
seoData: {
title: string;
description: string;
robots: {
index: boolean;
follow: boolean;
};
};
pageData: {
hero: {
title: string;
description: string;
ctaButtons: {
primary: {
text: string;
link: string;
};
secondary: {
text: string;
link: string;
};
};
};
skills: Array<{
name: string;
percentage: number;
}>;
about: {
content: string;
image: {
url: string;
alt: string;
};
};
};
}
// GraphQL query
const GET_HOME_PAGE_DATA = gql`
query GetHomePageData {
pageBy(uri: "/") {
seo {
title
description
robots
schema {
raw
}
opengraphTitle
opengraphDescription
opengraphImage {
sourceUrl
}
}
acf {
hero_section {
title
description
cta_buttons {
primary {
text
link
}
secondary {
text
link
}
}
}
skills {
technical_skills {
name
percentage
}
}
about {
content
image {
url
alt
}
}
}
}
}
`;
// Skills component
const SkillBar = ({ name, percentage }: { name: string; percentage: number }) => (
<div className="mb-4">
<div className="flex justify-between mb-1">
<span className="text-base font-medium">{name}</span>
<span className="text-sm font-medium">{percentage}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
// Loading skeleton
const SkillsLoadingSkeleton = () => (
<div className="animate-pulse">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="mb-4">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-2.5 bg-gray-200 rounded-full"></div>
</div>
))}
</div>
);
export const getStaticProps: GetStaticProps = async () => {
try {
const { data } = await client.query({
query: GET_HOME_PAGE_DATA,
});
return {
props: {
seoData: data.pageBy.seo,
pageData: {
hero: data.pageBy.acf.hero_section,
skills: data.pageBy.acf.skills.technical_skills,
about: data.pageBy.acf.about,
},
},
revalidate: 3600, // Revalidate every hour
};
} catch (error) {
console.error('Error fetching data:', error);
return {
notFound: true,
};
}
};
export default function HomePage({ seoData, pageData }: HomePageProps) {
return (
<>
<Head>
<title>{seoData.title}</title>
<meta name="description" content={seoData.description} />
<meta
name="robots"
content={`${seoData.robots.index ? 'index' : 'noindex'},${seoData.robots.follow ? 'follow' : 'nofollow'}`}
/>
{/* Open Graph tags */}
<meta property="og:title" content={seoData.title} />
<meta property="og:description" content={seoData.description} />
<meta property="og:type" content="website" />
<meta property="og:image" content="/og-image.jpg" />
{/* Schema markup */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Person",
"name": "Eugene Skomorokhov",
"url": "https://eskomorokhov.com",
"jobTitle": "Full Stack Developer",
"knowsAbout": ["React", "Next.js", "Node.js", "MongoDB", "PostgreSQL"],
"sameAs": [
"https://github.com/yourprofile",
"https://linkedin.com/in/yourprofile",
"https://t.me/yourprofile"
]
})
}}
/>
</Head>
<main className="container mx-auto px-4">
{/* Hero Section */}
<section className="py-20">
<h1 className="text-4xl font-bold mb-4">{pageData.hero.title}</h1>
<p className="text-xl mb-8">{pageData.hero.description}</p>
<div className="flex gap-4">
<a
href={pageData.hero.ctaButtons.primary.link}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
{pageData.hero.ctaButtons.primary.text}
</a>
<a
href={pageData.hero.ctaButtons.secondary.link}
className="border border-blue-600 text-blue-600 px-6 py-2 rounded-lg hover:bg-blue-50 transition-colors"
>
{pageData.hero.ctaButtons.secondary.text}
</a>
</div>
</section>
{/* Skills Section */}
<section className="py-16">
<h2 className="text-3xl font-bold mb-8">My Skills</h2>
<div className="max-w-2xl">
<Suspense fallback={<SkillsLoadingSkeleton />}>
{pageData.skills.map((skill) => (
<SkillBar
key={skill.name}
name={skill.name}
percentage={skill.percentage}
/>
))}
</Suspense>
</div>
</section>
</main>
</>
);
}
Now, let’s implement dynamic sitemap generation. Create pages/sitemap.xml.ts
:
import { GetServerSideProps } from 'next';
import { gql } from '@apollo/client';
import client from '../lib/apollo-client';
const Sitemap = () => null;
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
try {
const { data } = await client.query({
query: gql`
query GetAllPages {
pages {
nodes {
uri
modified
}
}
}
`,
});
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL;
const pages = data.pages.nodes;
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages
.map(
(page: { uri: string; modified: string }) => `
<url>
<loc>${baseUrl}${page.uri}</loc>
<lastmod>${new Date(page.modified).toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>${page.uri === '/' ? '1.0' : '0.8'}</priority>
</url>
`
)
.join('')}
</urlset>`;
res.setHeader('Content-Type', 'text/xml');
res.write(sitemap);
res.end();
return {
props: {},
};
} catch (error) {
console.error('Error generating sitemap:', error);
res.status(500).end();
return { props: {} };
}
};
export default Sitemap;
Finally, create pages/robots.txt.ts
:
import { GetServerSideProps } from 'next';
const RobotsTxt = () => null;
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
const robotsTxt = `
User-agent: *
Allow: /
# Sitemap
Sitemap: ${process.env.NEXT_PUBLIC_SITE_URL}/sitemap.xml
# Host
Host: ${process.env.NEXT_PUBLIC_SITE_URL}
`.trim();
res.setHeader('Content-Type', 'text/plain');
res.write(robotsTxt);
res.end();
return {
props: {},
};
};
export default RobotsTxt;
In your WordPress admin panel, configure Rank Math SEO for your homepage:
For ACF fields, create the following structure:
Hero Section
Skills Section
Image Optimization:
priority
prop for above-the-fold imagesCaching Strategy:
Loading States:
Set up monitoring:
Regular maintenance:
This implementation provides a solid foundation for a high-performing, SEO-optimized Next.js portfolio website with WordPress as a headless CMS. The combination of Rank Math SEO and ACF allows for flexible content management while maintaining optimal SEO practices.
Remember to regularly check your SEO performance and make adjustments based on the data from Google Search Console and other monitoring tools. This setup allows you to easily update content through WordPress while maintaining the performance benefits of Next.js.