Build a Blog with Next.js #5: SEO and Deployment (Wrap-up)
Last time we finished the comments feature. Our blog now has all the core capabilities — writing/viewing/categorizing/searching/commenting. In this final post, we wrap up with search engine optimization, sitemap/RSS, and deploying to Vercel, then look back across this series and all the React content.
Metadata API — info for search engines #
Each page needs an appropriate <title>, <meta description>, OpenGraph, etc., for search engines and social platforms to display posts well. Next.js’s Metadata API handles this elegantly.
Static metadata #
The metadata in src/app/layout.js is the site-wide default.
export const metadata = {
metadataBase: new URL('https://your-blog.example.com'),
title: {
default: 'My Blog',
template: '%s | My Blog',
},
description: 'Posts about React and web development',
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://your-blog.example.com',
siteName: 'My Blog',
},
};title.template is automatically applied when child pages set their title. If a child sets 'Hello', the final title becomes Hello | My Blog.
metadataBase is the base domain for absolute URLs (like OG images). Replace it with your real deploy domain.
Dynamic metadata — post detail page #
Each post page’s metadata is generated dynamically from the post’s frontmatter. Export a generateMetadata function.
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return {};
return {
title: post.frontmatter.title,
description: post.frontmatter.description,
openGraph: {
title: post.frontmatter.title,
description: post.frontmatter.description,
type: 'article',
publishedTime: post.frontmatter.date,
tags: post.frontmatter.tags,
},
};
}Now each post page has its own title/description, reflected directly in search results and social previews.
Tag pages too #
export async function generateMetadata({ params }) {
const { tag } = await params;
const decodedTag = decodeURIComponent(tag);
return {
title: `#${decodedTag}`,
description: `Posts tagged with ${decodedTag}`,
};
}The list page just inherits the layout’s default metadata — no extra config needed.
Sitemap — telling search engines about your pages #
To make sure crawlers find every page, provide a sitemap.xml. The App Router can auto-generate one from a sitemap.js file.
src/app/sitemap.js:
import { getAllPosts, getAllTags } from './lib/posts';
const SITE_URL = 'https://your-blog.example.com';
export default function sitemap() {
const posts = getAllPosts();
const tags = getAllTags();
const staticPages = ['', '/tags', '/search'].map(path => ({
url: `${SITE_URL}${path}`,
lastModified: new Date(),
}));
const postPages = posts.map(post => ({
url: `${SITE_URL}/posts/${post.slug}`,
lastModified: new Date(post.frontmatter.date),
}));
const tagPages = tags.map(({ tag }) => ({
url: `${SITE_URL}/tags/${encodeURIComponent(tag)}`,
lastModified: new Date(),
}));
return [...staticPages, ...postPages, ...tagPages];
}With this file in place, /sitemap.xml is served automatically. Next.js converts the function’s return value to XML. Build and inspect the result — it follows the standard sitemap format.
Robots.txt #
Tells search engine crawlers what they may crawl.
src/app/robots.js:
const SITE_URL = 'https://your-blog.example.com';
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}It surfaces automatically at /robots.txt. The standard setup: allow all crawlers everywhere and point them to the sitemap.
RSS feed #
Let’s also build an RSS feed for subscribers — so RSS readers can pick up our posts.
The App Router doesn’t have a built-in convention for RSS, so we build it as a route handler.
src/app/feed.xml/route.js:
import { getAllPosts } from '../lib/posts';
const SITE_URL = 'https://your-blog.example.com';
const SITE_TITLE = 'My Blog';
const SITE_DESCRIPTION = 'Posts about React and web development';
function escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
export async function GET() {
const posts = getAllPosts().slice(0, 50);
const buildDate = new Date().toUTCString();
const items = posts.map(post => `
<item>
<title>${escapeXml(post.frontmatter.title)}</title>
<link>${SITE_URL}/posts/${post.slug}</link>
<guid isPermaLink="true">${SITE_URL}/posts/${post.slug}</guid>
<pubDate>${new Date(post.frontmatter.date).toUTCString()}</pubDate>
<description>${escapeXml(post.frontmatter.description ?? '')}</description>
</item>
`).join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escapeXml(SITE_TITLE)}</title>
<link>${SITE_URL}</link>
<description>${escapeXml(SITE_DESCRIPTION)}</description>
<language>en</language>
<lastBuildDate>${buildDate}</lastBuildDate>
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>
`;
return new Response(xml, {
headers: {
'Content-Type': 'application/rss+xml; charset=utf-8',
},
});
}Hitting /feed.xml returns the RSS XML.
A route.js file is special: it defines an API endpoint instead of a page. This site’s app/feed.xml/route.ts provides RSS in almost the same pattern.
Add the auto-discovery link in the layout’s <head> so RSS readers find it:
export const metadata = {
// ...existing config...
alternates: {
types: {
'application/rss+xml': '/feed.xml',
},
},
};Build and verify #
Run a production build.
npm run buildThe build log shows how each page is generated (static/dynamic), page sizes, and which routes were created. You’ll see /sitemap.xml, /robots.txt, and /feed.xml listed.
To run the build locally:
npm starthttp://localhost:3000 runs in production mode. It feels much faster and lighter than dev mode — every static page is pre-generated and JavaScript is minimized.
Deploying to Vercel #
Time to put it on the internet. Vercel is the official Next.js host, so deployment is the smoothest.
1. Push the code to GitHub #
cd my-blog
git init
git add .
git commit -m "Initial blog"Create an empty repo on GitHub and push.
git remote add origin https://github.com/<you>/my-blog.git
git branch -M main
git push -u origin main2. Sign up for Vercel and link the repo #
Sign in to https://vercel.com with GitHub, click “New Project,” and pick the repo you just pushed.
Vercel auto-detects a Next.js project and applies the right build settings. The first deploy starts without extra configuration.
When the deploy finishes, you get a your-blog.vercel.app domain where your blog runs. The in-memory comment store still has its limitation (comments live only while the instance is alive), but post rendering works perfectly.
3. Connect a custom domain (optional) #
If you have your own domain, connect it from the Vercel project’s “Domains” tab. The DNS guide walks you through it step by step.
4. Workflow for adding posts #
The new post workflow becomes:
- Write
posts/new-article.mdx git commit -m "feat(content): publish ..."git push- Vercel auto-detects → auto-builds → auto-deploys
A single git push puts a new post on the internet — one of the big draws of file-based blogging.
Per-environment config — dev vs production #
We’ve been hardcoding the site URL as your-blog.example.com, but it’s better to set it dynamically per environment.
.env.local:
NEXT_PUBLIC_SITE_URL=http://localhost:3000In Vercel project settings under Environment Variables:
NEXT_PUBLIC_SITE_URL=https://your-actual-domain.comUse it in code:
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000';This split lets each environment (dev/preview/production) get the right URL.
Series retrospective — Build a Blog with Next.js #
Across this series, we built a blog from an empty project to a level you could actually run.
| # | Added feature | Key patterns/tools |
|---|---|---|
| 1 | Data model, setup | MDX setup, frontmatter, folder structure |
| 2 | Post list + detail | Server Component reading fs, compileMDX, generateStaticParams |
| 3 | Tags + search | Dynamic folder vs searchParams, decodeURIComponent |
| 4 | Comments | Server Actions, useActionState, useFormStatus, bind, revalidatePath |
| 5 | SEO + deployment | Metadata API, sitemap, robots, RSS, Vercel |
Although it’s the same 5-post length as the Todo series, the tone was different. The Todo series focused on client-side patterns; the blog centered on the server side (RSC + Actions). Following both gives you practical experience on both the client and server sides.
Retrospective across all 31 React posts #
Now let’s pull together the full picture of the React category as we close this series.
A. React Basics (#1–#15) — client-side fundamentals #
JSX, components, props, state, useEffect, custom hooks, performance optimization, routing. The base everything else stacks on.
B. Build a Todo App with React (#1–#5) — first practical build #
Combining the basics step by step into a small interactive app. A natural evolution: add / toggle / filter / edit / persistence.
C. Modern React + Next.js (#1–#6) — server/client paradigm #
Server Components, Server Actions, Suspense, streaming. The mental shift to “where does this code run?”.
D. Build a Blog with Next.js (#1–#5) — second practical build (this series) #
Brings together everything from A–C into a full-stack blog. A production-grade result + actually deployed.
31 posts in total. Working through all of them, you’ve encountered nearly every core piece of the React ecosystem at least once.
Where to go from here #
You now have a solid base for starting your own projects. Possible directions:
Immediate things to try #
- Make this blog yours — write and run it. The fastest learning is real use
- TypeScript migration —
.js→.tsx. Bigger codebase, more safety - Adopt testing — unit tests for core behavior with Vitest + React Testing Library
Bigger areas #
- DB integration — Prisma or Drizzle, Supabase or Vercel Postgres
- Authentication — NextAuth.js / Clerk / Lucia
- State management library — Zustand, Jotai, Redux Toolkit for larger apps
- Data fetching library — TanStack Query when client-side fetching is needed
Another build #
- E-commerce — cart, payments, inventory, orders — the most comprehensive challenge
- Social app — board, DMs, notifications — real-time and data consistency
- Dashboard/admin — charts, tables, filters — data visualization
Wrap-up #
Thanks so much for following along. With 31 posts on top of the existing content, this site now covers a great deal. Starting from a single small component — and never touching class components (modern React standardizes function components and hooks) — we worked through every client-side fundamental, then the modern RSC paradigm, and finally two kinds of practical apps.
If there’s one thing to remember — “build a small thing you actually want to build.” Take this blog and run it as your own, or try implementing a daily-life tool with React. Whatever you build, real learning happens at the points where you get stuck.
React evolves fast, but the fundamentals covered in this series (component-based thinking, unidirectional data flow, declarative UI, “where does the code run”) don’t change. New tools just keep stacking on top. Knowing the essence lets you pick up any new tool quickly.
This post wraps up the series.