How I Set Up SEO in My Next.js Portfolio from Zero
Introduction
When I built my portfolio, I didn't want to just slap a title tag on the page and call it SEO. I wanted Google to fully understand who I am, what I do, and why my site deserves to show up when someone searches "frontend developer Ahmedabad" or "hire React developer India."
This blog walks you through exactly how I set up SEO in my Next.js App Router portfolio — with real code from my actual project — and how I verified everything worked using Google Search Console.
Why `metadata` in Next.js Beats a Raw `<meta>` Tag
In traditional HTML, you'd add SEO tags like this:
<head>
<title>Krupesh Vachhani | Frontend Developer</title>
<meta name="description" content="..." />
</head>
This works — but it's manual, error-prone, and doesn't scale. Next.js App Router has a built-in metadata export in layout.tsx that handles everything automatically, including generating <title> and <meta> tags, setting canonical URLs, configuring robots behavior, and handling Open Graph and Twitter cards.
Here's exactly how I set it up in my portfolio's src/app/layout.tsx:
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://krupesh.dev"),
title: "Krupesh Vachhani | Frontend Developer",
description:
"Portfolio of Krupesh Vachhani – Frontend Developer with 1+ years of experience building scalable React.js and Next.js applications.",
keywords: [
"Krupesh Vachhani",
"Frontend Developer",
"React developer",
"Next.js developer",
"hire frontend developer India",
"frontend developer Ahmedabad",
],
authors: [{ name: "Krupesh Vachhani", url: "https://krupesh.dev" }],
creator: "Krupesh Vachhani",
alternates: {
canonical: "https://krupesh.dev",
},
};Let me break down each field.
Breaking Down Every Field
metadataBase
metadataBase: new URL("https://krupesh.dev"),This is the base URL for your site. Next.js uses this to resolve relative paths for images in Open Graph, Twitter cards, etc. Without it, image URLs will be broken in social previews. Always set `metadataBase` first. Everything else depends on it.
title
title: "Krupesh Vachhani | Frontend Developer",
Format: [Your Name] | [Your Role]. Google shows ~60 characters in search results. Keep it under 60. Include your primary keyword (Frontend Developer) naturally.
description
description: "Portfolio of Krupesh Vachhani – Frontend Developer with 1+ years of experience...",Google shows ~155–160 characters as the meta description snippet. Write it like a value proposition — who you are, what you build, what makes you different. Do NOT keyword-stuff this. Google ignores it for ranking but users read it to decide whether to click.
keywords
keywords: ["Frontend Developer", "React developer", "hire frontend developer India"],
Google officially says it ignores the keywords meta tag — but other search engines (Bing, DuckDuckGo) may use it. More importantly, listing them here forces you to think clearly about what terms you want to rank for.
canonical
alternates: {
canonical: "https://krupesh.dev",
},
A canonical URL tells Google: "This is the one true version of this page." It prevents duplicate content issues if your site is accessible at multiple URLs (e.g., www.krupesh.dev and krupesh.dev).
robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
},
},- →
index: true— Allow Google to index this page - →
follow: true— Allow Google to follow links on this page - →
max-image-preview: "large"— Google can show large preview images in search results - →
max-snippet: -1— No limit on how much text Google can show as a snippet
How `next/font` Affects SEO
import { Fira_Code } from "next/font/google";
const firaCode = Fira_Code({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
variable: "--font-fira-code",
display: "swap",
});
display: "swap" tells the browser to show the fallback font immediately, then swap to Fira Code once it loads. Without swap, the browser hides text until the font loads — this causes a blank flash and hurts your LCP (Largest Contentful Paint) score, which is a Core Web Vital that directly affects your Google ranking.
next/font also self-hosts fonts — no external request to fonts.googleapis.com, which removes a DNS lookup from your critical path.
The Favicon Trap
This is the mistake I made — and almost every developer makes it. I added a favicon.ico to my Next.js project, saw it showing correctly in the browser tab, and assumed Google would pick it up. It didn't. For weeks my portfolio showed a generic grey globe icon in search results.
The reason? Google has a strict minimum size rule for favicons in search results. Google will only display your favicon if it is at least 48×48 pixels. A standard favicon.ico contains 16×16 and 32×32 sizes — both below the threshold. Google silently ignores it and falls back to the generic globe icon. This is documented in Google's favicon guidelines but almost nobody reads it until they notice the problem.
The Fix: Add a 192×192 PNG in Next.js App Router
Next.js App Router treats a file named icon.png in src/app/ as your site's primary icon and generates the correct <link rel="icon"> tag automatically.
Step 1: Get your favicon as a 192×192 PNG using Favicon.io, RealFaviconGenerator.net, or Squoosh.
Step 2: Place files in the correct locations:
src/app/
favicon.ico ← browser tab (keep this)
icon.png ← 192×192 PNG — Google Search reads THIS
apple-icon.png ← 180×180 PNG — iOS home screen
public/
android-chrome-192x192.png ← referenced by webmanifest
android-chrome-512x512.png ← referenced by webmanifest
site.webmanifest ← PWA manifest
Step 3: Register the icons and manifest in metadata inside layout.tsx:
export const metadata: Metadata = {
manifest: "/site.webmanifest",
icons: {
icon: [
{ url: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180" }],
},
};
Step 4: Update your site.webmanifest with your real name and brand colors:
{
"name": "Krupesh Vachhani",
"short_name": "Krupesh",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#0a0a0a",
"background_color": "#0a0a0a",
"display": "standalone"
}
Step 5: Delete any static public/robots.txt and switch to src/app/robots.ts (covered in Blog 04).
After deploying, open Google Search Console → URL Inspection → Test Live URL on your homepage. Click "More Info" and confirm icon.png, favicon.ico, and site.webmanifest all return 200 OK. Google re-fetches favicons on its own crawl schedule — expect the correct favicon to appear within 1–2 weeks after deploying the fix.
The Quick Reference
- →
favicon.ico(16×16, 32×32) — Browser tab - →
icon.png(192×192) — Google Search - →
apple-icon.png(180×180) — iOS home screen - →
android-chrome-512x512.png(512×512) — Android / PWA splash
Google Search needs the 192×192 PNG. Everything else is a bonus.
Google Search Console: Verify Your Site
Before Google indexes you, you need to prove you own the site. Go to Google Search Console → Add property → choose "URL prefix" → enter https://krupesh.dev → choose "HTML tag" verification method. You'll get a code like:
<meta name="google-site-verification" content="D6fCS07AgEH0S..." />
In Next.js, add it to your metadata — no raw <meta> tag needed:
verification: {
google: "xxxxxxxxxxxxx",
},
Next.js automatically injects the verification meta tag in the <head>. Click "Verify" in GSC and you're done.
What to Check in GSC After Setup
Once verified, wait 24–48 hours then check:
- 1.Coverage report — Are your pages indexed?
- 2.Performance report — What queries are people using to find you?
- 3.URL Inspection — Paste your homepage URL, see exactly how Google sees it
The URL Inspection tool shows whether the page is indexed, the last crawl date, any indexing errors, and a live preview of how Google renders your page.
Summary Checklist
- →
metadataBaseset to your domain - →
titleunder 60 characters with primary keyword - →
descriptionunder 160 characters, written as a value proposition - →
canonicalURL set - →
robotsconfigured withmax-image-previewandmax-snippet - →
next/fontwithdisplay: "swap"for LCP - →
src/app/icon.pngat 192×192 — Google Search favicon - →
src/app/apple-icon.pngat 180×180 — iOS home screen - →
site.webmanifestinpublic/with correct name and brand colors - →
metadata.iconsandmetadata.manifestregistered inlayout.tsx - →GSC URL Inspection confirms all icon files return
200 OK - →Google verification added via
metadata.verification.google - →Site submitted and verified in Google Search Console