Power SEO Meta vs Next SEO: Which SEO Library is Better for Modern Next.js App Router?
Three weeks after deploying a 60-page product catalog, a client's marketing team messaged me: "Why are our product images not showing when we share links on LinkedIn?" I opened Google Search Console. Twenty-three out of sixty product pages had max-image-preview missing from their robots directives. Six draft pages were indexed when they should have been noindex. Zero build errors. Zero runtime warnings. Everything looked perfect in the code. That was the moment I stopped trusting raw metadata strings and started thinking seriously about how App Router changed the SEO game — and which tools have actually caught up. Before Next.js 13's App Router, the standard move was — drop it into your component, pass props, done. It works beautifully on the Pages Router. Genuinely great DX. But is a React component. And React components can't run inside React Server Components. The only way to use it on App Router is to add 'use client' to your page — which ships unnecessary JavaScript to the browser for something that should be pure server logic. Or you skip it entirely and write raw Metadata objects by hand. That's the architectural mismatch. App Router introduced generateMetadata() — a server function that returns a Metadata object at request time, with zero client JS. Next-seo has no equivalent. So if you're on App Router and typing import { NextSeo } from 'next-seo', you're either working around the framework or ignoring it entirely. Here's the scenario I see constantly: a product catalog with 50+ pages, each needing its own title, description, canonical URL, and Open Graph image. Without a shared utility, the robots logic drifts. One page has max-image-preview:large, another doesn't, a third has a missing canonical. This is how I structure it now using @power-seo/meta: // lib/seo/product-metadata.ts import { createMetadata } from '@power-seo/meta'; import type { Metadata } from 'next'; interface Product { name: string; description: string; slug: string; imageUrl: string; inStock: boolean; } // One function = one source of truth for all 50 product pages. // Change robots logic here → every page updates immediately. export function buildProductMetadata(product: Product): Metadata { return createMetadata({ title: `${product.name} | Shop`, description: product.description.slice(0, 160), canonical: `https://shop.example.com/products/${product.slug}`, openGraph: { type: 'website', title: `${product.name} | Shop`, description: product.description.slice(0, 160), images: [{ url: product.imageUrl, width: 1200, height: 630, alt: product.name }], }, robots: { index: product.inStock, // out-of-stock pages become noindex automatically follow: true, maxImagePreview: 'large', // typed union: 'none' | 'standard' | 'large' }, }); } // app/products/[slug]/page.tsx import { buildProductMetadata } from '@/lib/seo/product-metadata'; import { getProduct } from '@/lib/products'; import type { Metadata } from 'next'; export async function generateMetadata( { params }: { params: Promise } ): Promise { const { slug } = await params; const product = await getProduct(slug); return buildProductMetadata(product); // 1 line per page file } export default function ProductPage() { return {/* product content */}; } One shared utility. One line per page file. Updating robots logic across all 50 product pages means editing one function. I've shipped this pattern on three projects and the metadata drift issues haven't come back. Compare this to the next-seo equivalent on App Router — there isn't one. Each page writes its own `Metadata` object from scratch: 20+ repeated lines with `openGraph.title`, `openGraph.description`, and the full robots object rebuilt every time. That's where the drift comes from. ## The silent robots bug TypeScript can actually catch Here's the bug from the client situation I described above. This is a real robots string I found in production: typescript Spot it? `preveiw` instead of `preview`. The build passes. The page renders. Google silently ignores the directive. You find out three weeks later in Search Console. There's a second issue too: next-seo's `noindex` prop emits ``. Then the `additionalMetaTags` robots entry emits *another* `` tag. Google's behavior with duplicate robots tags is not guaranteed. I've seen this cause real indexing inconsistencies on two separate client projects. Here's the typed approach: typescript export async function generateMetadata( return createMetadata({ https://example.com/products/${slug}, Two specific things worth calling out: If you write `maxImagePreview: 'Large'` (capital L), TypeScript catches it at compile time. It never reaches production. `unavailableAfter` is automatically serialised to the correct `unavailable_after` snake_case format. Build that string manually and a missing underscore silently makes Google ignore the directive entirely. ## One gotcha that trips everyone up during migration If you're switching from next-seo to `@power-seo/meta`, there's one thing that will catch you: **Open Graph title and description do NOT auto-fallback.** In next-seo, `openGraph` inherits `title` and `description` from top-level props automatically. In `@power-seo/meta`, you must set `openGraph.title` and `openGraph.description` explicitly. Miss this and your OG tags will be empty. It's the most common mistake I've seen in migration. typescript // CORRECT — set them explicitly ## What I learned - **App Router and client components are fundamentally mismatched for metadata.** A server function returning a `Metadata` object works with the framework. A component that injects tags at runtime works around it. - **Raw string props in SEO libraries are a liability.** The typo you can't see is the bug you won't find until it's in Search Console three weeks later. - **Metadata drift is a consistency problem that grows slowly and gets noticed late.** Shared utility functions that return metadata are the fix — one place to update, zero pages to hunt down. - **Community size matters.** `next-seo`'s 7,500+ stars and 800K weekly downloads mean your problem has probably been solved somewhere. `@power-seo/meta` doesn't have that yet. For Pages Router projects, that community advantage is real and worth respecting. If you want to explore the typed approach, the source is here: [github.com/CyberCraftBD/power-seo](https://github.com/CyberCraftBD/power-seo) ## Still deciding? The short version: if you're on Pages Router and have no migration planned, `next-seo` is still excellent — use it. If you're on App Router and writing `'use client'` just to get meta tags working, that's the sign to reconsider. **What's your current setup?** Are you on App Router using `generateMetadata()`, or still on Pages Router? And if you've hit the duplicate robots tag issue I described — curious whether you caught it before or after it showed up in Search Console. Drop a comment below.
