AgentSkillsCN

nuxt-website

构建并维护 Vue 3/Nuxt 4 营销网站。当您需要处理网站页面、组件、布局、SEO、Nuxt 配置,或当用户提及“网站”、“营销站点”、“着陆页”、“文档站点”、“Vue”或“Nuxt”时,可灵活运用此工具。

SKILL.md
--- frontmatter
name: nuxt-website
description: Build and maintain Vue 3/Nuxt 4 marketing websites. Use when working with website pages, components, layouts, SEO, Nuxt configuration, or when user mentions 'website', 'marketing site', 'landing page', 'docs site', Vue, or Nuxt.

When to use this skill

  • User asks to update, modify, or build the website/marketing site/landing page/docs site
  • Working with Vue 3 components or Nuxt 4 features in the website directory
  • Creating or editing website pages, layouts, or UI components
  • Managing website content, SEO, meta tags, or Open Graph data
  • Configuring Nuxt plugins, modules, middleware, or nuxt.config.ts
  • Working with Nuxt composables, auto-imports, or data fetching (useFetch, useAsyncData)
  • Optimizing website performance, images, or assets
  • Setting up or modifying website deployment configurations
  • Implementing forms, navigation, or interactive features on the website
  • Working with API routes in the server directory
  • User explicitly mentions "website", "marketing site", "landing page", "docs", "Vue", "Nuxt", or references website-specific files/paths

What this skill does

This skill provides comprehensive guidance for building and maintaining marketing websites using Vue 3 and Nuxt 4, including:

  • Component development and composition
  • Page and layout management
  • Content management and SEO optimization
  • Nuxt configuration and module setup
  • Performance optimization
  • Deployment strategies

Nuxt 4 Project Structure

code
website/
├── .nuxt/                 # Generated by Nuxt (ignore)
├── .output/               # Build output (ignore)
├── node_modules/          # Dependencies (ignore)
├── assets/                # Uncompiled assets (CSS, images, fonts)
│   ├── css/              # Global styles, variables
│   ├── images/           # Image assets
│   └── fonts/            # Custom fonts
├── components/           # Vue components (auto-imported)
│   ├── ui/              # UI components (Button, Card, etc.)
│   ├── layout/          # Layout components (Header, Footer, etc.)
│   └── content/         # Content components (Hero, Features, etc.)
├── composables/         # Composable functions (auto-imported)
│   └── useApi.ts        # Example: API composable
├── layouts/             # Layout templates
│   └── default.vue      # Default layout
├── middleware/          # Route middleware
├── pages/               # File-based routing
│   ├── index.vue        # Home page (/)
│   ├── about.vue        # About page (/about)
│   └── [...slug].vue    # Catch-all route
├── plugins/             # Nuxt plugins
├── public/              # Static assets (served as-is)
│   ├── favicon.ico
│   └── robots.txt
├── server/              # Server routes and middleware
│   ├── api/            # API endpoints
│   └── middleware/     # Server middleware
├── utils/               # Utility functions (auto-imported)
├── app.vue              # Root component
├── nuxt.config.ts       # Nuxt configuration
├── package.json         # Dependencies
└── tsconfig.json        # TypeScript config

Component Development

Component File Structure

Use the Composition API with <script setup>:

vue
<script setup lang="ts">
// Props
interface Props {
  title: string
  description?: string
}

const props = withDefaults(defineProps<Props>(), {
  description: ''
})

// Composables (auto-imported)
const router = useRouter()
const route = useRoute()

// Local state
const isVisible = ref(false)

// Computed
const fullTitle = computed(() => `${props.title} - Marketing`)

// Methods
const handleClick = () => {
  isVisible.value = !isVisible.value
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div class="component">
    <h1>{{ fullTitle }}</h1>
    <p v-if="description">{{ description }}</p>
    <button @click="handleClick">Toggle</button>
  </div>
</template>

<style scoped>
.component {
  padding: 2rem;
}

h1 {
  font-size: 2rem;
  font-weight: bold;
}
</style>

Component Naming

  • Use PascalCase for component files: HeroSection.vue, FeatureCard.vue
  • Auto-import uses PascalCase in templates: <HeroSection />, <FeatureCard />
  • Organize by type: components/ui/, components/layout/, components/content/

Component Best Practices

  1. Props: Use TypeScript interfaces for type safety
  2. Emits: Define emits explicitly with TypeScript
  3. Slots: Use named slots for flexibility
  4. Composables: Extract reusable logic to composables
  5. Auto-imports: All components in components/ are auto-imported
  6. Scoped styles: Use <style scoped> to avoid style conflicts

Page Development

Creating Pages

Pages in pages/ directory are automatically routed:

vue
<!-- pages/index.vue -> / -->
<script setup lang="ts">
// SEO with useHead
useHead({
  title: 'Home - Marketing Site',
  meta: [
    { name: 'description', content: 'Welcome to our marketing site' },
    { property: 'og:title', content: 'Home - Marketing Site' },
    { property: 'og:description', content: 'Welcome to our marketing site' }
  ]
})

// Data fetching
const { data: features } = await useFetch('/api/features')
</script>

<template>
  <div>
    <HeroSection 
      title="Welcome to Our Product"
      subtitle="The best solution for your needs"
    />
    <FeaturesList :features="features" />
    <CallToAction />
  </div>
</template>

Dynamic Routes

code
pages/
├── blog/
│   ├── index.vue          # /blog
│   ├── [slug].vue         # /blog/:slug
│   └── [...slug].vue      # /blog/* (catch-all)
└── products/
    └── [id].vue           # /products/:id

Example dynamic page:

vue
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug

const { data: post } = await useFetch(`/api/blog/${slug}`)

useHead({
  title: `${post.value?.title} - Blog`,
  meta: [
    { name: 'description', content: post.value?.excerpt }
  ]
})
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
</template>

SEO and Meta Tags

Using useHead

vue
<script setup lang="ts">
useHead({
  title: 'Page Title',
  titleTemplate: '%s - Marketing Site', // Optional template
  meta: [
    { name: 'description', content: 'Page description' },
    { name: 'keywords', content: 'keyword1, keyword2' },
    // Open Graph
    { property: 'og:title', content: 'Page Title' },
    { property: 'og:description', content: 'Page description' },
    { property: 'og:image', content: '/images/og-image.jpg' },
    { property: 'og:url', content: 'https://example.com/page' },
    // Twitter
    { name: 'twitter:card', content: 'summary_large_image' },
    { name: 'twitter:title', content: 'Page Title' },
    { name: 'twitter:description', content: 'Page description' },
    { name: 'twitter:image', content: '/images/twitter-image.jpg' }
  ],
  link: [
    { rel: 'canonical', href: 'https://example.com/page' }
  ]
})
</script>

useSeoMeta Composable

Preferred for SEO (better type safety):

vue
<script setup lang="ts">
useSeoMeta({
  title: 'Page Title',
  description: 'Page description',
  ogTitle: 'Page Title',
  ogDescription: 'Page description',
  ogImage: '/images/og-image.jpg',
  ogUrl: 'https://example.com/page',
  twitterCard: 'summary_large_image',
  twitterTitle: 'Page Title',
  twitterDescription: 'Page description',
  twitterImage: '/images/twitter-image.jpg'
})
</script>

Layouts

Creating Layouts

vue
<!-- layouts/default.vue -->
<script setup lang="ts">
const route = useRoute()
</script>

<template>
  <div class="layout">
    <SiteHeader />
    <main>
      <slot /> <!-- Page content -->
    </main>
    <SiteFooter />
  </div>
</template>

<style scoped>
.layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
}
</style>

Using Layouts in Pages

vue
<!-- pages/about.vue -->
<script setup lang="ts">
definePageMeta({
  layout: 'default' // Use specific layout
})
</script>

<template>
  <div>About page content</div>
</template>

Nuxt Configuration

nuxt.config.ts

typescript
export default defineNuxtConfig({
  // Development
  devtools: { enabled: true },
  
  // TypeScript
  typescript: {
    strict: true,
    typeCheck: true
  },

  // Modules
  modules: [
    '@nuxtjs/tailwindcss',  // Tailwind CSS
    '@nuxt/image',          // Image optimization
    '@nuxtjs/seo',          // SEO utilities
  ],

  // App config
  app: {
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
      htmlAttrs: {
        lang: 'en'
      },
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
      ]
    }
  },

  // CSS
  css: [
    '~/assets/css/main.css'
  ],

  // Runtime config (environment variables)
  runtimeConfig: {
    // Private keys (server-side only)
    apiSecret: process.env.API_SECRET,
    
    // Public keys (client-side)
    public: {
      apiBase: process.env.API_BASE_URL || 'https://api.example.com',
      siteUrl: process.env.SITE_URL || 'https://example.com'
    }
  },

  // Nitro (server) config
  nitro: {
    preset: 'node-server', // or 'vercel', 'netlify', etc.
    compressPublicAssets: true
  },

  // Build optimization
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'vendor': ['vue', 'vue-router']
          }
        }
      }
    }
  }
})

Composables

Creating Composables

Composables in composables/ are auto-imported:

typescript
// composables/useApi.ts
export const useApi = () => {
  const config = useRuntimeConfig()
  const baseUrl = config.public.apiBase

  const fetchData = async <T>(endpoint: string): Promise<T> => {
    const { data, error } = await useFetch<T>(`${baseUrl}${endpoint}`)
    
    if (error.value) {
      throw new Error(`API Error: ${error.value.message}`)
    }
    
    return data.value as T
  }

  return {
    fetchData
  }
}

Usage in components:

vue
<script setup lang="ts">
const api = useApi()
const { data: products } = await api.fetchData('/products')
</script>

Common Composables

  • useRoute() - Access current route
  • useRouter() - Navigate programmatically
  • useFetch() - Data fetching with SSR support
  • useAsyncData() - Advanced data fetching
  • useState() - Shared state across components
  • useHead() - Manage head tags
  • useSeoMeta() - Manage SEO meta tags
  • useRuntimeConfig() - Access runtime config

Data Fetching

useFetch

vue
<script setup lang="ts">
// Simple fetch
const { data } = await useFetch('/api/data')

// With options
const { data, pending, error, refresh } = await useFetch('/api/data', {
  method: 'GET',
  query: { page: 1 },
  headers: { 'Authorization': 'Bearer token' },
  lazy: false, // Wait for data before rendering
  server: true, // Fetch on server-side
  pick: ['id', 'name'] // Pick specific fields
})

// Refresh data
const handleRefresh = () => refresh()
</script>

useAsyncData

For custom async operations:

vue
<script setup lang="ts">
const { data, pending } = await useAsyncData('unique-key', async () => {
  // Custom async logic
  const response = await $fetch('/api/data')
  return response.items.map(item => ({
    id: item.id,
    name: item.name.toUpperCase()
  }))
})
</script>

Styling

Global Styles

css
/* assets/css/main.css */
:root {
  --color-primary: #0070f3;
  --color-secondary: #7928ca;
  --spacing-unit: 1rem;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  line-height: 1.6;
  color: #333;
}

Scoped Component Styles

vue
<style scoped>
/* Only applies to this component */
.card {
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Deep selector for child components */
:deep(.child-class) {
  color: red;
}

/* Slotted content */
:slotted(.slot-class) {
  font-weight: bold;
}
</style>

CSS Modules

vue
<style module>
.card {
  padding: 2rem;
}
</style>

<template>
  <div :class="$style.card">
    Content
  </div>
</template>

Performance Optimization

Image Optimization

Use @nuxt/image module:

vue
<template>
  <!-- Optimized image with auto-format and responsive -->
  <NuxtImg
    src="/images/hero.jpg"
    alt="Hero image"
    width="1200"
    height="600"
    format="webp"
    quality="80"
    loading="lazy"
  />

  <!-- Picture element with multiple formats -->
  <NuxtPicture
    src="/images/hero.jpg"
    alt="Hero image"
    :img-attrs="{ class: 'hero-image' }"
  />
</template>

Lazy Loading Components

vue
<script setup lang="ts">
// Lazy load component
const LazyComponent = defineAsyncComponent(() => 
  import('~/components/HeavyComponent.vue')
)
</script>

<template>
  <LazyComponent v-if="shouldShow" />
</template>

Or use Nuxt's Lazy prefix:

vue
<template>
  <!-- Automatically lazy-loaded -->
  <LazyHeavyComponent />
</template>

Code Splitting

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: (id) => {
            if (id.includes('node_modules')) {
              return 'vendor'
            }
            if (id.includes('components/ui')) {
              return 'ui'
            }
          }
        }
      }
    }
  }
})

Middleware

Route Middleware

typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useState('user')
  
  if (!user.value && to.path !== '/login') {
    return navigateTo('/login')
  }
})

Usage:

vue
<script setup lang="ts">
definePageMeta({
  middleware: 'auth'
})
</script>

Global Middleware

typescript
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // Runs on every route change
  console.log('Navigating to:', to.path)
})

Plugins

Creating Plugins

typescript
// plugins/analytics.client.ts
export default defineNuxtPlugin((nuxtApp) => {
  // Only runs on client-side
  nuxtApp.hook('page:finish', () => {
    // Track page view
    console.log('Page view:', nuxtApp.$router.currentRoute.value.path)
  })
  
  return {
    provide: {
      analytics: {
        track: (event: string) => {
          console.log('Track event:', event)
        }
      }
    }
  }
})

Usage in components:

vue
<script setup lang="ts">
const { $analytics } = useNuxtApp()

const handleClick = () => {
  $analytics.track('button_click')
}
</script>

API Routes

Creating API Endpoints

typescript
// server/api/hello.get.ts
export default defineEventHandler((event) => {
  return {
    message: 'Hello from API',
    timestamp: Date.now()
  }
})

// server/api/users/[id].get.ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')
  
  return {
    id,
    name: 'User Name'
  }
})

// server/api/contact.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  // Validate and process
  if (!body.email) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email is required'
    })
  }
  
  return {
    success: true,
    message: 'Contact form submitted'
  }
})

Deployment

Build Commands

bash
# Development
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

# Generate static site (if applicable)
npm run generate

Environment Variables

bash
# .env
API_SECRET=secret123
API_BASE_URL=https://api.example.com
SITE_URL=https://example.com

Access in Nuxt:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: process.env.API_SECRET,
    public: {
      apiBase: process.env.API_BASE_URL
    }
  }
})

Vercel Deployment

json
// vercel.json
{
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm install",
  "framework": "nuxtjs"
}

Netlify Deployment

toml
# netlify.toml
[build]
  command = "npm run build"
  publish = ".output/public"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Docker Deployment

dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

Common Patterns

Loading States

vue
<script setup lang="ts">
const { data, pending, error } = await useFetch('/api/data')
</script>

<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else>{{ data }}</div>
  </div>
</template>

Form Handling

vue
<script setup lang="ts">
const form = reactive({
  name: '',
  email: '',
  message: ''
})

const pending = ref(false)
const error = ref<string | null>(null)
const success = ref(false)

const handleSubmit = async () => {
  pending.value = true
  error.value = null
  
  try {
    const { data } = await useFetch('/api/contact', {
      method: 'POST',
      body: form
    })
    
    success.value = true
    // Reset form
    Object.assign(form, { name: '', email: '', message: '' })
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Something went wrong'
  } finally {
    pending.value = false
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.name" placeholder="Name" required />
    <input v-model="form.email" type="email" placeholder="Email" required />
    <textarea v-model="form.message" placeholder="Message" required />
    <button type="submit" :disabled="pending">
      {{ pending ? 'Sending...' : 'Send' }}
    </button>
    <div v-if="error" class="error">{{ error }}</div>
    <div v-if="success" class="success">Message sent!</div>
  </form>
</template>

Pagination

vue
<script setup lang="ts">
const page = ref(1)
const pageSize = 10

const { data: items } = await useFetch('/api/items', {
  query: {
    page,
    limit: pageSize
  },
  watch: [page] // Re-fetch when page changes
})

const nextPage = () => page.value++
const prevPage = () => page.value = Math.max(1, page.value - 1)
</script>

<template>
  <div>
    <div v-for="item in items?.data" :key="item.id">
      {{ item.name }}
    </div>
    <button @click="prevPage" :disabled="page === 1">Previous</button>
    <span>Page {{ page }}</span>
    <button @click="nextPage">Next</button>
  </div>
</template>

Best Practices

  1. Component Organization

    • Keep components small and focused
    • Use composition over inheritance
    • Extract reusable logic to composables
  2. Performance

    • Use lazy loading for heavy components
    • Optimize images with @nuxt/image
    • Implement proper caching strategies
    • Use useFetch over manual fetch for SSR support
  3. SEO

    • Use useSeoMeta for all pages
    • Add Open Graph and Twitter Card meta tags
    • Include canonical URLs
    • Generate sitemap and robots.txt
  4. Type Safety

    • Enable strict TypeScript mode
    • Define interfaces for all data structures
    • Use typed composables and utilities
  5. State Management

    • Use useState for simple shared state
    • Consider Pinia for complex state management
    • Keep state close to where it's used
  6. Error Handling

    • Always handle errors in async operations
    • Use createError for API routes
    • Provide user-friendly error messages
  7. Accessibility

    • Use semantic HTML elements
    • Add ARIA labels where needed
    • Ensure keyboard navigation works
    • Test with screen readers
  8. Security

    • Validate all user inputs
    • Sanitize content before rendering
    • Use environment variables for secrets
    • Implement rate limiting for API routes

Troubleshooting

Common Issues

Auto-imports not working:

  • Restart dev server
  • Check file naming (must be in correct directory)
  • Clear .nuxt directory and rebuild

SSR errors:

  • Check for browser-only code (use process.client guard)
  • Ensure data is available before rendering
  • Use <ClientOnly> component for client-only components

Build errors:

  • Clear .nuxt and .output directories
  • Verify all dependencies are installed
  • Check TypeScript errors

Hydration mismatches:

  • Ensure server and client render the same HTML
  • Avoid using Date.now() or random values in templates
  • Check for conditional rendering based on client-only state

Resources