Search Engine Optimization (SEO) ensures our blog app—where users filter posts by date or tags and admins manage content—reaches its audience via Google or Bing. A well-optimized post, like “Next.js Tips for Beginners,” should appear at the top of search results, driving organic traffic. Without SEO, even great content gets buried.
Picture this: You’ve added a post to our app, but it’s nowhere on Google. Why? Search engines couldn’t parse its structure or metadata. Today, we’ll harness Next.js, especially its App Router, to make our blog app a search engine star, using metadata, headers, and more.
Common Misconceptions:
Let’s make our blog app shine!
SEO hinges on content, technical structure, and user experience. Next.js excels with features like server-side rendering (SSR), static site generation (SSG), incremental static regeneration (ISR), and the App Router’s metadata API. These ensure fast, crawlable pages that search engines love.
/posts?tag=react
.<Image>
component speeds up load times, a key ranking factor.Consider our /posts/[slug]
page for a post like “Next.js Tips.” We want it to rank for “Next.js beginner tips.” Using the App Router, we’ll set metadata (title, description, Open Graph tags) and headers (cache-control, robots) to ensure it’s indexed and fast.
Misconception Alert: Many think the old <Head>
component is the only way to set metadata. In the App Router, Next.js offers a cleaner metadata API, and headers are just as critical for SEO control.
We’ll optimize our blog app’s /posts/[slug]
page (individual posts) and /posts?tag=react
route (filtered posts) using the App Router, focusing on metadata, headers, and other SEO essentials.
The App Router’s metadata API lets you set page titles, descriptions, and social tags either statically (via export const metadata
) or dynamically (via generateMetadata
). This tells search engines and social platforms what your page is about.
Simple Example: For a static post page in app/posts/[slug]/page.tsx
:
import { fetchPostBySlug } from '@/lib/api';
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
This sets a unique title and description, making the page searchable and appealing in results.
Advanced Example: Add Open Graph and Twitter tags for social sharing:
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.featuredImage],
url: `https://myblog.com/posts/${params.slug}`,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.featuredImage],
},
};
}
How It Works: generateMetadata
fetches the post and returns metadata dynamically. Search engines use title
and description
for snippets, while openGraph
and twitter
enhance social previews on platforms like X.
Advantages:
<Head>
, with type safety in TypeScript.images
) can weaken social previews.Headers like Cache-Control
, X-Robots-Tag
, and Link
(for canonical URLs) influence how search engines crawl and cache your pages. In the App Router, you can set headers in route handlers, middleware, or generateMetadata
.
Example: Canonical URLs and Robots: In app/posts/[slug]/page.tsx
, set a canonical URL to avoid duplicate content:
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
alternates: {
canonical: `https://myblog.com/posts/${params.slug}`,
},
robots: {
index: true,
follow: true,
},
};
}
Example: Cache Headers: For a static page, use headers()
in a route segment config:
export const dynamic = 'force-static';
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
export const headers = () => ({
'Cache-Control': 'public, max-age=31536000, immutable',
});
How It Works: The alternates.canonical
field prevents duplicate indexing (e.g., /posts/nextjs-tips
vs. /posts?tag=react
). robots
controls crawling (index: true
allows indexing). Cache-Control
ensures long-term caching for static pages, speeding up delivery.
Advantages:
noindex
) can hide pages.For /posts/[slug]
, SSG pre-renders posts, and ISR updates them dynamically. Combine with metadata for full SEO.
Example with ISR:
export const revalidate = 60; // Revalidate every 60 seconds
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
alternates: {
canonical: `https://myblog.com/posts/${params.slug}`,
},
};
}
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
How It Works: generateStaticParams
pre-renders all posts. revalidate: 60
updates pages every minute if edited in the admin panel. Metadata ensures proper indexing.
Advantages:
revalidate
risks stale data.Filtered routes are dynamic, so use SSR or dynamic rendering, paired with metadata.
Example: In app/posts/page.tsx
:
export async function generateMetadata({
searchParams,
}: {
searchParams: { tag?: string };
}) {
const tag = searchParams.tag || '';
return {
title: tag ? `${tag} Posts | My Blog` : 'All Posts | My Blog',
description: `Browse ${tag || 'all'} blog posts.`,
alternates: {
canonical: tag
? `https://myblog.com/posts?tag=${tag}`
: 'https://myblog.com/posts',
},
};
}
export default async function PostsPage({
searchParams,
}: {
searchParams: { tag?: string };
}) {
const tag = searchParams.tag || '';
const posts = await fetchPostsByTag(tag);
return (
<div>
{posts.map((post) => (
<div key={post.slug}>{post.title}</div>
))}
</div>
);
}
How It Works: generateMetadata
sets dynamic metadata based on the tag
query. The canonical URL ensures unique indexing for each tag page.
Pitfall to Avoid: Don’t use SSG for highly dynamic routes—it’s overkill and slows builds. SSR or dynamic rendering is better here.
Images slow pages if unoptimized. Use Next.js’s <Image>
component.
Example:
import Image from 'next/image';
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return (
<div>
<h1>{post.title}</h1>
<Image
src={post.featuredImage}
alt={post.title}
width={800}
height={400}
priority
/>
<p>{post.content}</p>
</div>
);
}
How It Works: <Image>
optimizes formats (e.g., WebP), lazy-loads, and uses alt
for accessibility and SEO.
Advantages:
Structured data enables rich snippets. Use JSON-LD in a <script>
tag.
Example:
import { fetchPostBySlug } from '@/lib/api';
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetchPostBySlug(params.slug);
const schema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
image: post.featuredImage,
datePublished: post.date,
author: { '@type': 'Person', name: post.author },
};
return (
<div>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
How It Works: The schema tells Google this is a blog post, potentially showing richer results.
Advantages:
/posts/nextjs-tips
and /posts?tag=react
might compete. Use alternates.canonical
in generateMetadata
.<Image>
and code-split.sitemap.xml
with next-sitemap
to index all posts.noindex
accidentally hides pages. Double-check robots
in metadata.SEO makes our blog app discoverable. The App Router’s metadata API and headers give us precise control over titles, descriptions, and crawling behavior. Here’s what to remember:
<strong>generateMetadata</strong>
for dynamic titles, descriptions, and social tags.Cache-Control
and robots
for performance and indexing.<Image>
.Best Practices:
Common Pitfalls:
With these practices, our “Next.js Tips” post will climb Google’s ranks, delighting users and admins alike. Let’s optimize and conquer search results!
Want your blog app to top Google searches? SEO is the key! In this lecture, we’ll use Next.js’s App Router to optimise our blogging app’s posts and filtered routes. We will learn about metadata, headers, and more to boost rankings.
Our blog app lets users filter posts and admins manage content. Without SEO, posts get lost. Today, we’ll use NextJS to optimise our blog, making it a search engine star.
SEO is important, as your page gets left behind in all important searches. SEO ensures our blog posts, like “NextJS Tips,” appear in Google searches, driving organic traffic to users browsing by tag or date.
NextJS simplifies SEO, with the App Router’s metadata API and headers making pages crawlable and fast, key for rankings.
Many think keywords and metadata alone win SEO, but structure and speed are equally vital. Check out
The simplest way to start SEO is with metadata. NextJS has a Metadata API that can be used to define your application metadata. In this example, we show that you can simply export a metadata variable with static metadata.
For dynamic pages, we can also generate meta-data dynamically using the generateMetadata function.
Our implementation of the generateMetadata function defines titles and descriptions per page, enhancing snippets.
generateMetadata fetches post data, setting up SEO dynamically for each slug.
The metadata object sets a unique title, description, and canonical URL, ensuring proper indexing and social previews. Canonical URLs allow you to define one URL for pages that you can access from different URLs.
Open Graph tags make posts shareable on Facebook, you can add similar tags for Twitter.
Let's check out another example.
Here, we force static site generation, which will generate a static page for every blog posts in our database. This is beneficial for speed and SEO rankings.
We will note that the static version is valid for 1 year, and after 1 years a new version should be generated.
The robots property provides instructions for web and AI crawlers, allowing indexing, making the post discoverable.
Last, we also set some custom headers, caching the page for a year and reinforce indexing, optimising delivery.
Yet, as we mentioned earlier, SEO is not just about metadata. Performance and semantic understanding help as well.
With NextJS, you should use the Image component, which ensures fast, SEO-friendly images for blog posts.
You can use schema markup, which tells Google this is a blog post, boosting rich snippets.
Last, using alt attribute makes images accessible and crawlable. In general, respecting accessibility guidelines will boost your SEO ranking.
Let's wrap up!
Use metadata export or generateMetadata to ensure unique, crawlable pages.
Set headers wisely and control caching and robots control speed and indexing
If you have duplicate content or slow images, it hurts rankings
We did not discuss this, but you can help crawlers to spell out all the urls the robots can visit, by providing sitemap xml file. This is very helpful when you can not provide all the links to your content from the landing page or there are no back links.
Good SEO is a work of art, and many businesses pay a lot of money to do it by professional companies. You can start learning about best practices on Google Central! Now, consider yourself SEO acolytes! Good luck!