Getting Your Site on Google — Next.js SEO Setup Guide
I Built a Site, but Google Can't Find It
You build a blog, write posts, and then search for it on Google — nothing comes up.
That's completely normal. Creating a site doesn't mean Google will find it on its own. For your site to appear in search results, Google's crawler needs to discover it, be able to read it, and understand what it's about.
Setting this up is called SEO (Search Engine Optimization). It sounds complicated, but the core is just three things:
- Open the door for crawlers (robots.txt)
- Submit a site map (sitemap.xml)
- Structure the information on each page (meta tags, Open Graph, JSON-LD)
Here's how I set up all three on a Next.js App Router blog.
Prerequisites
- Next.js 13+ (App Router)
- A deployed site (domain required)
- Google Search Console account
Step 1. robots.txt — Open the Door for Crawlers
robots.txt tells crawlers "here's what you're allowed to look at on this site." Without it, crawlers can get confused even if they do visit.
In Next.js App Router, this is just one file.
Create app/robots.ts:
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://yourdomain.com/sitemap.xml",
};
}
After building, /robots.txt is automatically generated. Replace yourdomain.com with your actual domain.
Step 2. sitemap.xml — Submit a Map of Your Site to Google
A sitemap tells Google "here are all the pages on my site." Without one, Google has to crawl and find pages on its own, which means some pages may get missed.
Create app/sitemap.ts:
import type { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";
const BASE_URL = "https://yourdomain.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE_URL}/posts/${post.slug}`,
lastModified: post.date ? new Date(post.date) : new Date(),
changeFrequency: "monthly",
priority: 0.8,
}));
const staticEntries: MetadataRoute.Sitemap = [
{ url: BASE_URL, changeFrequency: "daily", priority: 1.0 },
{ url: `${BASE_URL}/posts`, changeFrequency: "daily", priority: 0.9 },
{ url: `${BASE_URL}/about`, changeFrequency: "monthly", priority: 0.5 },
];
return [...staticEntries, ...postEntries];
}
After building, check /sitemap.xml to verify.
Multi-language sites:
If you're using an i18n library likenext-intl, include both/ko/posts/slugand/en/posts/slug. Exclude fallback pages (pages showing content in the wrong language) from the sitemap — they can be treated as duplicate content.
Step 3. Open Graph — Look Good When Shared
When you paste a link into KakaoTalk, Slack, or Twitter, you see a preview with a title and description. That's from Open Graph tags. Without them, it's just a bare URL.
app/[locale]/layout.tsx (site-wide):
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
default: "My Blog Name",
template: `%s | My Blog Name`, // post pages show "Post Title | Blog Name"
},
description: "Blog description",
metadataBase: new URL("https://yourdomain.com"),
openGraph: {
type: "website",
siteName: "My Blog Name",
title: "My Blog Name",
description: "Blog description",
},
twitter: {
card: "summary_large_image",
},
};
}
app/[locale]/posts/[slug]/page.tsx (individual post pages):
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug, locale } = await params;
const post = await getPost(slug, locale);
if (!post) return {};
return {
title: post.title,
description: post.description,
alternates: {
canonical: `https://yourdomain.com/${locale}/posts/${slug}`,
},
openGraph: {
type: "article", // explicitly identifies this as a blog post
title: post.title,
description: post.description,
publishedTime: post.date ? new Date(post.date).toISOString() : undefined,
tags: post.tags,
},
};
}
canonical tells Google "the official URL for this page is here." It prevents duplicate content penalties when multi-language sites have the same content at multiple URLs.
Step 4. JSON-LD — Help Google Recognize Blog Posts
JSON-LD is structured data hidden inside the page. Google reads it to understand "this is a blog post." When done well, it can produce rich results in search (showing publish date, tags, etc.).
Add this inside the post page component:
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.date ? new Date(post.date).toISOString() : undefined,
url: `https://yourdomain.com/posts/${slug}`,
author: {
"@type": "Person",
name: "Author Name",
url: "https://yourdomain.com",
},
keywords: post.tags?.join(", "),
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* rest of the post content */}
</article>
);
Step 5. Register with Google Search Console and Submit Your Sitemap
Now it's time to tell Google directly.
1. Access Search Console and add a property
search.google.com/search-console → Add property → URL prefix → enter your domain
2. Verify ownership
Choose the HTML tag method → you'll get a code like:
<meta name="google-site-verification" content="value-goes-here" />
In Next.js, add one line to generateMetadata:
verification: {
google: "value-goes-here",
},
Deploy, then click Verify in Search Console.
3. Submit your sitemap
Left menu → Sitemaps → enter sitemap.xml → Submit
Troubleshooting
Blank page when visiting sitemap.xml
- Confirm the build completed
- Check if
getAllPosts()or similar data fetch functions are throwing errors - If there are environment variables that only work in production, verify Vercel env var settings
"URL could not be fetched" error in Search Console
- Check that
robots.txtisn't blocking that URL - Make sure the site is actually deployed
- If you just deployed on Vercel, wait 1–2 minutes and retry
How to verify meta tags are applied
View page source in the browser (Cmd+U) and search for <meta property="og:title", or enter your URL at opengraph.xyz to see a live preview.
Summary — The Core Flow
1. app/robots.ts → allow crawlers + point to sitemap
2. app/sitemap.ts → generate a list of all page URLs
3. layout.tsx → site-wide OG / Twitter meta tags
4. posts/[slug]/page → per-post canonical + OG article + JSON-LD
5. Search Console → verify ownership → submit sitemap
Once this is set up, the sitemap updates automatically every time you publish a post. Appearing in search results typically takes 1–2 weeks after submission. The best SEO strategy while you wait: keep publishing consistently.
backtodev
A 40-something PM returns to code. Learning, failing, and growing.