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.",
};
// 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.",
};
Notice two things:
- 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.
- The sub-page titles follow a consistent
Page Name — Your Nameformat. 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"],
},
};
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) }}
/>
);
}
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>
);
}
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 />;
}
// app/projects/ProjectsClient.tsx — CLIENT COMPONENT
"use client";
import { motion } from "motion/react";
// all your interactive/animated UI here
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,
},
];
}
// 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",
};
}
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
/>
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",
});
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
titleanddescriptionmetadata on every page - [ ]
openGraphandtwittermetadata with an absolute OG image URL - [ ] JSON-LD
Personschema on the homepage - [ ] Server component shell + client component split for pages that need both metadata and interactivity
- [ ]
sitemap.tsgenerated and submitted to Google Search Console - [ ]
robots.tsconfigured - [ ]
priorityprop on your hero/above-fold image - [ ] Explicit
widthandheighton every<Image /> - [ ]
next/fontwithdisplay: 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)