nuxt-seo-utils
Requires Nuxt ^4.6.0 || ^5.0.0 and Unhead 3.4.2 or newer.
The module adds default head tags from site config, reads icon and social image files, and ships breadcrumb and share link composables.
Docs: https://nuxtseo.com/docs/seo-utils
Setup
Install with npx nuxt module add nuxt-seo-utils.
Included in @nuxtjs/seo v6. If the app uses @nuxtjs/seo, configure the seo key and do not add the module again.
The config key is seo. seoUtils does nothing.
Set site.url and site.name. The canonical URL, og:url, the title template, and absolute image URLs read them.
export default defineNuxtConfig({
modules: ['nuxt-seo-utils'],
site: { url: 'https://example.com', name: 'Acme', description: 'Default description', twitter: 'acme', defaultLocale: 'en-US' },
})Crawlers read only the SSR response. The defaults render on the server.
Automatic behaviour
With the defaults, every page gets:
<title>from the template%s %separator %siteName. The separator is|. Change it withsite.separator.- A fallback title from the last path segment, title cased:
/blog/my-postbecomesMy Post.definePageMeta({ title })sets it with a static string. <link rel="canonical">fromsite.urland the route path, lowercased, with only these query keys kept:page,sort,filter,search,q,category,tag.og:url(the resolved canonical, including one a page sets),og:type(website),og:site_name(site.name), anddescriptionfromsite.description.og:title,og:description, andtwitter:card(summary_large_image), inferred from the title and description.twitter:siteandtwitter:creatorfromsite.twitter. The@is added.<html lang>fromsite.currentLocaleorsite.defaultLocale, elseen.og:localeonly when the locale has a region.en-USrendersen_US.enrenders no tag.- Relative
og:imageandtwitter:imagevalues become absolute againstsite.url.
Module tags use tagPriority: 'low', so a page useSeoMeta() or useHead() call replaces them.
A page ogImage also drops the default image's width, height, type, and copied twitter:image. A page ogImageAlt alone keeps the default image.
A page canonical needs no extra priority:
useHead({ link: [{ rel: 'canonical', href: 'https://example.com/preferred' }] })Set seo: { automaticDefaults: false } to drop the canonical, title template, lang, and Open Graph defaults together. No option drops only one of them.
Metadata files
Files in public/ become site-wide head tags at build time:
favicon.{ico,png,svg},icon*.{ico,png,jpg,svg},apple-icon*.png,apple-touch-icon*.png:<link rel="icon">orapple-touch-icon, withtypeandsizesread from the file.og-image.{png,jpg,jpeg,gif}oropengraph-image.*:og:imagewith width, height, and type. It also fillstwitter:imagewhen notwitter-image.*file exists. The name must match exactly;blog-og-image.pngis ignored.<name>.alt.txtnext to an image setsog:image:alt.-darkor-lightin an icon name adds aprefers-color-schememedia query.
The same file names inside pages/<route>/ or pages/<route>/_dir/ apply to that route and its children. They replace a public/ image there.
An icon link in app.head.link with the same rel wins over the files.
Set seo: { metaDataFiles: false } to turn this off.
Generate icon files from one source image in public/:
pnpm exec nuxt-seo-utils icons --source logo.svgIt writes favicon.ico, apple-touch-icon.png, and icon-{16,32,192,512} PNGs to public/. Install the optional peer with pnpm add -D sharp before generating icons.
Site-wide and per-route meta
seo.meta takes useSeoMeta() input and renders on every page:
export default defineNuxtConfig({
seo: { meta: { author: 'Acme Team', themeColor: '#18181b' } },
})routeRules accept seoMeta and head:
export default defineNuxtConfig({
routeRules: {
'/blog/**': { seoMeta: { author: 'Blog Team' }, head: { meta: [{ name: 'x-section', content: 'blog' }] } },
},
})Route rule tags override seo.meta and app.head. A page useSeoMeta() or useHead() call overrides them. The tagPriority option does not change this.
Route rule tags render on the server only. They do not update on client navigation.
Breadcrumbs
useBreadcrumbItems() is auto-imported. It returns a ref of items from the route path, ready for Nuxt UI <UBreadcrumb :items>.
const items = useBreadcrumbItems({
overrides: [undefined, undefined, { label: post.value.title }], // index matches the path segment; false removes it
})- With
nuxt-schema-orginstalled, it also adds aBreadcrumbListnode. Without it, no JSON-LD renders and there is no warning. - Labels come from
definePageMeta({ breadcrumb: { label } }), thentitlein page meta, then the title-cased segment. The index route isHome. - Call it in the page component, not the layout. The page has the data for the last items. Calls in parent components with the same
idmerge their options.
Share links
const share = useShareLinks({ title: post.value.title, utm: { source: 'auto', campaign: 'launch' } })
// share.value.twitter, .facebook, .linkedin, .whatsapp, .telegram, .reddit, .pinterest, .email, .canonicalUrltitle defaults to site.name, not the page title. Pass it.
UTM defaults to utm_source=<platform> and utm_medium=social (or email). An object without source: 'auto' switches to manual mode: { campaign: 'launch' } sends only utm_campaign.
Redirects and gone URLs
seo.redirects lists old URLs, keyed by path without app.baseURL. Every entry needs addedAt.
export default defineNuxtConfig({
seo: {
redirects: {
'/blog/old-post': { to: '/blog/new-post', addedAt: '2026-10-13' }, // 301; set statusCode: 308 to keep the method
'/docs/v1/**': { to: '/docs/**', addedAt: '2026-10-13' }, // /docs/v1/setup -> /docs/setup
'/spring-sale': { statusCode: 410, addedAt: '2026-10-13', reason: 'Sale ended' }, // error page with 410
},
},
})- A moved URL has
to(a path or an absolute URL) andstatusCode301 (default) or 308. A gone URL has notoandstatusCode410 or 404. - The request query string is kept. A trailing slash does not change the match. An exact key beats a
/**key. reasonis a private note. Visitors never see it.- Nothing expires. After
redirectReviewAfterDays(365) fromaddedAtorreviewedAt,nuxt devandnuxt buildlist the entry for review. To keep it, setreviewedAtto today. Delete it only when the old URL gets no visits and no backlinks. - Chains flatten at build:
/ato/band/bto/cserve/ato/c. A loop or an invalid entry stops the build. A redirect to a gone URL warns. - With an i18n prefix strategy, each unprefixed entry also applies under each locale prefix. A translated path needs its own entry.
- For a long list, import a file typed with
satisfies Redirects(import type { Redirects } from 'nuxt-seo-utils'). - Do not redirect deleted pages to the home page. Google can treat that as a soft 404. Use a gone URL.
- Use
routeRulesredirectfor temporary 302 or 307 redirects.seo.redirectsaccepts only permanent ones.
Traps
htmlAttrs.langinuseHead()orapp.headis ignored. The module setslangfrom site config. Setsite.defaultLocale, or use@nuxtjs/i18n.- Before nuxt-schema-org 6.4.1, the Schema.org WebPage URL keeps the route case.
canonicalLowercaselowercases the canonical,og:url,useShareLinks()URLs, andBreadcrumbListitem URLs. nuxt-schema-org before 6.4.1 builds the WebPageurlfrom the route path. There,/blog/My-Postgets a canonical of/blog/my-postand a WebPage URL of/blog/My-Post. From 6.4.1, the WebPageurlfollows the canonical. On an older version, upgrade nuxt-schema-org, use lowercase route paths, or setcanonicalLowercase: false. Breadcrumbtolinks keep their case on purpose. definePageMeta({ title })takes a static string only. UseuseSeoMeta({ title })for data.- Only some static presets get native redirects.
nuxt generatewrites host rules fornetlify-static(_redirects, 308 becomes 301),vercel-static(config.jsonroutes), andcloudflare-pages-static(_redirects, no 410: gone URLs get the host 404 page). Withnetlify-staticand anapp.baseURL, and with every other preset, each exact moved URL becomes an instant meta refresh page, and gone URLs and/**keys need host rules. The build lists each entry the host cannot serve. - A catch-all page skips client-side redirects.
seo.redirectsruns on the server. ANuxtLinkto an old URL that a[...slug].vuepage matches renders that page. Fix the link. Nuxt Link Checker flags a link to a 301 entry.
Version limits
v9 breaking changes and their fixes:
- Nuxt
^4.6.0 || ^5.0.0, Node^22.22.3 || ^24.15.0 || >=26.0.0, and Unhead^3.4.2are required. Upgrade Nuxt and Node first. - Unhead v2 is not supported. Remove any
@unhead/vue2 pin or override. nuxt-site-config5 is required. Change a pinnednuxt-site-configto^5.0.0.- The module no longer installs Sharp. Run
pnpm add -D sharpbeforenuxt-seo-utils icons. - Runtime and prerender minification keep inline JavaScript unchanged. Put scripts that need minification in
app.head. - Only
#seo-utils/appresolves. Replace#seo-utils/types,#seo-utils/server/, and#seo-utils/shared/imports with#seo-utils/appexports. - The DevTools panel needs
nuxtseo-devtools-hostas a dev dependency.
Older limits:
- 8.5.2 and earlier: a page
ogImagekeeps the default image's dimensions andtwitter:image, a page canonical leavesog:urlon the route URL, apages/<route>/og-image.pngrendersog:image:alttwice, andpublic/opengraph-image.*is ignored. - 8.5.2 and earlier:
BreadcrumbListitem URLs keep the route case, anduseShareLinks()lowercases without the site locale. - 8.5.2 and earlier:
seo.metaandpublic/og-image.pngbeat route ruleseoMetaandpages/**/og-image.png. Put the route value in the page withuseSeoMeta(). automaticTwitterTags: false(removestwitter:card) arrived in 8.5.0.
Config
redirectToCanonicalSiteUrl(false): in production, send a 301 from other hosts to thesite.urlhost.canonicalQueryWhitelist: replaces the default list. It does not extend it.redirects: moved and gone URLs. See the section above.redirectReviewAfterDays(365): days until an entry is due for review.falseturns the review off.minify({ build: true, runtime: false }): minify staticapp.headJavaScript/CSS with parser-based tooling. Compact JSON/CSS in prerendered HTML.runtime: truecompacts JSON/CSS per SSR request. Dynamic JavaScript and custom script types remain unchanged.automaticOgAndTwitterTags(true): theog:title,og:description, andtwitter:cardinference.- Other options: https://nuxtseo.com/docs/seo-utils/api/config
Debug
- On
nuxt devandnuxt build, the module warns about conflicts inapp.head, such as a staticlangor anog:site_namethat differs fromsite.name. Fix the site config, not the head. SetvalidateAppHead: falseto hide the check. /__nuxt-seo-utils/debug.jsonin dev, or in production withseo: { debug: true }.- On
nuxt devandnuxt build, the module warns when SSR is off for indexable pages. Turn SSR on for public routes. For private routes, setrobots: falsewith@nuxtjs/robots. - Nuxt DevTools has an SEO Utils tab. The panel needs
nuxtseo-devtools-hostas a dev dependency.