Optimizing Next.js Portfolio with WordPress: Implementing Rank Math SEO and ACF
February 7, 2025

Optimizing Next.js Portfolio with WordPress: Implementing Rank Math SEO and ACF

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:

  1. Go to Rank Math > Titles & Meta > Homepage
  2. Set your homepage title: “Eugene Skomorokhov – Full Stack Developer”
  3. 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.”
  4. Enable Schema Markup and select “Person”
  5. 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)

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 priority prop 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.