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.
Prerequisites
Before we dive in, ensure you have the following set up:
- WordPress installation with:
- Rank Math SEO plugin
- Advanced Custom Fields Pro
- WP REST API
- Next.js project initialized
- Basic understanding of TypeScript and GraphQL
Let’s start by installing the necessary dependencies in your Next.js project:
npm install @apollo/client graphql next-seo
WordPress Configuration
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
Main Implementation
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;
SEO Configuration in WordPress
In your WordPress admin panel, configure Rank Math SEO for your homepage:
- Go to Rank Math > Titles & Meta > Homepage
- Set your homepage title: “Eugene Skomorokhov – Full Stack Developer”
- Set meta description: “I’m a passionate full-stack developer specializing in creating robust, scalable web applications with expertise in React, Next.js, Node.js, and modern database technologies.”
- Enable Schema Markup and select “Person”
- Fill in your social media profiles
For ACF fields, create the following structure:
Hero Section
- Title (Text)
- Description (Textarea)
- CTA Buttons (Group)
- Primary Button (Group)
- Text (Text)
- Link (Text)
- Secondary Button (Group)
- Text (Text)
- Link (Text)
- Primary Button (Group)
Skills Section
- Technical Skills (Repeater)
- Name (Text)
- Percentage (Number)
Performance Optimizations
Image Optimization:
- Use Next.js Image component for automatic optimization
- Implement lazy loading for images below the fold
- Use the
priorityprop for above-the-fold images
Caching Strategy:
- Implement ISR with a reasonable revalidation period
- Cache GraphQL queries on the client side
- Use proper cache control headers
Loading States:
- Implement loading skeletons for dynamic content
- Use Suspense boundaries for component-level loading states
Monitoring and Maintenance
Set up monitoring:
- Google Search Console for SEO performance
- Google Analytics for user behavior
- Core Web Vitals monitoring
Regular maintenance:
- Update content through WordPress
- Monitor SEO performance
- Keep dependencies updated
- Regular security updates
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.

Leave a Reply