コンテンツにスキップ

Nuxt.js Cheatsheet

Nuxt.js - The Intuitive Vue Framework

Nuxt.js is a free and open-source framework with an intuitive and extendable way to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js.

Table of Contents

Installation

Prerequisites

# Node.js version requirements
node --version  # Should be 16.10 or later

# Check npm version
npm --version

# Update npm if needed
npm install -g npm@latest

Create Nuxt.js App

# Create new Nuxt.js app
npx nuxi@latest init my-app

# Navigate to project
cd my-app

# Install dependencies
npm install

# Start development server
npm run dev

Alternative Installation Methods

# Using yarn
yarn create nuxt-app my-app

# Using pnpm
pnpm dlx nuxi@latest init my-app

# Using specific template
npx nuxi@latest init my-app --template ui
npx nuxi@latest init my-app --template content
npx nuxi@latest init my-app --template module

Manual Installation

# Create project directory
mkdir my-nuxt-app
cd my-nuxt-app

# Initialize package.json
npm init -y

# Install Nuxt.js
npm install nuxt

# Install Vue dependencies
npm install vue@latest

# Add scripts to package.json

Package.json Scripts

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  }
}

Project Creation

Starter Templates

# Official templates
npx nuxi@latest init my-app --template ui          # Nuxt UI
npx nuxi@latest init my-app --template content     # Nuxt Content
npx nuxi@latest init my-app --template module      # Nuxt Module
npx nuxi@latest init my-app --template layer       # Nuxt Layer

# Community templates
npx nuxi@latest init my-app --template docus       # Documentation
npx nuxi@latest init my-app --template minimal     # Minimal setup

Development Commands

# Start development server
npm run dev

# Start with specific port
npm run dev -- --port 3001

# Start with specific host
npm run dev -- --host 0.0.0.0

# Start with HTTPS
npm run dev -- --https

# Start with tunnel
npm run dev -- --tunnel

Build Commands

# Build for production
npm run build

# Generate static site
npm run generate

# Preview production build
npm run preview

# Analyze bundle
npm run build -- --analyze

Project Structure

Nuxt 3 Structure

my-nuxt-app/
├── .nuxt/                 # Auto-generated files
├── .output/               # Build output
├── assets/                # Uncompiled assets
├── components/            # Vue components
├── composables/           # Vue composables
├── content/               # Content files (if using @nuxt/content)
├── layouts/               # Layout components
├── middleware/            # Route middleware
├── pages/                 # File-based routing
├── plugins/               # Plugins
├── public/                # Static files
├── server/                # Server-side code
│   ├── api/              # API routes
│   └── middleware/       # Server middleware
├── stores/                # Pinia stores
├── utils/                 # Utility functions
├── app.vue               # Main app component
├── nuxt.config.ts        # Nuxt configuration
├── package.json
└── tsconfig.json

Configuration Files

// nuxt.config.ts
export default defineNuxtConfig({
  // Basic configuration
  devtools: { enabled: true },

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

  // Modules
  modules: [
    '@nuxtjs/tailwindcss',
    '@pinia/nuxt',
    '@nuxt/content'
  ],

  // Runtime config
  runtimeConfig: {
    // Private keys (only available on server-side)
    apiSecret: '123',
    // Public keys (exposed to client-side)
    public: {
      apiBase: '/api'
    }
  },

  // App configuration
  app: {
    head: {
      title: 'My Nuxt App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' }
      ]
    }
  }
});

Routing

File-based Routing

# Route structure
pages/
├── index.vue             # / (root)
├── about.vue             # /about
├── blog/
│   ├── index.vue         # /blog
│   └── [slug].vue        # /blog/[slug]
├── users/
│   ├── index.vue         # /users
│   ├── [id].vue          # /users/[id]
│   └── [id]/
│       └── edit.vue      # /users/[id]/edit
└── [...slug].vue         # Catch-all route

Dynamic Routes

<!-- pages/blog/[slug].vue -->
<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

<script setup>
// Get route parameters
const route = useRoute();
const slug = route.params.slug;

// Fetch data based on slug
const { data: post } = await $fetch(`/api/posts/${slug}`);

// Define page meta
definePageMeta({
  title: 'Blog Post',
  description: 'Read our latest blog post'
});
</script>

Nested Routes

<!-- pages/users/[id].vue -->
<template>
  <div>
    <h1>User Profile</h1>
    <NuxtPage />
  </div>
</template>

<!-- pages/users/[id]/index.vue -->
<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

<!-- pages/users/[id]/edit.vue -->
<template>
  <div>
    <h2>Edit User</h2>
    <form @submit="updateUser">
      <!-- Form fields -->
    </form>
  </div>
</template>
<template>
  <nav>
    <!-- Static navigation -->
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>

    <!-- Dynamic navigation -->
    <NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>

    <!-- External links -->
    <NuxtLink to="https://nuxtjs.org" external>Nuxt.js</NuxtLink>

    <!-- Programmatic navigation -->
    <button @click="navigateTo('/dashboard')">Dashboard</button>
    <button @click="navigateTo('/login', { replace: true })">Login</button>
  </nav>
</template>

<script setup>
// Programmatic navigation
const router = useRouter();

function goToProfile() {
  router.push('/profile');
}

function goBack() {
  router.back();
}

// Navigation guards
definePageMeta({
  middleware: 'auth'
});
</script>

Route Middleware

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useAuthUser();

  if (!user.value) {
    return navigateTo('/login');
  }
});

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

Pages and Layouts

Default Layout

<!-- layouts/default.vue -->
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
        <NuxtLink to="/blog">Blog</NuxtLink>
      </nav>
    </header>

    <main>
      <slot />
    </main>

    <footer>
      <p>&copy; 2024 My Nuxt App</p>
    </footer>
  </div>
</template>

<style scoped>
header {
  background: #f8f9fa;
  padding: 1rem;
}

nav a {
  margin-right: 1rem;
  text-decoration: none;
}

main {
  min-height: 80vh;
  padding: 2rem;
}

footer {
  background: #343a40;
  color: white;
  text-align: center;
  padding: 1rem;
}
</style>

Custom Layouts

<!-- layouts/admin.vue -->
<template>
  <div class="admin-layout">
    <aside class="sidebar">
      <nav>
        <NuxtLink to="/admin">Dashboard</NuxtLink>
        <NuxtLink to="/admin/users">Users</NuxtLink>
        <NuxtLink to="/admin/posts">Posts</NuxtLink>
      </nav>
    </aside>

    <main class="content">
      <slot />
    </main>
  </div>
</template>

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

.sidebar {
  width: 250px;
  background: #2c3e50;
  color: white;
  padding: 1rem;
}

.content {
  flex: 1;
  padding: 2rem;
}
</style>

Using Layouts in Pages

<!-- pages/admin/index.vue -->
<template>
  <div>
    <h1>Admin Dashboard</h1>
    <div class="stats">
      <div class="stat">
        <h3>Users</h3>
        <p>{{ stats.users }}</p>
      </div>
      <div class="stat">
        <h3>Posts</h3>
        <p>{{ stats.posts }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
// Use custom layout
definePageMeta({
  layout: 'admin',
  middleware: 'admin-auth'
});

// Fetch dashboard stats
const { data: stats } = await $fetch('/api/admin/stats');
</script>

Page Meta and SEO

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

<script setup>
const route = useRoute();
const { data: post } = await $fetch(`/api/posts/${route.params.slug}`);

// Dynamic page meta
useHead({
  title: post.title,
  meta: [
    { name: 'description', content: post.excerpt },
    { name: 'keywords', content: post.tags.join(', ') },
    { property: 'og:title', content: post.title },
    { property: 'og:description', content: post.excerpt },
    { property: 'og:image', content: post.image },
    { property: 'og:url', content: `https://example.com/blog/${post.slug}` }
  ]
});

// Structured data
useJsonld({
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.excerpt,
  author: {
    '@type': 'Person',
    name: post.author.name
  },
  datePublished: post.publishedAt
});
</script>

Components

Auto-imported Components

<!-- components/TheHeader.vue -->
<template>
  <header class="header">
    <div class="container">
      <NuxtLink to="/" class="logo">
        <img src="/logo.png" alt="Logo" />
      </NuxtLink>

      <nav class="nav">
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
        <NuxtLink to="/blog">Blog</NuxtLink>
      </nav>
    </div>
  </header>
</template>

<script setup>
// Component is auto-imported and can be used as <TheHeader />
</script>

Nested Components

<!-- components/blog/PostCard.vue -->
<template>
  <article class="post-card">
    <img :src="post.image" :alt="post.title" />
    <div class="content">
      <h3>{{ post.title }}</h3>
      <p>{{ post.excerpt }}</p>
      <NuxtLink :to="`/blog/${post.slug}`" class="read-more">
        Read More
      </NuxtLink>
    </div>
  </article>
</template>

<script setup>
interface Post {
  id: string;
  title: string;
  excerpt: string;
  image: string;
  slug: string;
}

defineProps<{
  post: Post;
}>();
</script>

<!-- Usage: <BlogPostCard :post="post" /> -->

Composables

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState<User | null>('auth.user', () => null);

  const login = async (credentials: LoginCredentials) => {
    const { data } = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials
    });

    user.value = data.user;
    await navigateTo('/dashboard');
  };

  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' });
    user.value = null;
    await navigateTo('/');
  };

  const isAuthenticated = computed(() => !!user.value);

  return {
    user: readonly(user),
    login,
    logout,
    isAuthenticated
  };
};

// Usage in components
const { user, login, logout, isAuthenticated } = useAuth();

Global State with useState

// composables/useCounter.ts
export const useCounter = () => {
  const count = useState('counter', () => 0);

  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => count.value = 0;

  return {
    count: readonly(count),
    increment,
    decrement,
    reset
  };
};

Data Fetching

Server-side Data Fetching

<template>
  <div>
    <h1>Posts</h1>
    <div v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.excerpt }}</p>
    </div>
  </div>
</template>

<script setup>
// Fetch data on server-side
const { data: posts } = await $fetch('/api/posts');

// Alternative with useFetch
const { data: posts, pending, error } = await useFetch('/api/posts');

// With reactive parameters
const page = ref(1);
const { data: posts } = await useFetch('/api/posts', {
  query: { page }
});
</script>

Client-side Data Fetching

<template>
  <div>
    <button @click="loadPosts">Load Posts</button>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <div v-for="post in posts" :key="post.id">
        <h2>{{ post.title }}</h2>
      </div>
    </div>
  </div>
</template>

<script setup>
const posts = ref([]);
const pending = ref(false);
const error = ref(null);

const loadPosts = async () => {
  pending.value = true;
  error.value = null;

  try {
    posts.value = await $fetch('/api/posts');
  } catch (err) {
    error.value = err.message;
  } finally {
    pending.value = false;
  }
};
</script>

Advanced Data Fetching

<script setup>
// Lazy loading
const { data: posts, pending } = await useLazyFetch('/api/posts');

// With caching
const { data: user } = await useFetch(`/api/users/${userId}`, {
  key: `user-${userId}`,
  server: true
});

// With transformation
const { data: posts } = await useFetch('/api/posts', {
  transform: (data: any[]) => data.map(post => ({
    ...post,
    formattedDate: new Date(post.createdAt).toLocaleDateString()
  }))
});

// With error handling
const { data: posts, error } = await useFetch('/api/posts', {
  onResponseError({ request, response, options }) {
    console.error('Response error:', response.status);
  },
  onRequestError({ request, options, error }) {
    console.error('Request error:', error);
  }
});

// Refresh data
const { data: posts, refresh } = await useFetch('/api/posts');

// Refresh manually
await refresh();
</script>

Data Fetching with Pinia

// stores/posts.ts
export const usePostsStore = defineStore('posts', () => {
  const posts = ref([]);
  const loading = ref(false);
  const error = ref(null);

  const fetchPosts = async () => {
    loading.value = true;
    error.value = null;

    try {
      const data = await $fetch('/api/posts');
      posts.value = data;
    } catch (err) {
      error.value = err.message;
    } finally {
      loading.value = false;
    }
  };

  const addPost = async (post: CreatePostData) => {
    const newPost = await $fetch('/api/posts', {
      method: 'POST',
      body: post
    });
    posts.value.unshift(newPost);
  };

  return {
    posts: readonly(posts),
    loading: readonly(loading),
    error: readonly(error),
    fetchPosts,
    addPost
  };
});

State Management

Pinia Setup

# Install Pinia
npm install pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
});

Pinia Store

// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null);
  const token = ref<string | null>(null);

  // Getters
  const isAuthenticated = computed(() => !!user.value);
  const isAdmin = computed(() => user.value?.role === 'admin');

  // Actions
  const login = async (credentials: LoginCredentials) => {
    try {
      const response = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials
      });

      user.value = response.user;
      token.value = response.token;

      // Store in cookie
      const tokenCookie = useCookie('auth-token');
      tokenCookie.value = response.token;

      return response;
    } catch (error) {
      throw error;
    }
  };

  const logout = async () => {
    try {
      await $fetch('/api/auth/logout', { method: 'POST' });
    } finally {
      user.value = null;
      token.value = null;

      const tokenCookie = useCookie('auth-token');
      tokenCookie.value = null;
    }
  };

  const fetchUser = async () => {
    try {
      const userData = await $fetch('/api/auth/me');
      user.value = userData;
    } catch (error) {
      // Token might be invalid
      logout();
    }
  };

  return {
    user: readonly(user),
    token: readonly(token),
    isAuthenticated,
    isAdmin,
    login,
    logout,
    fetchUser
  };
});

Using Stores in Components

<template>
  <div>
    <div v-if="isAuthenticated">
      <p>Welcome, {{ user.name }}!</p>
      <button @click="logout">Logout</button>
    </div>
    <div v-else>
      <form @submit.prevent="handleLogin">
        <input v-model="email" type="email" placeholder="Email" required />
        <input v-model="password" type="password" placeholder="Password" required />
        <button type="submit" :disabled="loading">Login</button>
      </form>
    </div>
  </div>
</template>

<script setup>
const authStore = useAuthStore();
const { user, isAuthenticated } = storeToRefs(authStore);

const email = ref('');
const password = ref('');
const loading = ref(false);

const handleLogin = async () => {
  loading.value = true;

  try {
    await authStore.login({
      email: email.value,
      password: password.value
    });

    await navigateTo('/dashboard');
  } catch (error) {
    console.error('Login failed:', error);
  } finally {
    loading.value = false;
  }
};

const logout = async () => {
  await authStore.logout();
  await navigateTo('/');
};
</script>

Persisting State

// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref([]);

  // Persist to localStorage
  const persistedItems = useCookie('cart-items', {
    default: () => [],
    serializer: {
      read: (value: string) => {
        try {
          return JSON.parse(value);
        } catch {
          return [];
        }
      },
      write: (value: any[]) => JSON.stringify(value)
    }
  });

  // Initialize from persisted data
  onMounted(() => {
    items.value = persistedItems.value;
  });

  // Watch for changes and persist
  watch(items, (newItems) => {
    persistedItems.value = newItems;
  }, { deep: true });

  const addItem = (product: Product) => {
    const existingItem = items.value.find(item => item.id === product.id);

    if (existingItem) {
      existingItem.quantity++;
    } else {
      items.value.push({ ...product, quantity: 1 });
    }
  };

  const removeItem = (productId: string) => {
    const index = items.value.findIndex(item => item.id === productId);
    if (index > -1) {
      items.value.splice(index, 1);
    }
  };

  const total = computed(() => {
    return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  });

  return {
    items: readonly(items),
    total,
    addItem,
    removeItem
  };
});

Styling

CSS and SCSS

<template>
  <div class="component">
    <h1 class="title">Hello World</h1>
    <p class="description">This is a Nuxt component</p>
  </div>
</template>

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

.title {
  color: #2c3e50;
  font-size: 2rem;
  margin-bottom: 1rem;
}

.description {
  color: #7f8c8d;
  line-height: 1.6;
}
</style>

<!-- SCSS support -->
<style lang="scss" scoped>
$primary-color: #3498db;
$secondary-color: #2c3e50;

.component {
  padding: 2rem;

  .title {
    color: $primary-color;
    font-size: 2rem;

    &:hover {
      color: darken($primary-color, 10%);
    }
  }

  .description {
    color: $secondary-color;
  }
}
</style>

Tailwind CSS

# Install Tailwind CSS
npm install --save-dev @nuxtjs/tailwindcss
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss']
});
<template>
  <div class="min-h-screen bg-gray-100">
    <header class="bg-white shadow">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
          <div class="flex items-center">
            <h1 class="text-xl font-semibold text-gray-900">My App</h1>
          </div>
          <nav class="flex space-x-8">
            <NuxtLink 
              to="/" 
              class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
            >
              Home
            </NuxtLink>
            <NuxtLink 
              to="/about" 
              class="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
            >
              About
            </NuxtLink>
          </nav>
        </div>
      </div>
    </header>

    <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
      <slot />
    </main>
  </div>
</template>

CSS Modules

<template>
  <div :class="$style.container">
    <h1 :class="$style.title">CSS Modules</h1>
    <p :class="$style.description">Scoped and modular CSS</p>
  </div>
</template>

<style module>
.container {
  padding: 2rem;
  background: #f8f9fa;
}

.title {
  color: #2c3e50;
  font-size: 2rem;
}

.description {
  color: #7f8c8d;
}
</style>

Global Styles

/* assets/css/main.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

/* Custom global styles */
body {
  font-family: 'Inter', sans-serif;
}

.btn {
  @apply px-4 py-2 rounded font-medium transition-colors;
}

.btn-primary {
  @apply bg-blue-600 text-white hover:bg-blue-700;
}

.btn-secondary {
  @apply bg-gray-600 text-white hover:bg-gray-700;
}
// nuxt.config.ts
export default defineNuxtConfig({
  css: ['~/assets/css/main.css']
});

Plugins

Client-side Plugins

// plugins/vue-toastification.client.ts
import Toast, { POSITION } from 'vue-toastification';
import 'vue-toastification/dist/index.css';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(Toast, {
    position: POSITION.TOP_RIGHT,
    timeout: 3000,
    closeOnClick: true,
    pauseOnFocusLoss: true,
    pauseOnHover: true,
    draggable: true,
    draggablePercent: 0.6,
    showCloseButtonOnHover: false,
    hideProgressBar: false,
    closeButton: 'button',
    icon: true,
    rtl: false
  });
});

Server-side Plugins

// plugins/api.server.ts
export default defineNuxtPlugin(async (nuxtApp) => {
  // Server-side initialization
  const config = useRuntimeConfig();

  // Setup API client
  const api = $fetch.create({
    baseURL: config.public.apiBase,
    headers: {
      'Authorization': `Bearer ${config.apiSecret}`
    }
  });

  // Provide to app
  return {
    provide: {
      api
    }
  };
});

Universal Plugins

// plugins/pinia-persistence.ts
export default defineNuxtPlugin(() => {
  // This runs on both client and server
  const nuxtApp = useNuxtApp();

  // Setup Pinia persistence
  if (process.client) {
    nuxtApp.$pinia.use(({ store }) => {
      // Persist store state
      const stored = localStorage.getItem(`pinia-${store.$id}`);
      if (stored) {
        store.$patch(JSON.parse(stored));
      }

      store.$subscribe((mutation, state) => {
        localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state));
      });
    });
  }
});

Plugin with Composables

// plugins/auth.ts
export default defineNuxtPlugin(async () => {
  const { $fetch } = useNuxtApp();
  const user = useState('auth.user', () => null);

  // Check for existing session
  if (process.client) {
    const token = useCookie('auth-token');

    if (token.value) {
      try {
        const userData = await $fetch('/api/auth/me', {
          headers: {
            Authorization: `Bearer ${token.value}`
          }
        });
        user.value = userData;
      } catch (error) {
        // Invalid token, clear it
        token.value = null;
      }
    }
  }

  return {
    provide: {
      auth: {
        user,
        login: async (credentials: any) => {
          const response = await $fetch('/api/auth/login', {
            method: 'POST',
            body: credentials
          });

          user.value = response.user;
          const token = useCookie('auth-token');
          token.value = response.token;

          return response;
        },
        logout: () => {
          user.value = null;
          const token = useCookie('auth-token');
          token.value = null;
        }
      }
    }
  };
});

Modules

Installing Modules

# Popular Nuxt modules
npm install @nuxtjs/tailwindcss
npm install @nuxt/content
npm install @pinia/nuxt
npm install @nuxtjs/google-fonts
npm install @nuxt/image
npm install @vueuse/nuxt

Module Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/tailwindcss',
    '@nuxt/content',
    '@pinia/nuxt',
    '@nuxtjs/google-fonts',
    '@nuxt/image',
    '@vueuse/nuxt'
  ],

  // Module-specific configuration
  googleFonts: {
    families: {
      Inter: [400, 500, 600, 700],
      'Fira Code': [400, 500]
    }
  },

  content: {
    highlight: {
      theme: 'github-dark'
    }
  },

  image: {
    cloudinary: {
      baseURL: 'https://res.cloudinary.com/demo/image/upload/'
    }
  }
});

Content Module

<!-- pages/blog/[...slug].vue -->
<template>
  <div>
    <ContentDoc />
  </div>
</template>

<!-- Alternative with custom rendering -->
<template>
  <article>
    <header>
      <h1>{{ data.title }}</h1>
      <p>{{ data.description }}</p>
      <time>{{ formatDate(data.date) }}</time>
    </header>

    <ContentRenderer :value="data" />
  </article>
</template>

<script setup>
const { data } = await useAsyncData('blog-post', () => 
  queryContent('/blog').where({ _path: useRoute().path }).findOne()
);

if (!data.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Post not found'
  });
}

const formatDate = (date: string) => {
  return new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};
</script>

Image Module

<template>
  <div>
    <!-- Optimized images -->
    <NuxtImg
      src="/hero.jpg"
      alt="Hero image"
      width="800"
      height="600"
      loading="lazy"
    />

    <!-- Responsive images -->
    <NuxtPicture
      src="/responsive.jpg"
      alt="Responsive image"
      :img-attrs="{
        class: 'responsive-image',
        style: 'display: block; margin: auto;'
      }"
    />

    <!-- Cloudinary integration -->
    <NuxtImg
      provider="cloudinary"
      src="/sample.jpg"
      width="400"
      height="300"
      :modifiers="{ quality: 'auto', format: 'auto' }"
    />
  </div>
</template>

Server-Side Rendering

SSR Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true, // Enable SSR (default)

  // Nitro configuration
  nitro: {
    prerender: {
      routes: ['/sitemap.xml', '/robots.txt']
    }
  },

  // Route rules
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },

    // Product page generated on-demand, revalidates in background
    '/products/**': { isr: true },

    // Blog post cached for 1 hour
    '/blog/**': { isr: 3600 },

    // Admin dashboard renders only on client-side
    '/admin/**': { ssr: false },

    // API routes
    '/api/**': { cors: true }
  }
});

Hybrid Rendering

<!-- pages/products/[id].vue -->
<template>
  <div>
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
    <p>Price: ${{ product.price }}</p>

    <!-- Client-side only component -->
    <ClientOnly>
      <ProductReviews :product-id="product.id" />
      <template #fallback>
        <div>Loading reviews...</div>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
// This runs on server for SSR
const route = useRoute();
const { data: product } = await $fetch(`/api/products/${route.params.id}`);

// Define route rules
defineRouteRules({
  isr: 3600 // Regenerate every hour
});
</script>

Static Site Generation

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: [
        '/',
        '/about',
        '/contact'
      ]
    }
  }
});
# Generate static site
npm run generate

# Preview generated site
npm run preview

ISR (Incremental Static Regeneration)

<script setup>
// Enable ISR with 1 hour revalidation
defineRouteRules({
  isr: 3600
});

// Fetch data that will be cached
const { data: posts } = await $fetch('/api/posts');
</script>

API Routes

Basic API Routes

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  // Get query parameters
  const query = getQuery(event);
  const page = parseInt(query.page as string) || 1;
  const limit = parseInt(query.limit as string) || 10;

  // Fetch users from database
  const users = await getUsersFromDB({ page, limit });

  return {
    users,
    pagination: {
      page,
      limit,
      total: users.length
    }
  };
});

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  // Get request body
  const body = await readBody(event);

  // Validate input
  if (!body.email || !body.name) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email and name are required'
    });
  }

  // Create user
  const user = await createUser(body);

  // Set response status
  setResponseStatus(event, 201);

  return user;
});

Dynamic API Routes

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  if (!id) {
    throw createError({
      statusCode: 400,
      statusMessage: 'User ID is required'
    });
  }

  const user = await getUserById(id);

  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found'
    });
  }

  return user;
});

// server/api/users/[id].put.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  const body = await readBody(event);

  const updatedUser = await updateUser(id, body);

  return updatedUser;
});

// server/api/users/[id].delete.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  await deleteUser(id);

  setResponseStatus(event, 204);
  return null;
});

Middleware

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // Only apply to API routes
  if (!event.node.req.url?.startsWith('/api/')) {
    return;
  }

  // Skip auth for public routes
  const publicRoutes = ['/api/auth/login', '/api/auth/register'];
  if (publicRoutes.includes(event.node.req.url)) {
    return;
  }

  // Check authorization header
  const authorization = getHeader(event, 'authorization');

  if (!authorization) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Authorization header required'
    });
  }

  const token = authorization.replace('Bearer ', '');

  try {
    const user = await verifyToken(token);
    event.context.user = user;
  } catch (error) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid token'
    });
  }
});

File Upload

// server/api/upload.post.ts
import { writeFile } from 'fs/promises';
import { join } from 'path';

export default defineEventHandler(async (event) => {
  const form = await readMultipartFormData(event);

  if (!form) {
    throw createError({
      statusCode: 400,
      statusMessage: 'No file uploaded'
    });
  }

  const file = form.find(item => item.name === 'file');

  if (!file) {
    throw createError({
      statusCode: 400,
      statusMessage: 'File field is required'
    });
  }

  // Generate unique filename
  const filename = `${Date.now()}-${file.filename}`;
  const filepath = join(process.cwd(), 'public/uploads', filename);

  // Save file
  await writeFile(filepath, file.data);

  return {
    filename,
    url: `/uploads/${filename}`,
    size: file.data.length
  };
});

Deployment

Vercel Deployment

# Install Vercel CLI
npm install -g vercel

# Deploy to Vercel
vercel

# Deploy to production
vercel --prod
// vercel.json
{
  "framework": "nuxtjs",
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm install"
}

Netlify Deployment

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

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

[build.environment]
  NODE_VERSION = "18"

Docker Deployment

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy source code
COPY . .

# Build application
RUN npm run build

# Expose port
EXPOSE 3000

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

Static Hosting

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: ['/sitemap.xml']
    }
  },

  // For static hosting
  ssr: false, // Disable SSR for SPA mode

  // Or use static generation
  nitro: {
    prerender: {
      crawlLinks: true
    }
  }
});

Environment Variables

# .env
NUXT_SECRET_KEY=your-secret-key
NUXT_PUBLIC_API_URL=https://api.example.com

# .env.production
NUXT_SECRET_KEY=production-secret-key
NUXT_PUBLIC_API_URL=https://api.production.com
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Private keys (only available on server-side)
    secretKey: process.env.NUXT_SECRET_KEY,

    // Public keys (exposed to client-side)
    public: {
      apiUrl: process.env.NUXT_PUBLIC_API_URL
    }
  }
});

Testing

Vitest Setup

# Install testing dependencies
npm install --save-dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom'
  }
});

Component Testing

// tests/components/Button.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Button from '~/components/Button.vue';

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: { text: 'Hello world' }
    });

    expect(wrapper.text()).toContain('Hello world');
  });

  it('emits click event', async () => {
    const wrapper = mount(Button);

    await wrapper.trigger('click');

    expect(wrapper.emitted()).toHaveProperty('click');
  });
});

E2E Testing with Playwright

# Install Playwright
npm install --save-dev @playwright/test
npx playwright install
// tests/e2e/home.spec.ts
import { test, expect } from '@playwright/test';

test('homepage', async ({ page }) => {
  await page.goto('/');

  await expect(page).toHaveTitle(/Nuxt/);
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});

Performance Optimization

Bundle Analysis

# Analyze bundle
npm run build -- --analyze

# Or use webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer

Code Splitting

<script setup>
// Lazy load components
const LazyComponent = defineAsyncComponent(() => import('~/components/Heavy.vue'));

// Lazy load pages
definePageMeta({
  mode: 'spa' // Client-side only
});
</script>

Image Optimization

<template>
  <div>
    <!-- Optimized images -->
    <NuxtImg
      src="/hero.jpg"
      alt="Hero"
      width="800"
      height="600"
      loading="lazy"
      placeholder
    />

    <!-- WebP format -->
    <NuxtImg
      src="/image.jpg"
      alt="Image"
      format="webp"
      quality="80"
    />
  </div>
</template>

Caching Strategies

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Cache homepage for 1 hour
    '/': { isr: 3600 },

    // Cache blog posts for 1 day
    '/blog/**': { isr: 86400 },

    // Prerender at build time
    '/about': { prerender: true }
  }
});

Troubleshooting

Common Issues

Hydration Mismatch

<template>
  <div>
    <!-- Use ClientOnly for client-specific content -->
    <ClientOnly>
      <div>{{ new Date().toLocaleString() }}</div>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </ClientOnly>
  </div>
</template>

Memory Leaks

<script setup>
// Proper cleanup
const interval = setInterval(() => {
  console.log('tick');
}, 1000);

onUnmounted(() => {
  clearInterval(interval);
});
</script>

Build Errors

# Clear Nuxt cache
rm -rf .nuxt .output

# Clear node_modules
rm -rf node_modules package-lock.json
npm install

# Check for TypeScript errors
npx nuxi typecheck

Best Practices

Project Structure

# Recommended structure
components/
├── ui/              # Basic UI components
├── forms/           # Form components
└── layout/          # Layout components

composables/
├── useAuth.ts       # Authentication
├── useApi.ts        # API calls
└── useState.ts      # State management

utils/
├── validation.ts    # Validation helpers
├── formatting.ts    # Formatting utilities
└── constants.ts     # App constants

Performance

<script setup>
// Use computed for expensive operations
const expensiveValue = computed(() => {
  return heavyCalculation(props.data);
});

// Use lazy loading for heavy components
const HeavyChart = defineAsyncComponent(() => import('~/components/HeavyChart.vue'));

// Optimize images
</script>

<template>
  <div>
    <NuxtImg
      src="/image.jpg"
      alt="Image"
      loading="lazy"
      placeholder
    />
  </div>
</template>

SEO

<script setup>
// Dynamic meta tags
const route = useRoute();
const { data: post } = await $fetch(`/api/posts/${route.params.slug}`);

useSeoMeta({
  title: post.title,
  ogTitle: post.title,
  description: post.excerpt,
  ogDescription: post.excerpt,
  ogImage: post.image,
  twitterCard: 'summary_large_image'
});
</script>

Summary

Nuxt.js is a powerful Vue.js framework that provides a complete solution for building modern web applications. Key features include:

  • File-based Routing: Automatic routing based on file structure
  • Server-Side Rendering: Built-in SSR with hybrid rendering options
  • Auto-imports: Automatic component and composable imports
  • Data Fetching: Powerful data fetching with caching and reactivity
  • Modules: Rich ecosystem of modules for extended functionality
  • Performance: Built-in optimizations and code splitting

For optimal results, leverage server-side rendering for better SEO, use composables for reusable logic, implement proper caching strategies, and follow Nuxt.js conventions for routing and data fetching.