Index of every page on this site: llms.txt
All skills
harlan-zw avatar

/nuxt-seo-utils

@a86cea5
Run it yourself

Set up, override, and debug the default SEO head tags in a Nuxt app with the nuxt-seo-utils module. Use when a task mentions canonical URLs, the title template or separator, fallback titles, og:image or twitter:image files, favicons and app icons, seo.meta, seoMeta or head in routeRules, useBreadcrumbItems, useShareLinks, the nuxt-seo-utils icons CLI, seo.redirects, moved or deleted URLs, 301 or 410 responses, or the seo config key. Also use when a page tag does not override a default, html lang ignores useHead, or og:image renders twice.

Use this Skill: https://skilld.dev/gh/harlan-zw/nuxt-seo-utils/nuxt-seo-utils

Nothing lands on disk. Nothing to clean up.

Fork this Skill

Edit a local copy. It keeps the author and licence.

SKILL.md

≈140 tokens for metadata: the name and description. ≈2.9k when used: this file.

Description uses 7.0% of example budget

Before choosing a Skill, your Agent reads its name and description. All available Skills share that space.

  • A shorter description leaves more room for other Skills. This entry exceeds our 1% size suggestion.

In our Claude Code example, all Skill names and descriptions share 8,000 characters. This Skill uses ≈561 characters, or 7.0%.

The 1% threshold is a size suggestion. Longer descriptions can still fit.

Your model, settings, and other Skills decide how much text your Agent can read.

Example settings and source

The example uses a 200k-token context and default Claude Code settings. The count includes the name, description, separators, and when_to_use when present. Codex also counts local file paths.

Skit's source and limits: Codex 0.160.1, Claude Code 2.1.292.

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 with site.separator.
  • A fallback title from the last path segment, title cased: /blog/my-post becomes My Post. definePageMeta({ title }) sets it with a static string.
  • <link rel="canonical"> from site.url and 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), and description from site.description.
  • og:title, og:description, and twitter:card (summary_large_image), inferred from the title and description.
  • twitter:site and twitter:creator from site.twitter. The @ is added.
  • <html lang> from site.currentLocale or site.defaultLocale, else en.
  • og:locale only when the locale has a region. en-US renders en_US. en renders no tag.
  • Relative og:image and twitter:image values become absolute against site.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"> or apple-touch-icon, with type and sizes read from the file.
  • og-image.{png,jpg,jpeg,gif} or opengraph-image.*: og:image with width, height, and type. It also fills twitter:image when no twitter-image.* file exists. The name must match exactly; blog-og-image.png is ignored.
  • <name>.alt.txt next to an image sets og:image:alt.
  • -dark or -light in an icon name adds a prefers-color-scheme media 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.svg

It 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-org installed, it also adds a BreadcrumbList node. Without it, no JSON-LD renders and there is no warning.
  • Labels come from definePageMeta({ breadcrumb: { label } }), then title in page meta, then the title-cased segment. The index route is Home.
  • 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 id merge 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, .canonicalUrl

title 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) and statusCode 301 (default) or 308. A gone URL has no to and statusCode 410 or 404.
  • The request query string is kept. A trailing slash does not change the match. An exact key beats a /** key.
  • reason is a private note. Visitors never see it.
  • Nothing expires. After redirectReviewAfterDays (365) from addedAt or reviewedAt, nuxt dev and nuxt build list the entry for review. To keep it, set reviewedAt to today. Delete it only when the old URL gets no visits and no backlinks.
  • Chains flatten at build: /a to /b and /b to /c serve /a to /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 routeRules redirect for temporary 302 or 307 redirects. seo.redirects accepts only permanent ones.

Traps

  • htmlAttrs.lang in useHead() or app.head is ignored. The module sets lang from site config. Set site.defaultLocale, or use @nuxtjs/i18n.
  • Before nuxt-schema-org 6.4.1, the Schema.org WebPage URL keeps the route case. canonicalLowercase lowercases the canonical, og:url, useShareLinks() URLs, and BreadcrumbList item URLs. nuxt-schema-org before 6.4.1 builds the WebPage url from the route path. There, /blog/My-Post gets a canonical of /blog/my-post and a WebPage URL of /blog/My-Post. From 6.4.1, the WebPage url follows the canonical. On an older version, upgrade nuxt-schema-org, use lowercase route paths, or set canonicalLowercase: false. Breadcrumb to links keep their case on purpose.
  • definePageMeta({ title }) takes a static string only. Use useSeoMeta({ title }) for data.
  • Only some static presets get native redirects. nuxt generate writes host rules for netlify-static (_redirects, 308 becomes 301), vercel-static (config.json routes), and cloudflare-pages-static (_redirects, no 410: gone URLs get the host 404 page). With netlify-static and an app.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.redirects runs on the server. A NuxtLink to an old URL that a [...slug].vue page 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.2 are required. Upgrade Nuxt and Node first.
  • Unhead v2 is not supported. Remove any @unhead/vue 2 pin or override.
  • nuxt-site-config 5 is required. Change a pinned nuxt-site-config to ^5.0.0.
  • The module no longer installs Sharp. Run pnpm add -D sharp before nuxt-seo-utils icons.
  • Runtime and prerender minification keep inline JavaScript unchanged. Put scripts that need minification in app.head.
  • Only #seo-utils/app resolves. Replace #seo-utils/types, #seo-utils/server/, and #seo-utils/shared/ imports with #seo-utils/app exports.
  • The DevTools panel needs nuxtseo-devtools-host as a dev dependency.

Older limits:

  • 8.5.2 and earlier: a page ogImage keeps the default image's dimensions and twitter:image, a page canonical leaves og:url on the route URL, a pages/<route>/og-image.png renders og:image:alt twice, and public/opengraph-image.* is ignored.
  • 8.5.2 and earlier: BreadcrumbList item URLs keep the route case, and useShareLinks() lowercases without the site locale.
  • 8.5.2 and earlier: seo.meta and public/og-image.png beat route rule seoMeta and pages/**/og-image.png. Put the route value in the page with useSeoMeta().
  • automaticTwitterTags: false (removes twitter:card) arrived in 8.5.0.

Config

  • redirectToCanonicalSiteUrl (false): in production, send a 301 from other hosts to the site.url host.
  • 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. false turns the review off.
  • minify ({ build: true, runtime: false }): minify static app.head JavaScript/CSS with parser-based tooling. Compact JSON/CSS in prerendered HTML. runtime: true compacts JSON/CSS per SSR request. Dynamic JavaScript and custom script types remain unchanged.
  • automaticOgAndTwitterTags (true): the og:title, og:description, and twitter:card inference.
  • Other options: https://nuxtseo.com/docs/seo-utils/api/config

Debug

  • On nuxt dev and nuxt build, the module warns about conflicts in app.head, such as a static lang or an og:site_name that differs from site.name. Fix the site config, not the head. Set validateAppHead: false to hide the check.
  • /__nuxt-seo-utils/debug.json in dev, or in production with seo: { debug: true }.
  • On nuxt dev and nuxt build, the module warns when SSR is off for indexable pages. Turn SSR on for public routes. For private routes, set robots: false with @nuxtjs/robots.
  • Nuxt DevTools has an SEO Utils tab. The panel needs nuxtseo-devtools-host as a dev dependency.

Source: SKILL.md on GitHub

skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.

skilld run checks every file with the same patterns. It asks for approval before it loads a Skill with a behavior marked Needs approval.

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides documentation and configuration instructions for the `nuxt-seo-utils` module, which is used to manage SEO metadata in Nuxt applications. The analysis found no evidence of malicious behavior, obfuscation, or unauthorized data access. The skill describes standard development workflows, such as installing packages from public registries and using CLI tools to generate website icons.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at a86cea5. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated last month
Other metadata
compatibility
Requires Node.js ^22.22.3 || ^24.15.0 || >=26.0.0. Requires Nuxt ^4.6.0 || ^5.0.0. Requires Unhead ^3.4.2.

README badge

README badge for harlan-zw/nuxt-seo-utils