fix: convert apps/website from submodule to regular directory

This commit is contained in:
yeuimu
2026-07-16 01:42:45 +08:00
parent f12ae12ad3
commit 9595030625
341 changed files with 50808 additions and 1 deletions
@@ -0,0 +1,88 @@
---
name: nuxt-seo
description: Nuxt SEO meta-module with robots, sitemap, og-image, schema-org. Use when configuring SEO, generating sitemaps, creating OG images, or adding structured data.
license: MIT
---
# Nuxt SEO
```bash
npx nuxi module add @nuxtjs/seo
```
## When to Use
Working with:
- SEO configuration (site URL, name, indexability)
- Robots.txt and sitemap.xml generation
- Dynamic OG image generation
- JSON-LD structured data (schema.org)
- Breadcrumbs and canonical URLs
## Loading Files
**Consider loading these reference files based on your task:**
- [ ] [references/site-config.md](references/site-config.md) - if configuring site URL, name, or SEO foundation
- [ ] [references/crawlability.md](references/crawlability.md) - if setting up robots.txt or sitemap.xml
- [ ] [references/og-image.md](references/og-image.md) - if generating dynamic OG images
- [ ] [references/schema-org.md](references/schema-org.md) - if adding JSON-LD structured data
- [ ] [references/utilities.md](references/utilities.md) - if working with breadcrumbs, canonical URLs, or link checking
**DO NOT load all files at once.** Load only what's relevant to your current task.
## Site Config
Foundation for all SEO modules. Configure `site` in `nuxt.config.ts`, access via `useSiteConfig()`. See [references/site-config.md](references/site-config.md) for full options.
## Module Overview
| Module | Purpose | Key API |
| ----------------- | --------------- | ----------------------------- |
| nuxt-site-config | Shared config | `useSiteConfig()` |
| @nuxtjs/robots | robots.txt | `useRobotsRule()` |
| @nuxtjs/sitemap | sitemap.xml | `defineSitemapEventHandler()` |
| nuxt-og-image | OG images | `defineOgImage()` |
| nuxt-schema-org | JSON-LD | `useSchemaOrg()` |
| nuxt-seo-utils | Meta utilities | `useBreadcrumbItems()` |
| nuxt-link-checker | Link validation | Build-time checks |
## Nuxt Content v3
Use `asSeoCollection()` for automatic sitemap, og-image, and schema-org from frontmatter:
```ts
// content.config.ts
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'
export default defineContentConfig({
collections: {
posts: defineCollection(asSeoCollection({ type: 'page', source: 'posts/**' }))
}
})
```
**Important:** Load `@nuxtjs/seo` before `@nuxt/content` in modules array:
```ts
export default defineNuxtConfig({
modules: ['@nuxtjs/seo', '@nuxt/content']
})
```
Frontmatter fields: `ogImage`, `sitemap`, `robots`, `schemaOrg`.
## Related Skills
- [nuxt-content](../nuxt-content/SKILL.md) - For MDC rendering with SEO frontmatter
## Links
- [Documentation](https://nuxtseo.com)
- [GitHub](https://github.com/harlan-zw/nuxt-seo)
## Token Efficiency
Main skill: ~250 tokens. Each sub-file: ~400-600 tokens. Only load files relevant to current task.
@@ -0,0 +1,153 @@
# Crawlability: Robots & Sitemap
## Robots.txt
Auto-generated at `/robots.txt`. Respects `site.indexable` setting.
### Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
robots: {
// Block AI crawlers
blockAiBots: true,
// Block non-SEO bots (reduces server load)
blockNonSeoBots: true,
// Custom rules
groups: [
{ userAgent: '*', disallow: ['/admin'] }
]
}
})
```
### Per-Page Control
```ts
// Disable indexing
useRobotsRule('noindex, nofollow')
// Object syntax with AI directives
useRobotsRule({
noindex: true,
nofollow: true,
noai: true, // Block AI training
noimageai: true, // Block AI image training
'max-snippet': 150, // Preview controls
'max-image-preview': 'large'
})
```
Route rules:
```ts
export default defineNuxtConfig({
routeRules: {
'/admin/**': { robots: 'noindex, nofollow' },
'/hidden': { robots: false }
}
})
```
### Nuxt Content Frontmatter
```yaml
---
robots: noindex, nofollow
# Or structured:
robots:
noindex: true
nofollow: true
---
```
## Sitemap.xml
Auto-generated at `/sitemap.xml` from app routes.
### Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
sitemap: {
sources: ['/api/__sitemap__/urls'],
exclude: ['/admin/**', '/secret'],
// For static sites - no runtime generation
zeroRuntime: true
}
})
```
### Dynamic URLs via API
```ts
// server/api/__sitemap__/urls.ts
import { defineSitemapEventHandler } from '#imports'
import type { SitemapUrlInput } from '#sitemap/types'
export default defineSitemapEventHandler(async () => {
const posts = await $fetch('/api/posts')
return posts.map(post => ({
loc: post.path,
lastmod: post.updatedAt,
// Image sitemap
images: [{ loc: post.image, title: post.title }],
// Video sitemap
videos: [{ content_loc: post.videoUrl, title: post.title }]
} satisfies SitemapUrlInput))
})
```
### Per-Page Control
Route rules:
```ts
export default defineNuxtConfig({
routeRules: {
'/blog/**': { sitemap: { changefreq: 'daily', priority: 0.9 } },
'/hidden': { sitemap: false }
}
})
```
Nuxt Content frontmatter:
```yaml
---
sitemap:
changefreq: weekly
priority: 0.8
lastmod: 2025-01-15
---
```
### Multiple Sitemaps
For large sites:
```ts
export default defineNuxtConfig({
sitemap: {
sitemaps: {
pages: { include: ['/**'], exclude: ['/blog/**'] },
blog: { include: ['/blog/**'] }
}
}
})
```
Generates `/pages-sitemap.xml`, `/blog-sitemap.xml`, and `/sitemap_index.xml`.
### i18n Sitemaps
With `@nuxtjs/i18n`, auto-generates per-locale sitemaps with `hreflang` alternates.
## Debug
In development:
- Robots: Check `/robots.txt` directly
- Sitemap: Visit `/__sitemap__/debug.json` for raw data
@@ -0,0 +1,170 @@
# OG Image Generation
Dynamic Open Graph image generation using Vue components.
## Quick Start
```ts
// Component-first (recommended)
defineOgImage('NuxtSeo', { title: 'My Page Title' })
// Object syntax
defineOgImage({ component: 'NuxtSeo', title: 'My Page Title' })
// Disable OG image
defineOgImage(false)
```
## Built-in Template
The `NuxtSeo` template supports:
```ts
defineOgImage('NuxtSeo', {
title: 'Hello World',
description: 'My description',
theme: '#3b82f6',
colorMode: 'dark',
icon: 'carbon:cloud',
siteName: 'My Site',
siteLogo: '/logo.png'
})
```
## Multiple Images Per Page
Use `key` for platform-specific images:
```ts
// Default OG image (1200x600)
defineOgImage('NuxtSeo', { title: 'Default' })
// Square for WhatsApp (800x800)
defineOgImage('NuxtSeo', {
title: 'Square',
key: 'square',
width: 800,
height: 800
})
```
## Custom Vue Components
Create in `components/OgImage/`:
```vue
<!-- components/OgImage/Blog.vue -->
<script setup lang="ts">
defineProps<{ title: string; author: string }>()
</script>
<template>
<div class="w-full h-full flex flex-col justify-center items-center bg-gradient-to-br from-blue-500 to-purple-600 p-12">
<h1 class="text-6xl font-bold text-white text-center">{{ title }}</h1>
<p class="text-2xl text-white/80 mt-4">By {{ author }}</p>
</div>
</template>
```
Use in pages:
```ts
defineOgImage('OgImageBlog', { title: 'My Post', author: 'John' })
```
## Renderers
| Renderer | Speed | CSS Support | Edge | Best For |
| -------- | ----- | ----------- | ---- | -------------------------- |
| satori | Fast | Partial | ✅ | Default, most templates |
| chromium | Slow | Full | ❌ | Complex designs, prerender |
```ts
export default defineNuxtConfig({
ogImage: {
defaults: { renderer: 'satori' }
}
})
```
### Satori Limitations
- No `display: grid` - use `flex`
- No `position: absolute` without explicit dimensions
- Fonts: use `@nuxt/fonts` with `global: true` for best results
## Configuration
```ts
export default defineNuxtConfig({
ogImage: {
defaults: {
component: 'NuxtSeo',
width: 1200,
height: 600,
cacheMaxAgeSeconds: 60 * 60 * 24 * 3 // 3 days
},
// For static sites
zeroRuntime: true
}
})
```
## Nuxt Content
Frontmatter:
```yaml
---
ogImage:
component: OgImageBlog
props:
author: John Doe
---
```
With `asSeoCollection()` (see main SKILL.md):
```vue
<script setup>
const { data: page } = await useAsyncData(() => queryCollection('posts').path(route.path).first())
if (page.value?.ogImage)
defineOgImage(page.value.ogImage)
</script>
```
## Debug
- Preview: `/__og-image__/image/[path]/og.png`
- Inspector: Enable `ogImage: { debug: true }` in config
## Screenshots
Capture page as OG image (requires Chromium):
```ts
defineOgImageScreenshot({
colorScheme: 'dark',
mask: '.navigation, .footer',
selector: '.article-content'
})
```
## Route Rules
```ts
export default defineNuxtConfig({
routeRules: {
'/blog/**': { ogImage: { component: 'OgImageBlog' } },
'/admin/**': { ogImage: false }
}
})
```
## Deployment
Community templates are dev-only. Before deploying, eject:
```bash
npx nuxt-og-image eject NuxtSeo
```
@@ -0,0 +1,182 @@
# Schema.org Structured Data
JSON-LD structured data for rich search results.
## Site Identity
Configure once in `nuxt.config.ts`:
```ts
import { defineOrganization } from 'nuxt-schema-org/schema'
export default defineNuxtConfig({
schemaOrg: {
identity: defineOrganization({
name: 'My Company',
url: 'https://example.com',
logo: '/logo.png',
sameAs: ['https://twitter.com/mycompany', 'https://github.com/mycompany']
})
}
})
```
For personal sites:
```ts
import { definePerson } from 'nuxt-schema-org/schema'
export default defineNuxtConfig({
schemaOrg: {
identity: definePerson({
name: 'John Doe',
url: 'https://johndoe.com',
image: '/avatar.jpg',
sameAs: ['https://twitter.com/johndoe']
})
}
})
```
## Page-Level Schema
Define functions are **auto-imported** in components (no import needed):
```ts
// Article page
useSchemaOrg([
defineArticle({
headline: 'My Article Title',
description: 'Article description',
image: '/article-image.jpg',
datePublished: '2025-01-15',
dateModified: '2025-01-20',
author: { name: 'John Doe', url: 'https://johndoe.com' }
})
])
```
```ts
// Product page (include url in offers for Google validation)
useSchemaOrg([
defineProduct({
name: 'Product Name',
description: 'Product description',
image: '/product.jpg',
offers: {
price: 99.99,
priceCurrency: 'USD',
availability: 'InStock',
url: 'https://example.com/product'
}
})
])
```
## Define Functions
| Function | Use Case |
| ----------------------- | ---------------------- |
| `defineArticle()` | Blog posts, news |
| `defineProduct()` | E-commerce products |
| `defineFAQPage()` | FAQ pages |
| `defineHowTo()` | Tutorial/guide pages |
| `defineRecipe()` | Recipe pages |
| `defineEvent()` | Events |
| `defineLocalBusiness()` | Business info |
| `defineVideo()` | Video content |
| `defineBreadcrumb()` | Breadcrumb navigation |
| `defineWebPage()` | Generic page |
| `defineWebSite()` | Site-wide (auto-added) |
| `defineJobPosting()` | Job listings |
| `defineSoftwareApp()` | Software/apps |
| `defineService()` | Services |
## Data Inference
Module auto-infers from page head:
- `title` → WebPage name
- `description` → WebPage description
- `og:image` → WebPage image
## Breadcrumbs
Auto-generated from route path, or customize:
```ts
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
{ name: 'Home', item: '/' },
{ name: 'Blog', item: '/blog' },
{ name: 'My Post', item: '/blog/my-post' }
]
})
])
```
Or use the `useBreadcrumbItems()` composable (from seo-utils):
```ts
const items = useBreadcrumbItems()
useSchemaOrg([defineBreadcrumb({ itemListElement: items })])
```
## FAQ Page
```ts
useSchemaOrg([
defineFAQPage({
mainEntity: [
{ name: 'What is your return policy?', acceptedAnswer: 'You can return within 30 days.' },
{ name: 'How do I contact support?', acceptedAnswer: 'Email us at support@example.com' }
]
})
])
```
## Nuxt Content
Frontmatter:
```yaml
---
title: My Article
schemaOrg:
- type: BlogPosting
headline: My Article
datePublished: 2025-01-15
author:
type: Person
name: John Doe
---
```
With `asSeoCollection()` (see main SKILL.md), ensure schema renders:
```vue
<script setup>
const { data: page } = await useAsyncData(() => queryCollection('posts').path(route.path).first())
useHead(page.value?.head || {})
</script>
```
## Debug & Validation
- Debug endpoint: `/__schema-org__/debug.json` in dev
- Config: `schemaOrg: { debug: true }`
- [Google Rich Results Test](https://search.google.com/test/rich-results)
- [Schema.org Validator](https://validator.schema.org/)
## Route Rules
```ts
export default defineNuxtConfig({
routeRules: {
'/blog/**': {
schemaOrg: { type: 'Article' }
}
}
})
```
@@ -0,0 +1,101 @@
# Site Config
Foundation module providing shared configuration for all SEO modules.
## Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
site: {
url: 'https://example.com', // Required for absolute URLs
name: 'My Site', // Site name (used in titles, schema)
description: 'Site description', // Default meta description
defaultLocale: 'en', // Default language
indexable: true, // Allow search engine indexing
trailingSlash: false, // URL trailing slash preference
}
})
```
## Environment-Based Indexing
Control indexing per environment using `NUXT_SITE_*` env vars:
```bash
# .env.production
NUXT_SITE_URL=https://example.com
NUXT_SITE_ENV=production
# .env.staging
NUXT_SITE_URL=https://staging.example.com
NUXT_SITE_ENV=staging
```
The module auto-detects `env` and sets `indexable: false` for non-production environments.
For explicit control:
```ts
export default defineNuxtConfig({
site: {
url: process.env.NUXT_SITE_URL,
// Explicit: only index when explicitly set to 'true'
indexable: process.env.NUXT_SITE_INDEXABLE === 'true'
}
})
```
**Note:** `!== 'false'` defaults to `true` when env var is undefined - use `=== 'true'` for fail-safe behavior.
## Runtime Access
```ts
const site = useSiteConfig()
console.log(site.url, site.name, site.description)
```
Works in components, composables, and server routes.
## i18n Integration
Automatically integrates with `@nuxtjs/i18n`:
```ts
export default defineNuxtConfig({
site: {
url: 'https://example.com',
defaultLocale: 'en',
},
i18n: {
locales: [
{ code: 'en', language: 'en-US' },
{ code: 'fr', language: 'fr-FR' },
]
}
})
```
Locale-specific overrides in `site` object:
```ts
site: {
name: 'My Site',
locales: {
fr: { name: 'Mon Site' }
}
}
```
## Override Per-Page
Use route rules for page-specific config:
```ts
export default defineNuxtConfig({
routeRules: {
'/admin/**': { site: { indexable: false } },
'/fr/**': { site: { name: 'Mon Site', defaultLocale: 'fr' } }
}
})
```
@@ -0,0 +1,203 @@
# SEO Utilities
Additional utilities from nuxt-seo-utils and nuxt-link-checker.
## Canonical URLs
Automatic canonical URLs based on site config.
```ts
export default defineNuxtConfig({
seoUtils: {
canonicalQueryWhitelist: ['page', 'sort'], // Keep these query params
redirectToCanonicalSiteUrl: true // 301 to canonical domain
}
})
```
Override per-page:
```ts
useHead({
link: [{ rel: 'canonical', href: 'https://example.com/preferred-url' }]
})
```
## Breadcrumbs
Generate breadcrumb items from current route:
```ts
const items = useBreadcrumbItems()
// [{ label: 'Home', to: '/' }, { label: 'Blog', to: '/blog' }, { label: 'My Post' }]
```
For schema.org integration, see [schema-org.md](schema-org.md#breadcrumbs).
Render in template:
```vue
<template>
<nav aria-label="Breadcrumb">
<ol class="flex gap-2">
<li v-for="(item, i) in items" :key="i">
<NuxtLink v-if="item.to" :to="item.to">{{ item.label }}</NuxtLink>
<span v-else>{{ item.label }}</span>
</li>
</ol>
</nav>
</template>
```
Customize labels in route meta:
```ts
// pages/blog/[slug].vue
definePageMeta({
breadcrumb: { label: 'Article' }
})
```
## Title Templates
Set site-wide title template:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
titleTemplate: '%s | My Site'
}
}
})
```
Override per-page:
```ts
useHead({
title: 'Page Title',
titleTemplate: '%s - Different Template'
})
```
## Meta Defaults
```ts
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
meta: [
{ name: 'author', content: 'My Name' },
{ property: 'og:site_name', content: 'My Site' }
]
}
}
})
```
## Link Checker
Build-time validation of links.
```ts
export default defineNuxtConfig({
linkChecker: {
failOnError: true, // Default: fail build on errors
exclude: ['/api/**'],
skipInspections: ['missing-hash'],
report: { html: true } // Generate HTML report
}
})
```
**Inspections:**
- `no-error-response` - 404/500 errors
- `no-baseless` - Missing base URL
- `no-javascript` - javascript: links
- `trailing-slash` - Inconsistent slashes
- `missing-hash` - Invalid anchor targets
- `no-uppercase-chars` - URL casing
- `absolute-site-urls` - Hardcoded domain
### Ignoring Links
```html
<a href="/maybe-broken" data-link-checker-ignore>Link</a>
```
## File-Based Icons
Place favicon files in `public/`:
```
public/
├── favicon.ico
├── favicon.svg # Modern browsers
├── apple-touch-icon.png
└── site.webmanifest
```
Auto-detected and added to `<head>`.
For SVG favicon with dark mode support:
```svg
<!-- public/favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #fff; }
}
</style>
<path d="..."/>
</svg>
```
## Social Meta Tags
Automatic Open Graph and Twitter cards from site config (see [site-config.md](site-config.md)).
Override per-page:
```ts
useSeoMeta({
title: 'Page Title',
description: 'Page description',
ogImage: '/images/page-og.png',
twitterCard: 'summary_large_image'
})
```
## Trailing Slash Redirect
Enforce consistent URLs:
```ts
export default defineNuxtConfig({
site: {
trailingSlash: false // Redirect /blog/ to /blog
}
})
```
## Debug Panel
Enable comprehensive debug panel:
```ts
export default defineNuxtConfig({
seo: { debug: true }
})
```
Shows in dev:
- Current meta tags
- Schema.org data
- OG image preview
- Sitemap/robots status