Skip to content
~sami-ullah

Published Updated on 6 min read

Nuxt SEO Guide: useSeoMeta, useHead, Sitemap, Prerender

Good Nuxt SEO comes from four pieces: per-page titles and descriptions with useSeoMeta, canonical links and JSON-LD with useHead, an accurate sitemap and robots.txt, and HTML that is rendered on the server or prerendered with routeRules. Get those right in Nuxt 3 and search engines and AI tools can read every page without running JavaScript.

By · #nuxt #vue #seo

Nuxt SEO comes down to four things: a unique title and description on every page with useSeoMeta, canonical links and structured data with useHead, a correct sitemap and robots.txt, and HTML that arrives fully rendered from the server or a prerender step. This guide walks through each with working code for Nuxt 3, tested Nov 2026.

I build this portfolio in Next.js, where the same ideas apply through the Metadata API. The Nuxt version below is what I use for Vue projects such as the Nuxt news site concept build.

What does a search engine need from a Nuxt page?#

A search engine needs HTML that already contains the content, a unique title and description, one canonical URL, and a way to discover every page. Everything in this guide serves one of those four needs.

Nuxt helps because server rendering is on by default. The risk is in the details: duplicate titles, missing canonicals, client-only content and sitemaps that list pages that do not exist.

How do I set the title and meta description in Nuxt?#

Call useSeoMeta in each page component with the page's own title and description. It is a typed wrapper, so a typo like ogTitel fails type checking instead of shipping silently.

First, set site-wide defaults in app.vue with a title template:

vue
<script setup lang="ts">
useHead({
  htmlAttrs: { lang: "en" },
  titleTemplate: (title?: string) => (title ? `${title} | Example News` : "Example News"),
});
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

Then set per-page values. For a dynamic article page in pages/articles/[slug].vue, fetch the data with useFetch and pass getters to useSeoMeta, so the tags update when the data arrives and are correct in the server-rendered HTML:

vue
<script setup lang="ts">
type Article = {
  slug: string;
  title: string;
  excerpt: string;
  image: string;
  publishedAt: string;
  updatedAt: string;
  author: string;
};

const route = useRoute();
const { data: article } = await useFetch<Article>(`/api/articles/${route.params.slug}`);

if (!article.value) {
  throw createError({ statusCode: 404, statusMessage: "Article not found", fatal: true });
}

useSeoMeta({
  title: () => article.value?.title,
  description: () => article.value?.excerpt,
  ogTitle: () => article.value?.title,
  ogDescription: () => article.value?.excerpt,
  ogImage: () => article.value?.image,
  ogType: "article",
  twitterCard: "summary_large_image",
});
</script>

<template>
  <article v-if="article">
    <h1>{{ article.title }}</h1>
    <p>{{ article.excerpt }}</p>
  </article>
</template>

Three details matter here:

  1. Throwing a 404 with createError means a missing article returns a real 404 status, not a 200 page that says "not found". Soft 404s waste crawl budget.
  2. useFetch runs on the server during SSR and the result is transferred to the client, so the content and meta tags are in the first HTML response.
  3. Descriptions should be written, not generated from the first 160 characters of the body. For a news site that means an excerpt field in the CMS.

The full option list is in the Nuxt docs for useSeoMeta.

How do I add a canonical URL and JSON-LD?#

Use useHead for anything that is not a meta tag: the canonical link, hreflang alternates, and script tags with JSON-LD structured data. Build absolute URLs from one configured site URL so they are identical on every environment.

Put the site URL in runtime config:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      siteUrl: process.env.NUXT_PUBLIC_SITE_URL || "https://example.com",
    },
  },
});

Then add the canonical and an Article schema in the same page:

ts
// inside pages/articles/[slug].vue, after the useFetch call
const { siteUrl } = useRuntimeConfig().public;
const canonical = computed(() => `${siteUrl}/articles/${route.params.slug}`);

useHead({
  link: [{ rel: "canonical", href: canonical }],
  script: [
    {
      type: "application/ld+json",
      innerHTML: computed(() =>
        JSON.stringify({
          "@context": "https://schema.org",
          "@type": "NewsArticle",
          headline: article.value?.title,
          description: article.value?.excerpt,
          image: [article.value?.image],
          datePublished: article.value?.publishedAt,
          dateModified: article.value?.updatedAt,
          author: { "@type": "Person", name: article.value?.author },
          mainEntityOfPage: canonical.value,
        }),
      ),
    },
  ],
});

The canonical should not include tracking parameters or filter query strings unless those filtered pages are meant to rank on their own. On a listing page with filters, the canonical usually points at the unfiltered URL.

Check structured data with Google's Rich Results Test after deploying, and make sure the JSON-LD matches what users can see on the page. Google's structured data guidelines are explicit that marked-up content must be visible.

My Nuxt news site demo uses this kind of NewsArticle markup on its article pages, together with server rendering and route caching, so you can view its page source and see the tags in the raw HTML.

How do I generate a sitemap in Nuxt?#

Use the @nuxtjs/sitemap module, or write a small server route that returns XML. The module is the better default because it reads your routes, supports dynamic URLs from an API, and handles lastmod and i18n alternates.

bash
npx nuxi module add @nuxtjs/sitemap
npx nuxi module add @nuxtjs/robots
ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@nuxtjs/sitemap", "@nuxtjs/robots"],
  site: {
    url: "https://example.com",
    name: "Example News",
  },
  sitemap: {
    sources: ["/api/__sitemap__/urls"],
  },
  robots: {
    disallow: ["/admin", "/api"],
  },
});

The sources endpoint returns the dynamic URLs, with real modification dates:

ts
// server/api/__sitemap__/urls.ts
import { defineSitemapEventHandler } from "#imports";

type ArticleRow = { slug: string; updatedAt: string };

export default defineSitemapEventHandler(async () => {
  const articles = await $fetch<ArticleRow[]>("https://cms.example.com/articles?fields=slug,updatedAt");
  return articles.map((a) => ({
    loc: `/articles/${a.slug}`,
    lastmod: a.updatedAt,
  }));
});

Only list URLs that return 200 and are canonical. A sitemap full of redirects or noindexed pages teaches search engines to trust it less. Use real lastmod values; setting every page to today's date on every build makes the field useless.

Once deployed, submit the sitemap in Google Search Console and Bing Webmaster Tools. Bing matters more than it used to, because several AI search tools rely on its index.

Should I prerender, use SSR, or both?#

Use both. Prerender pages that only change when you deploy, and server-render or cache the rest. Nuxt's routeRules let you set this per route in one place.

ts
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true },
    "/about": { prerender: true },
    "/articles/**": { swr: 600 },
    "/search": { ssr: true, robots: false },
    "/admin/**": { ssr: false, robots: false },
  },
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ["/sitemap.xml"],
    },
  },
});

What each rule does:

RuleWhat happensGood for
prerender: trueHTML built once at deploy time and served as a static fileHomepage, about, evergreen landing pages
swr: 600Rendered on first request, cached for 600 seconds, refreshed in the backgroundArticles, product pages, listings that change during the day
isr: trueLike swr, cached on supported hosts until the next deployLarge catalogues on Vercel or Netlify
ssr: true (default)Rendered on every requestSearch results, personalised pages
ssr: falseRendered only in the browserAdmin areas and dashboards that should not be indexed

crawlLinks: true tells Nitro to follow links from prerendered pages and prerender those too, which is handy for small content sites. Read the Nuxt rendering modes guide for the trade-offs of each mode on your host.

The robots: false key in route rules comes from the robots module and adds a noindex header for those routes. Search result pages and admin screens should not compete with your real content.

How do I handle Open Graph images and social previews?#

Give every shareable page an absolute ogImage URL of at least 1200 by 630 pixels, because social platforms and chat apps fetch it without running your JavaScript. A relative path or an image that only loads on the client results in a blank preview card.

For a news or blog site, the simplest option is a cover image stored with each article in the CMS, passed to useSeoMeta as shown above. If articles do not have images, the nuxt-og-image module can generate one per page from a Vue component template at build or request time, using the title and site name.

Test previews with the sharing debuggers from the platforms your audience uses, and remember that they cache aggressively. After you fix a tag, ask the debugger to scrape the page again before deciding the fix did not work.

What else breaks Nuxt SEO in practice?#

The usual problems are content that only renders on the client, hydration mismatches, slow images and missing internal links. None of them show up in a quick visual check.

A short checklist I run before launch:

  1. View the page source (not the inspector) and confirm the main content, title and description are in the raw HTML.
  2. Wrap browser-only widgets in ClientOnly, but never the main content.
  3. Use @nuxt/image with explicit width and height to avoid layout shift, and loading="lazy" for images below the fold.
  4. Check the browser console for hydration mismatch warnings; they often mean server and client render different text.
  5. Link related pages to each other with NuxtLink, so crawlers can find pages that are not in navigation.
  6. Run Lighthouse on a mobile profile and fix Largest Contentful Paint first.

Related reading: if you are still choosing a framework, see Vue vs React for a new project. My Vue and Nuxt work is collected on the Vue skill page, and the same principles for listing pages apply to the Vue property listing concept.

What should you do first?#

Open your three most important Nuxt pages, choose "View page source", and search for the title tag, the meta description and the canonical link. If any is missing, generic or identical across pages, fix it with useSeoMeta and useHead today. Then add @nuxtjs/sitemap, submit the sitemap in Search Console, and set prerender: true on every page that only changes when you deploy.

Frequently asked questions

What is the difference between useSeoMeta and useHead in Nuxt?

useSeoMeta is a typed helper for meta tags only: title, description, Open Graph and Twitter tags, with autocomplete and checks for typos. useHead is the general tool for anything in the document head, including link tags such as canonical and alternate, script tags such as JSON-LD, html attributes and body classes. Most pages use useSeoMeta for meta tags and useHead for links and structured data.

Do I need the Nuxt SEO module?

No, but it saves time. Nuxt itself provides useSeoMeta, useHead and server rendering. The community Nuxt SEO module bundles sitemap, robots, OG image, schema.org and link checking modules with shared site config. On a small site you can write a sitemap route by hand in about twenty lines. On a larger site, the modules handle edge cases like i18n alternates and lastmod dates.

Should I prerender my Nuxt site or use SSR?

Prerender pages whose content changes only when you deploy, such as the homepage, about page and blog posts. Use server rendering, optionally with ISR or SWR caching via routeRules, for pages that change often or have too many URLs to build ahead of time, such as search results or large catalogues. Nuxt lets you mix both in one app per route.

Is a Nuxt single page app bad for SEO?

It is weaker. With ssr set to false, Nuxt sends an almost empty HTML page and builds content in the browser. Google can render JavaScript, but rendering is delayed and less reliable, and many AI crawlers and social preview bots do not run JavaScript at all, so they see no content, title or description. Keep server rendering or prerendering on for any page you want found.

Where I used this

Keep reading

#nextjs6 min

JSON-LD Structured Data in Next.js 15

How to add JSON-LD structured data in Next.js 15: a safe script component, typed schema-dts graphs with stable @id values, FAQ rules and how to test it.

read →

#nextjs6 min

Next.js App Router SEO Checklist for Next.js 15

A practical Next.js App Router SEO checklist for Next.js 15: metadata, canonicals, sitemap, robots, JSON-LD, rendering, images and 404s, with working code.

read →