# Next.js SEO: The App Router Guide to Metadata, Sitemaps and Schema (2026)
Author: Sai Krishna
Author URL: https://superblog.ai/blog/author/sai-krishna/
Published: 2026-11-05
Category: SEO
Category URL: https://superblog.ai/blog/category/seo/
Meta Title: Next.js SEO: App Router Guide for 2026 | Superblog
Meta Description: Next.js SEO for the App Router: metadata, sitemap.ts, robots.ts, canonicals, JSON-LD and hreflang, with code. Plus where your blog should live.
Tags: nextjs, SEO, subdirectory, developer, blog seo
Tag URLs: nextjs (https://superblog.ai/blog/tag/nextjs/), SEO (https://superblog.ai/blog/tag/seo/), subdirectory (https://superblog.ai/blog/tag/subdirectory/), developer (https://superblog.ai/blog/tag/developer/), blog seo (https://superblog.ai/blog/tag/blog-seo/)
URL: https://superblog.ai/blog/nextjs-seo/

Good Next.js SEO comes down to two jobs. The first is technical: every page ships server-rendered HTML with the right title, description, canonical, structured data and sitemap entry. The second is content: a steady stream of pages that answer what your buyers search for. Next.js handles a large part of the first job for you, and nothing of the second.

This guide covers both. The first half is the Next.js SEO setup for the App Router, with code. Every API name and file convention below was checked against the official Next.js documentation for **Next.js 16** (16.4.5 is the latest stable release on npm as of November 2026).

Most of it also applies to Next.js 15, and the version notes call out where it does not. The second half covers the part most guides skip: where the content that ranks will live, and who maintains it.

## What Next.js handles for SEO, and what it leaves to you

Next.js renders on the server by default. In the App Router, pages are React Server Components, and the docs state that "Client Components and the RSC Payload are used to prerender HTML" ( [Next.js docs](https://nextjs.org/docs/app/getting-started/server-and-client-components)). A crawler that requests a URL gets real markup, not an empty `<div>` waiting for JavaScript.

Googlebot can run JavaScript, but Google's [JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) page still recommends server-side or pre-rendering, since it speeds pages up for users and crawlers and some bots never execute JavaScript. Google also renders JavaScript in a separate queue, which can delay when client-built content reaches the index. Our [React SEO guide](/blog/react-seo/) covers the client-rendering pitfalls that Next.js mostly removes.

So the rendering foundation is solid. What Next.js gives you beyond that is a set of primitives:

- The **Metadata API** for titles, descriptions, canonicals, Open Graph and robots tags
- **File conventions** for `sitemap.xml`, `robots.txt`, Open Graph images and icons
- **Static generation, ISR and dynamic rendering**, chosen per route
- **`next/image`** for sized, lazy-loaded, format-converted images

What it does not give you: structured data, hreflang strategy, a content model, an editor, or any opinion about which pages should exist. You wire those yourself.

## The Next.js SEO checklist for the App Router

Work through these in order. Each one is a file or an export, and each one is cheap to get right on day one and expensive to fix after Google has indexed the wrong thing.

### 1\. Set site-wide defaults in the root layout

The root layout is the place for values every page shares: the base URL, the title template and a fallback description. Nothing page-specific belongs here (more on that in the mistakes section).

```tsx tsx
// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://acme.com'),
  title: {
    default: 'Acme',
    template: '%s | Acme',
  },
  description: 'Acme helps finance teams close the books faster.',
}

```

Three details from the [generateMetadata reference](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) that trip teams up:

1. `metadataBase` lets every URL-based field below it use a relative path. Using a relative path without `metadataBase` causes a build error.
2. `title.template` applies to **child** segments only. A template in `app/layout.tsx` does not apply to `app/page.tsx` in the same segment, and a `default` is required whenever you set a template.
3. Metadata exports work only in Server Components. If a page needs `'use client'`, keep `page.tsx` as a Server Component that exports the metadata and move the interactive part into a separate file.

### 2\. Use static `metadata` or `generateMetadata` per page

For a page whose title never changes, export a static `metadata` object. For a route whose metadata depends on data (a feature page, a template gallery, a docs entry), export `generateMetadata`:

```tsx tsx
// app/features/[slug]/page.tsx
import type { Metadata } from 'next'
import { getFeature } from '@/lib/features'

type Props = { params: Promise<{ slug: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const feature = await getFeature(slug)

  return {
    title: feature.title,
    description: feature.summary,
    alternates: { canonical: `/features/${slug}` },
    openGraph: {
      title: feature.title,
      description: feature.summary,
      url: `/features/${slug}`,
      images: [feature.ogImage],
    },
  }
}

```

Notes for current versions:

- `params` is a Promise in Next.js 15 and 16, so you `await` it.
- You cannot export both `metadata` and `generateMetadata` from the same route segment.
- `fetch` calls inside `generateMetadata` are memoized with identical calls in the page, so fetching the feature twice costs one request. If `getFeature` reads from a database or SDK instead of `fetch`, wrap it in React's `cache` to get the same effect.
- Since Next.js 15.2, metadata from `generateMetadata` can stream. Next.js appends the tags to `<body>` for bots that execute JavaScript (the docs name Googlebot) and keeps them blocking in `<head>` for HTML-limited bots such as `facebookexternalhit`. The `htmlLimitedBots` option in `next.config` overrides the list. The default is fine for most sites.

### 3\. Give every indexable page its own canonical

A canonical tells Google which URL is the real one when the same content is reachable at several (tracking parameters, trailing-slash variants, filtered views). In the App Router it lives under `alternates`:

```tsx tsx
export const metadata: Metadata = {
  alternates: { canonical: '/pricing' },
}

```

With `metadataBase` set, that renders as `<link rel="canonical" href="https://acme.com/pricing" />`. Set it per page, as a self-referencing URL. The one place you must never set it is the root layout, for reasons covered below.

### 4\. Generate the sitemap with `sitemap.ts`

Drop a `sitemap.ts` into `app/` and export a function that returns URLs. Next.js serves the result at `/sitemap.xml`:

```ts ts
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllFeatures } from '@/lib/features'

const BASE = 'https://acme.com'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const features = await getAllFeatures()

  return [
    { url: BASE, lastModified: '2026-10-01' },
    { url: `${BASE}/pricing`, lastModified: '2026-09-14' },
    ...features.map((f) => ({
      url: `${BASE}/features/${f.slug}`,
      lastModified: f.updatedAt,
    })),
  ]
}

```

Use real modification dates, not `new Date()`. A sitemap that stamps every URL with the build time teaches Google to ignore the field. Per [Google's sitemap documentation](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap), `lastmod` is only trusted when it keeps matching the page's real change history, and the `priority` and `changefreq` fields are not used at all. Leave those two out of your objects.

For large sites, a single sitemap tops out at 50,000 URLs. Next.js offers `generateSitemaps` to split one into numbered files, and as of Next.js 16 the `id` it passes in is a Promise that resolves to a string ( [sitemap reference](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap)). A `sitemap.ts` route is cached by default unless it uses a request-time API.

### 5\. Generate `robots.txt` with `robots.ts`

The same pattern applies to robots rules. A `robots.ts` in the root of `app/` returns an object, and `sitemap` accepts a string or an array:

```ts ts
// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: '*', allow: '/', disallow: ['/dashboard/', '/api/'] },
      { userAgent: 'GPTBot', disallow: '/' },
      { userAgent: 'OAI-SearchBot', allow: '/', disallow: ['/dashboard/', '/api/'] },
    ],
    sitemap: [
      'https://acme.com/sitemap.xml',
      'https://acme.com/blog/sitemap.xml',
    ],
  }
}

```

Three things in that example are worth explaining.

First, the AI crawler rules. OpenAI splits its crawlers by job: GPTBot gathers data for model training, and OAI-SearchBot decides whether your pages can show up as cited sources in ChatGPT search. OpenAI's [crawler documentation](https://developers.openai.com/api/docs/bots) treats each robots.txt setting on its own, so the example refuses training and stays citable.

Google works differently. Its [Google-Extended token](https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers) covers Gemini model training and grounding in Gemini Apps and Vertex AI, and has no effect on Google Search. AI Overviews are part of Search, so your Googlebot rules govern them ( [Google's AI features guide](https://developers.google.com/search/docs/appearance/ai-features)).

Anthropic runs [three agents](https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler): ClaudeBot collects training data, Claude-User opens pages a person asks Claude to read, and Claude-SearchBot serves Claude's web search. Make the call for each bot deliberately.

Second, the array of sitemaps. If your blog runs on a separate system at `/blog`, list its sitemap here too, so crawlers find both from one file. Next.js 16.3 also added an `other` field per rule for non-standard directives such as `Clean-param` ( [robots reference](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots)).

Third, the repeated paths. A crawler that finds a group naming it follows that group alone and skips the `*` rules ( [Google's robots.txt spec](https://developers.google.com/crawling/docs/robots-txt/robots-txt-spec)). Every named group therefore needs its own copy of the paths you want kept out, which is why the OAI-SearchBot entry repeats `/dashboard/` and `/api/`.

### 6\. Add JSON-LD with a native `<script>` tag

There is no Metadata API field for structured data. The [Next.js JSON-LD guide](https://nextjs.org/docs/app/guides/json-ld) recommends rendering it "as a `<script>` tag in your `layout.js` or `page.js` components," and warns that plain `JSON.stringify` does not sanitize strings used in XSS injection. Escape `<` before you inject:

```tsx tsx
// app/layout.tsx
const organization = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'Acme',
  url: 'https://acme.com',
  logo: 'https://acme.com/logo.png',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify(organization).replace(/</g, '\\u003c'),
          }}
        />
        {children}
      </body>
    </html>
  )
}

```

Organization schema fits the root layout because it describes the whole site. Page-level types (Product, SoftwareApplication, FAQPage, BlogPosting) belong in the matching `page.tsx`, built from that page's data.

Use a native `<script>`, not `next/script`: the guide points out that `next/script` is built for executable JavaScript, and JSON-LD is data. For typed objects, the docs suggest the community `schema-dts` package. Validate the output with Google's [Rich Results Test](https://search.google.com/test/rich-results).

For which schema types earn what, our [blog schema markup guide](/blog/blog-schema-markup-guide/) goes type by type.

### 7\. Pick a rendering mode per route, knowing the SEO effect

All three modes produce server-rendered HTML, so none of them hides content from crawlers. The difference is freshness and response time.

ModeHow you get itSEO effect**Static**Default for routes with no request-time data; `generateStaticParams` for dynamic segmentsFastest response, served from cache or CDN. Content is frozen until the next build**ISR**`export const revalidate = 3600`, or on-demand `revalidatePath` / `revalidateTag`Static speed, content refreshes without a full rebuild. Good fit for pages backed by a CMS**Dynamic**Request-time APIs such as `cookies()`, or a `fetch` with `no-store` or `revalidate: 0`Always fresh, but every crawl hits your server. Slow data sources show up as slow TTFB

A few facts from the [ISR guide](https://nextjs.org/docs/app/guides/incremental-static-regeneration) that matter here: if any `fetch` on a route uses `revalidate: 0` or `no-store`, the whole route renders dynamically. ISR needs the Node.js runtime and does not work with a static export. And the guide recommends a long revalidation window (an hour rather than a second), with on-demand revalidation when you need precision.

The real SEO risk is not the mode. It is content that loads after hydration. A Client Component is prerendered to HTML, but data it fetches in `useEffect` is not in that HTML. If pricing, reviews or product copy arrive that way, fetch them in a Server Component instead.

### 8\. Use `next/image` correctly for Core Web Vitals

Images drive two of the three Core Web Vitals: Largest Contentful Paint and Cumulative Layout Shift. From the [Image component reference](https://nextjs.org/docs/app/api-reference/components/image):

- **Always pass `width` and `height`** (or use `fill` inside a sized container). The docs say these values are "used by browsers to reserve space for the image and avoid layout shift during loading."
- **Lazy loading is the default.** Below-the-fold images load as the user scrolls.
- **For the hero image, set `loading="eager"` together with `fetchPriority="high"`, or use `preload`.** In Next.js 16, `fetchPriority` on its own still leaves the image lazy-loaded, because the component only switches off lazy loading for `preload`, the deprecated `priority`, or `loading="eager"`. Next.js 16 deprecated `priority` in favor of `preload`, and the Image docs steer most pages away from `preload` toward `loading="eager"` and `fetchPriority="high"`. Google's [LCP guide](https://web.dev/articles/optimize-lcp) is clear that the LCP image should never be lazy-loaded.
- **WebP is the default output format.** AVIF is opt-in through `images.formats` in `next.config`.

```tsx tsx
<Image
  src="/hero.png"
  width={1200}
  height={630}
  alt="Acme dashboard showing month-end close status"
  loading="eager"
  fetchPriority="high"
/>

```

For what each metric measures and the thresholds Google uses, see [blog Core Web Vitals](/blog/blog-core-web-vitals/).

### 9\. Build locale routing and hreflang yourself

The App Router has no built-in locale config of the kind the Pages Router had. The [internationalization guide](https://nextjs.org/docs/app/guides/internationalization) has you nest routes under `app/[lang]`, detect the locale in `proxy.js` (the file convention that replaced `middleware` in Next.js 16), and prerender each locale with `generateStaticParams` in the `[lang]` layout, which also sets `<html lang>`.

hreflang is then your job, in two places. In page metadata:

```tsx tsx
// app/[lang]/pricing/page.tsx
export async function generateMetadata({
  params,
}: PageProps<'/[lang]/pricing'>): Promise<Metadata> {
  const { lang } = await params
  return {
    alternates: {
      canonical: `/${lang}/pricing`,
      languages: {
        en: '/en/pricing',
        de: '/de/pricing',
        es: '/es/pricing',
      },
    },
  }
}

```

And in the sitemap, where each entry accepts `alternates.languages` (supported since Next.js 14.2). Each language version should carry a self-referencing canonical, as above, and list every sibling. Google lists missing return links among the most common hreflang mistakes and may ignore annotations that lack them ( [Google's localized versions guide](https://developers.google.com/search/docs/specialty/international/localized-versions)). Nothing in Next.js checks for them.

## Common Next.js SEO mistakes

These are the App Router mistakes that cost indexing or social previews. Most trace back to one rule in the docs: metadata objects from layouts and pages are **shallowly merged**, so a nested field set in a page replaces the parent's version completely, and a field the page omits is inherited from the parent unchanged.

1. **A canonical in the root layout.** Put `alternates: { canonical: '/' }` in `app/layout.tsx` and every page that does not set its own canonical inherits it. Google can take that as a signal that every page duplicates the homepage and drop those pages from the index. Set canonicals per page only.
2. **An `openGraph` object in a page that drops the image.** Because nested fields are replaced rather than merged, a page that sets `openGraph: { title }` loses the `images` the layout defined. Its social previews ship without an image. The docs' fix is to pull shared fields into a variable and spread them into each page's `openGraph`.
3. **`openGraph.url` set in the root layout.** Same inheritance problem as the canonical: every page announces the homepage as its URL to social platforms.
4. **Relative URLs with no `metadataBase`.** This fails the build, which is the good outcome. The bad outcome is hard-coding `http://localhost:3000` into a `metadataBase` that reaches production.
5. **Primary content fetched on the client.** Copy that only appears after `useEffect` runs is missing from the prerendered HTML, and not every bot will run your JavaScript to find it.
6. **`lastModified: new Date()` on every sitemap URL.** It looks fresh and tells Google nothing. Use stored update dates.
7. **Preview and staging URLs left indexable.** If a preview host is publicly reachable, return a `noindex` robots setting there, keyed off an environment variable, so it never competes with production.
8. **Links that fight your trailing-slash setting.** Next.js redirects `/about/` to `/about` by default, and the reverse with `trailingSlash: true` ( [trailingSlash reference](https://nextjs.org/docs/app/api-reference/config/next-config-js/trailingSlash)). Write internal links, canonicals and sitemap URLs in the form that does not redirect. With `trailingSlash: true`, rewrites to external hosts also need the slash in `source`, and in `destination` when the external host expects it ( [rewrites reference](https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites)).

## Why a fully optimized Next.js site still needs a blog

Once the checklist above is done, your product pages are crawlable, fast and correctly tagged. That is necessary, and it is not enough. A typical Next.js product site has a homepage, a pricing page, a handful of feature pages and a login screen. That set of URLs can rank for your brand name and very little else.

The queries that bring in buyers look different: "how to automate month-end close", "Acme vs Competitor", "reconcile Stripe payouts automatically". Those rankings come from pages written to answer them, published steadily, and linked into the rest of the site. In other words, a blog.

Here is what building that blog inside your Next.js app actually commits you to:

- **A content source.** MDX files in the repo, or a headless CMS with a content model, API keys and a fetch layer.
- **Per-post metadata.** `generateMetadata` for every post route: title, description, canonical, Open Graph image.
- **Per-post schema.** BlogPosting or Article JSON-LD with author, dates and image, plus FAQPage where a post has FAQs.
- **Sitemap and feed.** Post URLs added to `sitemap.ts` with real `lastModified` dates, and an RSS feed you generate yourself.
- **Category, tag and author archives**, each with its own metadata and pagination.
- **A publishing workflow for non-developers.** Drafts, previews, scheduling and image uploads that do not need a pull request.
- **Revalidation.** A webhook from the CMS to `revalidatePath`, or a full rebuild per post.
- **Optional extras that keep growing:** hreflang for translated posts, IndexNow pings, an `llms.txt` index, internal link suggestions.

Each item is a few hours. Together they are a small product, owned by the same engineers who own your actual product.

Superblog ships that list as a finished product, and it is option 2 below.

## Three ways to run the blog on a Next.js site

### 1\. Build it in Next.js with MDX or a headless CMS

This is the default answer for a reason. You keep one codebase, full design control and one deploy pipeline. MDX works well when developers write the posts. A headless CMS such as Sanity, Contentful or Strapi gives marketers an editor and leaves rendering to your Next.js routes, which can use ISR so a new post does not need a full rebuild.

The trade-off is everything in the list above. A headless CMS stores and edits content; it does not generate your BlogPosting schema, add posts to your sitemap, produce an RSS feed or write hreflang tags. Your frontend does, so your team maintains it.

Choose this path if you have engineering time reserved for content infrastructure and want the blog inside the same React codebase. Our guide to [headless CMS for business blogs](/blog/headless-cms-for-business-blogs/) compares the main options.

### 2\. Run Superblog at /blog through Next.js rewrites

[Superblog](https://superblog.ai) is a managed blogging platform: CMS, blog frontend, hosting and SEO engine in one product. Nothing in your Next.js routes changes. `/blog` is handed to Superblog with rewrite rules in `next.config.js`.

Visitors and crawlers see `yoursite.com/blog/your-post`, on your domain, so links to the blog build authority for the domain your product lives on.

Every item on the build list above is already handled (hreflang translations on Super and above), which is the point of this option. The trade-off is design control: the blog uses Superblog's templates with color, font, layout and custom CSS controls, not your React components.

### 3\. Host the blog on a subdomain

A separate blog platform at `blog.yoursite.com` needs only a DNS record, so it touches nothing in your Next.js app. The cost is SEO: the blog sits on a separate hostname, so the links it earns support `blog.yoursite.com` rather than the domain your product pages rank on. Our [subdomain vs subdirectory comparison](/blog/subdomain-vs-subdirectory/) covers when that trade is acceptable.

Build in Next.jsSuperblog at /blogBlog on a subdomain**URL**yoursite.com/blogyoursite.com/blogblog.yoursite.com**Engineering to launch**Days to weeksTwo rewrite rules, one redeployDNS record**Ongoing engineering**Yours, indefinitelyNone for the blogNone for the blog**Editor for non-developers**Only with a headless CMSBuilt inDepends on platform**JSON-LD, sitemap, RSS**You build eachAutomaticDepends on platform**Design control**FullTemplates plus custom CSSDepends on platform**Publishing a post**PR or CMS plus revalidationClick publishPlatform publish

## How Superblog serves a pre-rendered blog at /blog

The mechanism is a Next.js feature you already have. Rewrites "act as a URL proxy and mask the destination path, making it appear the user hasn't changed their location on the site" ( [Next.js rewrites reference](https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites)), and they can point at an external URL.

The Superblog dashboard generates the exact config for your site. It places two rules in the `beforeFiles` phase, one for `/blog` and one for `/blog/:slug*`, so they are checked before your own pages and dynamic routes and cannot be swallowed by a catch-all route. You add them, redeploy once, and you are done with code. The full setup, including the Vercel and Cloudflare variants, is in our walkthrough on [adding a blog to Next.js](/blog/add-blog-to-nextjs/).

After that, publishing is independent of your app:

- **Pages are pre-built.** Superblog builds every post into static HTML ahead of time and ships it to 200+ CDN edge locations worldwide. Crawlers get complete markup on the first request, and pages score 90+ on Lighthouse.
- **No Next.js builds for posts.** A writer clicks publish in the Superblog editor. Your app's build pipeline and build minutes are untouched.
- **Schema on every post.** Article (or BlogPosting), WebPage, Organization and ImageObject JSON-LD is generated automatically, plus FAQ schema when a post uses FAQ blocks.
- **Sitemaps, feeds and discovery.** An XML sitemap and RSS feed regenerate on every deploy. IndexNow notifies Bing and other supporting engines when a post goes live.
- **AI readability.** An `llms.txt` index at `/blog/llms.txt` points to a plain-markdown copy of each post, alongside the server-rendered HTML. That gives ChatGPT, Claude and other assistants clean text to read and cite.
- **Per-post control.** You set the meta title, meta description, canonical URL and Open Graph tags on each post. A SERP preview displays the search snippet before it goes live.
- **Multilingual on Super and above.** Translations live at subdirectory paths like `/blog/de/`, with hreflang in both the HTML head and per-language sitemaps.

Add the blog's sitemap to the `sitemap` array in your `robots.ts`, as in the example earlier, and crawlers will find both halves of the site from one file. For other stacks and proxy setups, see [subdirectory blog hosting](/blog/subdirectory-blog-hosting/). Teams comparing options can read the [Superblog for Next.js overview](/use-cases/blog-for-nextjs).

Pricing starts at $49/month on Pro (up to 5 team members, 1,000 posts). Super is $99/month and adds multilingual SEO, the REST API and Zapier. A 30-day money-back guarantee applies.

## FAQ

### Is Next.js good for SEO?

Yes. It server-renders by default, gives you a typed Metadata API, and generates sitemaps and robots files from code. You still have to add structured data, hreflang and per-page canonicals yourself.

### Should I use `metadata` or `generateMetadata`?

Use the static `metadata` object when a page's tags never change. Use `generateMetadata` when they depend on route params or fetched data. The Next.js docs recommend the static object whenever metadata does not depend on request information.

### Does `'use client'` hurt SEO?

Not by itself. Client Components are still prerendered to HTML on the server. Content fetched in the browser after hydration is the problem, not the directive.

### Where should the blog sitemap go if the blog is not in my Next.js app?

List it alongside your own sitemap in `robots.ts` by passing an array to `sitemap`, and submit both in Google Search Console.

### Can I keep MDX for docs and use Superblog for the blog?

Yes. The rewrite only covers `/blog`. Docs, changelogs or anything else you render with MDX stay in your Next.js app.

## The short version

Technical Next.js SEO is a checklist: root-layout defaults, per-page metadata and canonicals, `sitemap.ts`, `robots.ts`, JSON-LD in a native script tag, deliberate rendering choices, sized images and hand-built hreflang. Do it once and it holds.

The content side does not hold still. It needs a publishing system that someone maintains for as long as you publish. If your engineers would rather spend that time on the product, [try Superblog](https://superblog.ai) free for 7 days with no card, then connect `/blog` with two rewrite rules on a paid plan and let the blog rank on your domain.


---
This blog is powered by Superblog. Visit https://superblog.ai to know more.
---

