The technical SEO issues that show up in almost every Next.js site

Short answer

The technical SEO issues that recur in Next.js projects are identical titles and descriptions inherited from the root layout, titles and descriptions at the wrong length, missing or wrong canonical URLs, a page-level openGraph that replaces the layout's, noindex inherited from a layout, duplicate www, http and trailing-slash URLs, a missing sitemap or robots file, and missing or invisible structured data. Also common are content that only exists after hydration, images without dimensions or alt text, skipped heading levels and missing hreflang. Each section gives the fix, with App Router code where it applies.

Next.js gives you a lot of SEO for free, such as server-rendered HTML and a typed metadata API. It also makes a handful of mistakes very easy. These are the ones that recur, with the fix for each. The examples use the App Router; check your own Next.js version's documentation, since details change between releases.

1. Every page inherits the same title and description

Set a title and description in the root layout and forget the rest, and every page ships with the same ones. Search engines then show identical results for different pages, and you lose the chance to match each page to its query.

app/pricing/page.tsx
export const metadata = {
  title: "Pricing",
  description: "What each plan includes and how billing works.",
};

For dynamic routes, export generateMetadata() and build the values from the same data the page renders. Use a title template in the root layout (title: { default, template: "%s | Brand" }) so pages only supply their own part.

2. Titles and descriptions at the wrong length

Google truncates a title link as needed, typically to fit the device width, not at a fixed character count, but as a working rule titles near 50 to 60 characters and descriptions near 120 to 160 are rarely cut off. Very short ones waste space; very long ones get truncated mid-phrase. It is a cheap check to automate and a common finding on older pages.

3. Missing or wrong canonical URLs

Without a canonical, the same page can be indexed under a tracking-parameter URL, a trailing-slash variant or a preview domain (see Google's guidance on canonical URLs). Set metadataBase once, then a relative canonical per page:

// app/layout.tsx
export const metadata = { metadataBase: new URL("https://example.com") };

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const { slug } = await params;
  return { alternates: { canonical: `/blog/${slug}` } };
}

4. A page-level openGraph that replaces the layout's

Metadata merges shallowly: if a page exports its own openGraph, it replaces the layout's openGraph object rather than extending it. Fields you set in the layout, such as siteName or images, silently vanish on that page. Repeat the shared fields, or build them from one helper.

5. noindex inherited from somewhere you forgot

A robots: { index: false } in a layout applies to every page beneath it. It is common during staging and easy to ship to production. If a section of your site is missing from search, check the layout chain before anything else.

6. www, trailing slashes and http all resolving

If www.example.com, example.com, and http:// versions all return 200, you have duplicates. Pick one, and redirect the rest permanently. In Next.js you can do this in redirects(), where permanent: true sends a 308, or at your host or proxy.

7. No sitemap or robots file

The App Router can generate both from code:

app/sitemap.ts
import type { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return [{ url: "https://example.com/", lastModified: "2026-01-15" }];
}

Use real last-modified dates. Google uses the field only if it is consistently and verifiably accurate, which a date that changes on every build is not. Only list URLs you want indexed, and make sure none of them are noindex, redirected or blocked in robots.txt.

8. Structured data that is missing, invalid or invisible

Add JSON-LD for what the page really is: Organization, Article, BreadcrumbList, FAQPage when the page really contains those questions and answers. Serialise it safely, so page text cannot close the script tag:

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(data).replace(/</g, "\\u003c"),
  }}
/>

Mark up only what visitors can see on the page. Markup that describes content that is not there is worse than none.

9. Core content that only exists after hydration

A client component that fetches data in useEffect renders an empty shell in the HTML. Crawlers that do not run JavaScript see the shell, and the crawler documentation from OpenAI, Anthropic and Perplexity does not say whether their bots run JavaScript. Fetch on the server for anything you want indexed and keep client-side fetching for interactive extras.

10. Images without dimensions or alt text

Missing width and height cause layout shift, which hurts Core Web Vitals. Missing alt costs accessibility and image-search context. next/image makes the first one hard to forget; the second is on you.

11. Headings that skip levels or repeat the H1

One descriptive <h1> per page, then <h2> and <h3> in order. Design systems often render a visual heading as the wrong element, which is invisible in the browser and obvious in an outline view.

12. Missing hreflang on multi-language sites

If you publish the same page in several languages, each version should list the others through alternates.languages, and the references must be reciprocal. One-way hreflang is ignored.

Checking all of this automatically

None of these are hard individually. The trouble is that they regress quietly as a site grows. Crawlmend checks for these patterns on every crawl and opens a pull request against your metadata files for the ones it can fix; see supported setups for what that covers in Next.js and what it does not. Or try it on your repo.