Skip to content

The Three Metadata Layers Behind a Clean Next.js SEO Audit

Adityo Guni Waluyo

Canonical, hreflang, metadataBase, and JSON-LD LocalBusiness: the three layers that made sixteen bilingual Next.js routes pass an SEO audit.

TL;DR

Sixteen bilingual Next.js routes passed a full SEO audit thanks to one metadata commit. Three layers did the work: a shared helper generating canonical and hreflang tags, metadataBase in the root layout, and JSON-LD marking up the business and website. Centralizing everything keeps URLs consistent, so new routes stay audit-proof instead of relying on Google to guess.

Sixteen routes passed the audit in one commit

I was staring at an internal SEO audit when sixteen bilingual routes of a Next.js App Router site passed clean off a single commit: seo: complete on-page metadata. No warnings, no findings. My first assumption about this kind of work had always been lazy: metadata is a title and a description, maybe an OG image, done. What actually made the audit pass was three separate layers working together: canonical and hreflang, the metadataBase configuration, and JSON-LD identity scripts in the head.

Canonical and hreflang: a reciprocal contract

The shared helper pageMetadata() in frontend/src/components/shared/meta.ts became the single source of truth for every route: an absolute canonical pointing at production, hreflang alternates for id/en with x-default aimed at the Indonesian version, and Open Graph plus Twitter cards sharing one 1200x630 image. Google's side of the deal is specific. Every language version must list itself and all the others, the URLs must be fully-qualified, and if two pages do not point at each other, the tags are ignored [3]. The docs also recommend a self-referencing canonical on the canonical page itself [2]. None of these methods are required; without them Google simply picks a URL on its own [2], so every explicit signal is advice, not an order.

One misconception died on the way: Google does not use hreflang or the HTML lang attribute to detect a page's language, its own algorithms do that [3]. The tags only steer which version gets served to which reader. That is why one centralized helper matters: the classic failure is a complete Indonesian cluster and an English route that forgot one link, and half the hreflang cluster collapses without a single error message.

metadataBase and the double-bookkeeping habit

The root layout also sets metadataBase: new URL(BASE_URL), and that is where I caught myself smiling. The helper already builds absolute URLs by hand while metadataBase sits in the layout, and per the Next.js documentation a metadata field carrying an absolute URL ignores metadataBase [1]. One of the two mechanisms is redundant. The pattern the docs expect is simpler: set metadataBase once at the root layout, let every other field use relative paths, and let the framework compose the final URLs [1]. Two rules make the pattern worth memorizing: a relative path without metadataBase triggers a build error [1], and title.template only applies to child route segments, never to the segment that defines it [1].

JSON-LD: telling machines this is a real business

The last layer is two JSON-LD scripts injected into the head from the root layout. The first types the organization as LocalBusiness, the schema.org type for a particular physical business or branch [4] and one of the most widely used types in the wild: 1 million to 10 million domains per Google's web index aggregation from August 2026 [4]. It carries the boring facts that matter: legal name, address, telephone, founding date. The second script types the WebSite and links it to the organization through an @id reference, so a crawler reads one connected graph instead of two loose blocks.

The canonicalization signals themselves stack: redirects are the strongest signal, rel=canonical follows, sitemap inclusion is weak [2], and combining methods makes them more effective [2]. The commit adds no redirects, but it keeps one URL shape across canonical, hreflang, and Open Graph, so every signal points the same way. It also works inward: internal links should point at the canonical URL rather than a duplicate [2]. After this consolidation, adding a route is one function call away from correct metadata, and a new route that skips the helper gets caught by the next audit run, not by Google.

Sources

[1] Next.js docs: generateMetadata
[2] Google Search Central: How to specify a canonical URL
[3] Google Search Central: Localized versions of your pages
[4] schema.org: LocalBusiness

Related articles