Learn how to fix missing metadata and structural issues in Next.js App Router to boost your technical SEO and organic traffic.
When the Next.js App Router launched, it brought incredible performance benefits, server components by default, and a streamlined way to handle layouts. But for many developers, moving from the Pages Router to the App Router introduced a steep learning curve, especially around search engine optimization (SEO).
If you have migrated a site or built a new web application using the App Router, you might have noticed that your search rankings dropped initially, or that social media scrapers like Twitter and LinkedIn are failing to pick up your Open Graph images. Most of the time, this comes down to how metadata is declared, inherited, or rendered on the server.
Today, I want to walk you through how I handle technical SEO and metadata in Next.js App Router projects for clients worldwide, ensuring your pages rank higher without relying on paid ads.
In the old Pages Router, we used to import the Head component from next/head inside any page and manually drop in our <title>, <meta>, and <link> tags. It was familiar, if a bit repetitive.
// Old Pages Router approach
import Head from 'next/head';
export default function BlogPost() {
return (
<>
<Head>
<title>My Blog Post</title>
<meta name="description" content="Learn how to code." />
</Head>
<h1>Hello World</h1>
</>
);
}
}
The App Router replaces this with a declarative Metadata API. Instead of messing with head tags inside components, you export a static metadata object or a dynamic generateMetadata function from your layout.tsx or page.tsx files.
// New App Router approach
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Blog Post',
description: 'Learn how to code.',
};
export default function BlogPost() {
return <h1>Hello World</h1>;
}
This runs entirely on the server, keeping your client bundle small and ensuring search engine crawlers receive the correct tags on the initial HTML response.
Tagged with
Leave a comment
Static metadata is easy, but real-world web apps deal with dynamic content—blog posts, user profiles, or SaaS dashboards fetched from a database or headless CMS. If you hardcode your metadata, every single product page will share the exact same title and description, which is a massive technical SEO red flag.
To fix this, we use generateMetadata. Here is an example of how I structure dynamic pages for content platforms, similar to how I built the Goldmines Picture Website:
import type { Metadata, ResolvingMetadata } from 'next';
type Props = {
params: { slug: string };
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
):
Promise<Metadata> {
const slug = params.slug;
// Fetch data from your database or API
const product = await fetch(`https://api.example.com/items/${slug}`).then((res) => res.json());
// Optionally access parent metadata
const previousImages = (await parent).openGraph?.images || [];
return {
title: `${product.title} | MyBrand`,
description: product.summary,
openGraph: {
images: [product.imageUrl, ...previousImages],
},
};
}
export default async function Page({ params }: Props) {
const product = await fetch(`https://api.example.com/items/${params.slug}`).then((res) => res.json());
return <div>{product.title}</div>;
}
By fetching the data inside generateMetadata, Next.js ensures that the <title> and <meta name="description"> tags are populated correctly before the page streams to the client.
Even with the correct metadata setup, I frequently see a few common mistakes during code reviews that hurt search visibility:
metadata inside a file that has 'use client' at the top, Next.js will throw an error or silently ignore it. Metadata exports are strictly reserved for Server Components.alternates.canonical property in your metadata config so search engines know which URL is the source of truth.robots.ts and sitemap.ts file in your app directory to explicitly guide search engine bots.Here is a quick example of a dynamic sitemap file in the App Router:
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('https://api.example.com/posts').then((res) => res.json());
const postEntries = posts.map((post: any) => ({
url: `https://mywebsite.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.7,
}));
return [
{
url: 'https://mywebsite.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postEntries,
];
}
Technical SEO in the Next.js App Router is powerful once you embrace the server-first mental model. By moving away from client-side head manipulation and adopting proper metadata exports, dynamic generation, and sitemaps, you give your web apps the best possible foundation for organic growth.
Need help auditing your web app's technical SEO or scaling your full-stack product? Let's build something great together. Get in touch to discuss your project.