Back to blogs

SEO Before You Ship

Check metadata, canonical URLs, social previews, crawlability, sitemaps, internal links, and missing pages before publishing your Next.js product.

By Antoine RossPublished
Share:

Good SEO before launch starts with three outcomes: search engines can reach the pages you want public, each page identifies one production URL, and private or missing pages stay out of search.

For a Next.js product, that means checking metadata, canonical URLs, social previews, crawlable content, structured data, internal links, sitemap membership, and not-found behavior. No single item is "SEO." Together, they determine whether the application presents a clear, consistent set of pages.

The standalone CoreMVP template connects those outputs to shared URL helpers and the MDX article source. It does not choose the production domain, write the final page content, set honest publication dates, or decide which custom routes should be indexable. Those are launch decisions you still own.

This is not a promise of rankings. It is the technical foundation that lets search engines discover the right pages, understand them, and represent them consistently.

The browser is only one reader. Crawlers read metadata and links. Social platforms request preview images. Sitemaps publish another site inventory. The rule for every public page is simple: its visible content, metadata, canonical, social preview, internal links, and sitemap entry should agree about what the page is and where it lives.

One page, one identity

Before you tune a title or preview, decide which URL owns the page.

Every absolute public URL needs a common origin. In the standalone template, NEXT_PUBLIC_APP_URL supplies that origin. The URL helpers use it when they build canonicals, social URLs, the sitemap location, and public sitemap entries.

The alternative is easy to get almost right: type the production hostname into a layout, repeat it in the sitemap, then use a Vercel preview URL in an image route. Each value works. Together they describe different sites.

Central URL construction removes that drift. It also creates a constraint: if the public origin is wrong, every derived URL can be consistently wrong. The configuration matters more because it is authoritative.

Canonicals sit on top of that decision. A public page can be reached through several URLs, but it should identify one preferred URL. Google treats the canonical as a signal when it chooses a representative URL; it is not a command and does not guarantee indexing or ranking. Google's canonical guidance makes that distinction explicit.

Indexability is a separate decision. A sign-in page or account dashboard should not become a public canonical owner merely because it has a URL. The template keeps public-page metadata and excluded-page metadata separate instead of forcing every route through one helper.

One application can have many pages. Every public page still needs one preferred identity.

One source, many outputs

Once the URL has an owner, the content needs one too.

In the Blog, that owner is the MDX article. Its frontmatter supplies the title, description, dates, author, and tags. Its slug determines the path. The Blog reader fans that record out to every public representation that depends on it.

If changing an article title requires editing the article, its card, its metadata, its social image, and its structured data separately, you do not have five SEO features. You have five opportunities for drift.

The template's slug-aware opengraph-image.tsx and twitter-image.tsx routes resolve the article record, then pass its title into the shared Blog composition. The square card and wide sharing image stay aligned without another copy of the title.

Next.js also supports static files such as opengraph-image.png. Generated images are easier to synchronize; static images give you more art direction. The bad state is letting both act as separate authorities. Next.js documents both options.

Dates need the same discipline. The sitemap uses an article's updated date when it exists and its publication date otherwise. A deployment is not necessarily a page change. Google says lastmod should represent the last significant update and remain consistently accurate. Its sitemap guidance includes changes to primary content, structured data, or links as examples.

Structured data should also repeat visible truth, not invent a richer story. The article renderer derives BlogPosting and breadcrumb data from the article record and canonical URL. It includes fields the implementation can support truthfully. It omits a structured-data image instead of guessing a durable public URL for a framework-managed generated asset.

Google defines no required properties for Article markup; it recommends the properties that apply. Its Article documentation and general structured-data guidelines point toward the same rule: markup should represent the visible page.

The tradeoff is that frontmatter matters more. A wrong title or date can propagate everywhere. That is still easier to diagnose than several plausible values in different files.

Choose one article record, then make every public representation derive from it.

Give signals separate jobs

Links, sitemaps, robots.txt, and noindex all affect discovery. They are not interchangeable.

Internal links help readers and crawlers reach a page. A sitemap advertises eligible URLs. robots.txt controls crawling. noindex controls indexing.

The Blog listing and latest-article section link to published posts with real href values. That makes an article part of the site instead of an isolated URL known only by its slug. Google generally discovers standard <a href> links and recommends linking to important pages from elsewhere on the site. Its crawlable-links guidance also helps readers continue.

The sitemap publishes another view of the intentional public inventory: marketing pages, ungated documentation, and Blog articles. It can help discovery, but it cannot make a weak or contradictory page useful, canonical, or indexable by itself.

robots.txt answers a different question: which paths may compliant crawlers request? The template allows the public site and disallows /api/. It does not block the accessible HTML pages that rely on page-level noindex.

That last detail avoids a common contradiction. If you block an HTML page in robots.txt, a crawler may never retrieve the page and see its noindex directive. Google explicitly says a page must remain accessible to its crawler for noindex to be effective. The noindex guidance also warns that noindex inside robots.txt is not supported.

Authentication is another boundary again. It protects private content. A page-level indexing policy tells supporting search engines what to do with accessible HTML. Neither replaces the other.

Do not ask one SEO mechanism to perform another mechanism's job. Decide whether a page may be crawled and whether it may be indexed as two separate questions.

Missing pages refuse ownership

A missing URL has one fact to publish: the requested content does not exist.

For a Blog article, the route resolves the requested slug against the MDX source. If there is no matching article, it calls notFound(). The generated Open Graph and Twitter image routes use the same rule rather than creating a preview for an article that does not exist.

The missing article stays out of the Blog listing and sitemap because both come from the article collection. No separate list needs to remove it.

The HTTP status needs one caveat. Next.js returns a 404 for a non-streamed not-found response, but a response that has already started streaming can remain HTTP 200 because its headers were sent earlier. Next.js still renders the missing state and injects noindex. The framework documents the distinction in its not-found convention and its explanation of streaming status codes.

This makes the useful rule more precise than "every notFound() returns 404."

A missing URL must not become an indexable content owner.

If a literal 404 is required for analytics, compliance, or monitoring, the application needs to establish that the resource is absent before the response begins streaming. That is an operational requirement, not a reason to misdescribe what the framework does.

HTTP status and search ownership are related. Once streaming enters the path, they are not the same question.

Test four URL states

Before publishing, choose four representative URLs. Prove that each kind tells one coherent story.

A public product page exists and wants search ownership. Its content, self-referencing canonical, social metadata, internal links, and sitemap membership should point to the same production URL.

A published MDX article also wants ownership, but its repeated facts should come from the article record. Its rendered title, metadata, sharing image, structured data, canonical, article links, and sitemap date should agree with that source.

An excluded page exists but does not want public search ownership. Its access boundary should match the content it protects. Accessible HTML should carry the intended indexing policy, avoid presenting itself as a public canonical owner, and stay out of the public sitemap. If a crawler needs to observe noindex, robots.txt must not prevent that crawl.

A missing article does not exist. It should render the missing state, carry noindex, stay out of the Blog listing and sitemap, and return the status your route's streaming contract allows.

The template connects its public-origin helpers, metadata, MDX source, sharing images, structured data, sitemap, links, and missing routes. You still own the production origin, page copy and dates, public-route decisions, and each article's generated or static artwork.

You also own Search Console, analytics, distribution, redirects, RSS, and localization. The template can make a page eligible and internally consistent. It cannot guarantee crawling, indexing, ranking, a rich result, or a click.

Ask four questions before you ship:

  1. Does this URL exist?
  2. Should it own a search result?
  3. If yes, do its public representations agree on its identity?
  4. If no, do its discovery and indexing surfaces agree that it owns nothing?

Ship when every public representation agrees about what the page is, where it lives, and whether it belongs in search.

Set the final origin and product identity in Configuration, choose the Blog's generated or static social-image path in Customization, then use the Vercel deployment guide to verify the production URL before you publish.

About the author

I'm Antoine. I build CoreMVP and write about the problems I run into, the decisions I make, and what I learn along the way.