DEV Community

Abosi Godwin
Abosi Godwin

Posted on

How I Optimized My Next.js 15 Portfolio for Google Search (and won)

I'm not a marketing person. I'm a developer. So when I decided to take SEO seriously on my portfolio, I approached it the same way I approach everything else — read, implement, measure, iterate.

This is the exact process I followed on my Next.js 15 portfolio to get it ranking on Google. Not theory. The actual implementation, the mistakes I made, and what finally worked.


Why most developer portfolios rank for nothing

Before the technical stuff, let me say the quiet part out loud.

Most portfolio sites are invisible to Google — not because they're badly built, but because they're built only for humans who already have the URL. No metadata. No structured data. Generic <title> tags that say "Portfolio" or the developer's name and nothing else.

Google doesn't know what you do, who you do it for, or where you are. So when a founder searches "Next.js developer Nigeria" or "hire React developer freelance", your portfolio doesn't show up — even if you're exactly the right person for the job.

Here's how I fixed that.


The Stack

  • Framework: Next.js 15 (App Router)
  • Styling: Tailwind CSS v4
  • Animation: Motion v12
  • Deployment: Vercel

Step 1: Treat Every Page as a Unique Document

The most common SEO mistake in Next.js App Router projects is reusing the same metadata object across pages — or worse, only defining it in the root layout.tsx and leaving all other pages to inherit it.

Every page on your site is a separate document as far as Google is concerned. Each one needs its own title and description.

In Next.js 15, this is clean to implement:

// app/page.tsx (homepage)
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Abosi Godwin — Next.js & React Developer in Nigeria",
  description:
    "Full-stack web developer specialising in Next.js, React, Supabase and Tailwind CSS. Based in Asaba, Nigeria. Available for freelance projects.",
};
Enter fullscreen mode Exit fullscreen mode
// app/projects/page.tsx
export const metadata: Metadata = {
  title: "Projects — Abosi Godwin",
  description:
    "A selection of production web apps built with Next.js, Supabase, Firebase, and Tailwind CSS.",
};
Enter fullscreen mode Exit fullscreen mode

Notice two things:

  1. The homepage title includes what I do and where I am. This is keyword-loaded on purpose. Founders searching for a developer in Nigeria will match that.
  2. The sub-page titles follow a consistent Page Name — Your Name format. This is good for brand recognition in search results.

Step 2: Open Graph Metadata (the one most developers skip)

Open Graph tags control how your site looks when shared on Twitter/X, LinkedIn, WhatsApp, and in iMessage previews. They also give Google additional context about your content.

Without OG tags, link previews are ugly and generic. With them, your portfolio looks professional every time someone shares it.

export const metadata: Metadata = {
  title: "Abosi Godwin — Next.js & React Developer in Nigeria",
  description: "Full-stack web developer...",
  openGraph: {
    title: "Abosi Godwin — Next.js & React Developer",
    description: "Building fast, production-ready web apps with Next.js and Supabase.",
    url: "https://abosi.vercel.app",
    siteName: "Abosi Godwin",
    images: [
      {
        url: "https://abosi.vercel.app/og-image.png",
        width: 1200,
        height: 630,
        alt: "Abosi Godwin — Next.js Developer Portfolio",
      },
    ],
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "Abosi Godwin — Next.js & React Developer",
    description: "Building fast, production-ready web apps with Next.js and Supabase.",
    images: ["https://abosi.vercel.app/og-image.png"],
  },
};
Enter fullscreen mode Exit fullscreen mode

The OG image matters. A 1200×630px image with your name, title, and a clean design is worth the 30 minutes it takes to make in Figma. I used a dark background with my name, role, and a subtle grid — nothing fancy, but it looks deliberate.

⚠️ Common mistake: Using a relative path for the OG image URL. It must be an absolute URL (starting with https://). I got burned by this — the preview was broken everywhere until I caught it.


Step 3: JSON-LD Structured Data

This is the one that most developers — even experienced ones — skip entirely. Structured data tells Google exactly what kind of entity your page represents. For a portfolio, you want two schemas: Person and WebSite.

Create a reusable component:

// components/JsonLd.tsx
export function PersonJsonLd() {
  const schema = {
    "@context": "https://schema.org",
    "@type": "Person",
    name: "Abosi Godwin",
    url: "https://abosi.vercel.app",
    jobTitle: "Full-Stack Web Developer",
    worksFor: {
      "@type": "Organization",
      name: "Freelance",
    },
    address: {
      "@type": "PostalAddress",
      addressLocality: "Asaba",
      addressRegion: "Delta State",
      addressCountry: "NG",
    },
    sameAs: [
      "https://github.com/Abosi-Godwin",
      "https://linkedin.com/in/abosigodwin",
    ],
    knowsAbout: [
      "Next.js",
      "React",
      "TypeScript",
      "Supabase",
      "Firebase",
      "Tailwind CSS",
      "Full-Stack Development",
    ],
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Then drop it in your root layout.tsx:

// app/layout.tsx
import { PersonJsonLd } from "@/components/JsonLd";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <PersonJsonLd />
        {children}
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Google's Rich Results Test will confirm it's being read correctly. Once indexed, your name, location, and skills can appear directly in Knowledge Panel results.


Step 4: The Server/Client Component Split for Metadata

This one trips up a lot of Next.js developers. The metadata export only works in Server Components. The moment you add "use client" to a page file, you lose the ability to export metadata from it.

The fix is simple but important: separate your page shell (server) from your interactive UI (client).

// app/projects/page.tsx — SERVER COMPONENT
import type { Metadata } from "next";
import { ProjectsClient } from "./ProjectsClient"; // client component

export const metadata: Metadata = {
  title: "Projects — Abosi Godwin",
  description: "Production web apps built with Next.js, Supabase, and Firebase.",
};

export default function ProjectsPage() {
  return <ProjectsClient />;
}
Enter fullscreen mode Exit fullscreen mode
// app/projects/ProjectsClient.tsx — CLIENT COMPONENT
"use client";

import { motion } from "motion/react";
// all your interactive/animated UI here
Enter fullscreen mode Exit fullscreen mode

This pattern lets you have both: full metadata control on every page and animations, state, and client-side hooks wherever you need them.


Step 5: sitemap.ts and robots.ts

Next.js 15 has built-in support for generating your sitemap and robots file dynamically. This is two files and 30 minutes of work that directly improves how Google crawls your site.

// app/sitemap.ts
import type { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://abosi.vercel.app",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 1,
    },
    {
      url: "https://abosi.vercel.app/projects",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 0.8,
    },
  ];
}
Enter fullscreen mode Exit fullscreen mode
// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
    },
    sitemap: "https://abosi.vercel.app/sitemap.xml",
  };
}
Enter fullscreen mode Exit fullscreen mode

Submit the sitemap URL (https://yoursite.com/sitemap.xml) to Google Search Console. This tells Google exactly which pages exist and how important they are relative to each other.


Step 6: Core Web Vitals — The Score That Actually Matters

Metadata and structured data tell Google what your site is. Core Web Vitals tell Google how good the experience is. Both feed into rankings.

The three metrics:

Metric What it measures Target
LCP How fast the main content loads < 2.5s
CLS How much the layout shifts < 0.1
INP Responsiveness to interaction < 200ms

What I did to improve them:

LCP: Added priority prop to the hero image in Next.js <Image />. This triggers a preload link in the HTML and makes the image load immediately instead of waiting for JavaScript.

<Image
  src="/profile.jpg"
  alt="Abosi Godwin"
  width={400}
  height={400}
  priority // ← this one line shaved 800ms off my LCP
/>
Enter fullscreen mode Exit fullscreen mode

CLS: Made sure every <Image /> had explicit width and height props so the browser reserves space before the image loads. Without this, images load and push content down — Google penalises that.

Fonts: Used next/font with display: swap to prevent invisible text during font load.

import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
});
Enter fullscreen mode Exit fullscreen mode

The Results

After implementing all of this and waiting about 3 weeks for Google to re-crawl:

  • My portfolio now appears on the first page for "Next.js developer Nigeria" and variations
  • Google Search Console shows impressions for "hire React developer Asaba" and "full stack developer Delta State"
  • The OG image shows correctly on every platform — LinkedIn, Twitter, WhatsApp

I'm not ranking for "Next.js developer" globally — I don't need to. I'm ranking for the searches that my actual clients make.


The Full Checklist

If you want to run through your own portfolio right now, here's everything in one place:

  • [ ] Unique title and description metadata on every page
  • [ ] openGraph and twitter metadata with an absolute OG image URL
  • [ ] JSON-LD Person schema on the homepage
  • [ ] Server component shell + client component split for pages that need both metadata and interactivity
  • [ ] sitemap.ts generated and submitted to Google Search Console
  • [ ] robots.ts configured
  • [ ] priority prop on your hero/above-fold image
  • [ ] Explicit width and height on every <Image />
  • [ ] next/font with display: swap
  • [ ] Verified with Google Rich Results Test and PageSpeed Insights

Final Thought

Your portfolio is not just a showcase. It is a landing page for your freelance business. Treat it like one.

Every founder who finds you through Google already has intent. They're not scrolling a feed and half-paying attention — they searched for someone like you. That's the best kind of lead. Make sure your site is the one they land on.


I'm Abosi Godwin — a full-stack developer based in Asaba, Nigeria. I build with Next.js, React, Supabase, and Tailwind CSS. If you found this useful, I write more practical content like this for developers and Nigerian founders. Follow me here on dev.to.

Want to work together? → abosi.vercel.app

Top comments (0)