nuxt-website

SKILL.md

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

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>:

<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:

<!-- 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

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

Example dynamic page:

<!-- 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

<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):

<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

<!-- 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

<!-- 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

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:

// 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:

<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

<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:

<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

/* 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

<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

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

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

Performance Optimization

Image Optimization

Use @nuxt/image module:

<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

<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:

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

Code Splitting

// 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

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

Usage:

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

Global Middleware

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

Plugins

Creating Plugins

// 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:

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

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

API Routes

Creating API Endpoints

// 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

# 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

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

Access in Nuxt:

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

Vercel Deployment

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

Netlify Deployment

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

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

Docker Deployment

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

<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

<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

<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

Weekly Installs
2
GitHub Stars
37
First Seen
12 days ago
Installed on
opencode2
gemini-cli2
codebuddy2
github-copilot2
codex2
kimi-cli2