@cooperco/nuxt-layer-seo
v1.1.0
Published
SEO Nuxt layer for cooperco projects
Downloads
199
Readme
Nuxt SEO Layer
A Nuxt layer that provides the full Nuxt SEO suite plus Nuxt AI Ready for Nuxt projects, plus automated linting.
Features
The layer registers each of the @nuxtjs/seo bundled sub-modules directly in its modules array, alongside AI Ready and its MCP toolkit:
@nuxtjs/sitemap— XML sitemap generation@nuxtjs/robots— robots.txt configurationnuxt-og-image— Open Graph image generationnuxt-schema-org— Schema.org structured datanuxt-link-checker— broken link detectionnuxt-seo-utils— meta tag utilitiesnuxt-site-config— shared site config (site.url,site.name, etc.)nuxt-ai-ready— generates/llms.txtand/llms-full.txtfor LLM crawlers and exposes an MCP surface (list_pages,search_pages) that AI agents can query. Per-page markdown companions (e.g./about.md) are emitted automatically when pages are prerendered.@nuxtjs/mcp-toolkit— runtime MCP server that hosts the AI Ready tools/resources at/mcpover Streamable HTTP. Shipped alongside AI Ready so the MCP surface activates out of the box.- ESLint integration with stylistic rules
Why register each
@nuxtjs/seosub-module explicitly instead of just listing@nuxtjs/seo? The@nuxtjs/seometa-module uses a declarativemoduleDependenciesfield to tell Nuxt which sub-modules to load. In practice, that auto-loading does not kick in when the meta-module is registered from within a Nuxt layer'smodulesarray — no auto-imports are registered anddefineSitemapEventHandleris not found at build time. Enumerating the sub-modules directly sidesteps the issue.nuxt-ai-readyand@nuxtjs/mcp-toolkitare not meta-modules, so they're registered normally.
Configuration
The SEO layer's own nuxt.config.ts:
// nuxt.config.ts in the seo layer
export default defineNuxtConfig({
modules: [
'@nuxtjs/sitemap',
'@nuxtjs/robots',
'nuxt-og-image',
'nuxt-schema-org',
'nuxt-link-checker',
'nuxt-seo-utils',
'nuxt-site-config',
'nuxt-ai-ready',
'@nuxtjs/mcp-toolkit',
'@nuxt/eslint',
],
eslint: {
config: {
stylistic: true
}
}
})Consumer apps should set site.url (and optionally site.name) in their own nuxt.config.ts so the modules can generate absolute URLs:
export default defineNuxtConfig({
extends: ['@cooperco/nuxt-layer-seo'],
site: {
url: 'https://example.com',
name: 'My Site'
}
})Consumer app setup: known gotchas
Three things a consuming app needs beyond just extends-ing the layer:
1. Vite SSR externals (required — production build will 500 without this)
Vue's @vue/compiler-core CJS build does require('entities/decode'), which Rollup rewrites to an ESM default import. entities@^7.0.1 dropped default exports from its subpath ESM builds, so the bundled production server crashes at import time with "does not provide an export named 'default'". Fix: externalize the Vue compiler packages so Node handles the CJS/ESM interop at runtime.
// consumer app nuxt.config.ts
export default defineNuxtConfig({
extends: ['@cooperco/nuxt-layer-seo'],
vite: {
ssr: {
external: ['@vue/compiler-core', '@vue/compiler-dom', '@vue/compiler-ssr'],
},
},
nitro: {
externals: {
external: ['entities', 'entities/decode', 'entities/escape'],
},
},
})2. nuxt-og-image renderer + ejected template (required for OG image generation)
nuxt-og-image needs at least one image renderer. The layer ships @takumi-rs/core as a runtime dependency, so the takumi renderer resolves automatically for every consumer — no manual install needed on your end.
The bundled "community" templates (e.g., NuxtSeo, BlogPost) are dev-only, however. For production builds you must eject the template you want to use — copy it from the module into your app's components/OgImage/ directory so the build compiles it as a first-party component:
cp node_modules/nuxt-og-image/dist/runtime/app/components/Templates/Community/NuxtSeo.takumi.vue \
app/components/OgImage/NuxtSeo.takumi.vueThen call defineOgImage with the v6 API (the old object syntax was removed — see nuxt-modules/og-image#495):
// any page
defineOgImage('NuxtSeo', {
title: 'Your Page Title',
description: 'Your Page Description',
})3. AI Ready endpoints and the dev/prod page index
Once the layer is extended, the following endpoints are live:
/llms.txt— canonical LLM discovery manifest for the site (overview, sitemap links, MCP URL)./llms-full.txt— full markdown content of every indexed page, for retrieval pipelines./<route>.md— per-page markdown companion for any prerendered route (e.g./about.md,/index.md). Written at build time as pages are prerendered./mcp— Model Context Protocol endpoint exposinglist_pagesandsearch_pagestools and a pages resource. Reachable viaPOST /mcpwithAccept: application/json, text/event-stream.
AI Ready populates its page index at prerender time, which means:
- Dev mode (
nuxt dev): endpoints are reachable but the page index is empty —/llms.txtshows the structure and a "dev mode - runnuxi generatefor page titles" note. This is expected. - Production build (
nuxt build+ normal Nitro deploy):.output/server/package.jsondeclares the SQLite driver that AI Ready needs at runtime. Runningnpm install --productioninside.output/server/on the deploy host (the standard Nitro deployment pattern) installs the driver and the endpoints work fully. You never need to installbetter-sqlite3in your own app — Nitro's deploy flow handles it. - Static generation (
nuxt generate):/llms.txt,/llms-full.txt, and all*.mdcompanions are emitted as static files during the prerender pass.
4. ESLint link-checker rules (optional — disable if using a custom router)
nuxt-link-checker ships two ESLint rules (link-checker/valid-route, link-checker/valid-sitemap-link) that statically validate <NuxtLink to="…"> and <a href="…"> values against the app's route manifest. If your app uses a custom router.options.ts instead of file-based routing, the rules can't discover your routes and false-positive on valid links. Disable them:
// eslint.config.mjs
export default withNuxt({
rules: {
'link-checker/valid-route': 'off',
'link-checker/valid-sitemap-link': 'off',
},
})The runtime link-checker scanner (visible in Nuxt Devtools) still works.
Development
# Install dependencies
npm install
# Start development server
npm run dev
# Run ESLint
npm run lint
# Fix linting issues automatically
npm run lint:fix
# Run TypeScript type checking
npm run typecheckUsage
To use this layer in your Nuxt project:
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'@cooperco/nuxt-layer-seo'
]
})This will automatically include the SEO layer features.
Dependencies
All of the layer's runtime Nuxt modules live in dependencies (not devDependencies), so downstream consumers inherit them transitively when they install @cooperco/nuxt-layer-seo — no manual installs needed on the consumer side:
@nuxtjs/seo— the Nuxt SEO umbrella, which brings all seven sub-modules (sitemap, robots, og-image, schema-org, link-checker, seo-utils, site-config) as transitive deps.nuxt-ai-ready— AI/LLM discoverability module (llms.txt, llms-full.txt, per-page.mdcompanions, MCP tool/resource definitions).@nuxtjs/mcp-toolkit— runtime MCP server that hosts AI Ready's tools and resources at/mcp.@takumi-rs/core— image renderer backend used bynuxt-og-imagefor OG image generation.@nuxt/eslint— ESLint integration for the consumer'seslint.config.mjs.
The layer's own build-time deps (nuxt, vue, vue-tsc, typescript, eslint, @types/node) remain in devDependencies — they're only used to build and lint this layer's own source code in isolation, never by downstream consumers.
Together these provide a solid SEO + AI discoverability foundation for your Nuxt application.
Claude Code Skills
This repo includes Claude Code skills for common tasks in apps that use these layers. To use them, copy the relevant skill files from the skills/ directory into your app's .claude/skills/ directory.
Skills provided by the SEO layer:
| Skill | File | Description |
|-------|------|-------------|
| /add-sitemap-source | skills/add-sitemap-source.md | Add dynamic sitemap URL sources with defineSitemapEventHandler and configure sitemap.sources |
# Copy SEO layer skills into your app
mkdir -p .claude/skills
cp node_modules/@cooperco/nuxt-layer-seo/../../skills/add-sitemap-source.md .claude/skills/Or copy them directly from the nuxt-layers repository.
Publishing to npm (tag-based)
This layer is published using GitHub Actions when you push a tag that matches the seo-vX.Y.Z pattern.
High-level flow:
- Bump the version in
layers/seo/package.json(SemVer). - Commit and push your changes to main (or ensure the commit is on main).
- Create and push a tag named
seo-vX.Y.Z(matching thepackage.jsonversion). - The Publish SEO Layer workflow installs deps, checks if that exact version already exists on npm, and if not, publishes to npm.
Important notes:
- Do NOT rely on
npm versioncreating a tag for you (it will createvX.Y.Z). We use a custom tag prefixseo-v. - The workflow will skip if the version already exists.
Step-by-step
- Bump the version in
layers/seo/package.json
- Option A (recommended): use npm version without creating a tag
- Bash:
cd layers/seo npm version patch --no-git-tag-version # or minor | major - PowerShell:
Set-Location layers/seo npm version patch --no-git-tag-version # or minor | major
- Bash:
- Option B: manually edit the version field in
layers/seo/package.json(SemVer: MAJOR.MINOR.PATCH)
- Commit and push the change (from repo root or
layers/seo)
git add layers/seo/package.json
git commit -m "chore(seo): bump version"
git push origin main- Create and push the tag using the new version
- Get the new version value:
- Bash:
cd layers/seo VERSION=$(node -p "require('./package.json').version") cd ../.. git tag "seo-v$VERSION" git push origin "seo-v$VERSION" - PowerShell:
Set-Location layers/seo $version = node -p "require('./package.json').version" Set-Location ../.. git tag "seo-v$version" git push origin "seo-v$version"
- Bash:
- GitHub Actions will publish
- Workflow:
.github/workflows/publish-seo.yml - Auth: uses
NPM_TOKENconfigured as a GitHub secret - Behavior: installs, checks
npm view @cooperco/nuxt-layer-seo@<version>, publishes if not found
Troubleshooting
- Version already exists: bump the version again (patch/minor/major) and push a new tag.
- Auth errors (401/403): ensure
NPM_TOKENis set in repo secrets and org access is correct.
