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
- Props: Use TypeScript interfaces for type safety
- Emits: Define emits explicitly with TypeScript
- Slots: Use named slots for flexibility
- Composables: Extract reusable logic to composables
- Auto-imports: All components in
components/are auto-imported - 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 routeuseRouter()- Navigate programmaticallyuseFetch()- Data fetching with SSR supportuseAsyncData()- Advanced data fetchinguseState()- Shared state across componentsuseHead()- Manage head tagsuseSeoMeta()- Manage SEO meta tagsuseRuntimeConfig()- 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
-
Component Organization
- Keep components small and focused
- Use composition over inheritance
- Extract reusable logic to composables
-
Performance
- Use lazy loading for heavy components
- Optimize images with
@nuxt/image - Implement proper caching strategies
- Use
useFetchover manualfetchfor SSR support
-
SEO
- Use
useSeoMetafor all pages - Add Open Graph and Twitter Card meta tags
- Include canonical URLs
- Generate sitemap and robots.txt
- Use
-
Type Safety
- Enable strict TypeScript mode
- Define interfaces for all data structures
- Use typed composables and utilities
-
State Management
- Use
useStatefor simple shared state - Consider Pinia for complex state management
- Keep state close to where it's used
- Use
-
Error Handling
- Always handle errors in async operations
- Use
createErrorfor API routes - Provide user-friendly error messages
-
Accessibility
- Use semantic HTML elements
- Add ARIA labels where needed
- Ensure keyboard navigation works
- Test with screen readers
-
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
.nuxtdirectory and rebuild
SSR errors:
- Check for browser-only code (use
process.clientguard) - Ensure data is available before rendering
- Use
<ClientOnly>component for client-only components
Build errors:
- Clear
.nuxtand.outputdirectories - 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
Repository
gitarbor/gitarbor-tuiGitHub Stars
37
First Seen
12 days ago
Security Audits
Installed on
opencode2
gemini-cli2
codebuddy2
github-copilot2
codex2
kimi-cli2