Skip to content

Nuxt.js Integration with Element Plus โ€‹

Overview โ€‹

Nuxt.js is a powerful Vue.js framework that provides built-in SSR capabilities, making it an excellent choice for Element Plus applications. This guide covers how to integrate Element Plus with Nuxt.js for optimal server-side rendering performance.

Why Nuxt.js for Element Plus SSR? โ€‹

Advantages โ€‹

  1. Zero Configuration SSR: Built-in SSR support without complex setup
  2. Automatic Code Splitting: Route-based code splitting out of the box
  3. File-based Routing: Automatic route generation from file structure
  4. Built-in Performance Optimizations: Image optimization, lazy loading, etc.
  5. Rich Ecosystem: Extensive module ecosystem for common features
  6. TypeScript Support: First-class TypeScript integration

Nuxt.js vs Custom SSR Setup โ€‹

FeatureCustom SSRNuxt.js
Setup ComplexityHighLow
ConfigurationManualConvention-based
Code SplittingManualAutomatic
SEO FeaturesManualBuilt-in
PerformanceCustom optimizationOptimized by default
Learning CurveSteepModerate

Project Setup โ€‹

Creating a New Nuxt.js Project โ€‹

bash
# Create new Nuxt.js project
npx nuxi@latest init element-plus-nuxt-app
cd element-plus-nuxt-app

# Install dependencies
npm install

# Install Element Plus
npm install element-plus @element-plus/nuxt
npm install @element-plus/icons-vue

Project Structure โ€‹

element-plus-nuxt-app/
โ”œโ”€โ”€ .nuxt/                 # Auto-generated
โ”œโ”€โ”€ .output/               # Build output
โ”œโ”€โ”€ assets/                # Uncompiled assets
โ”‚   โ”œโ”€โ”€ css/
โ”‚   โ”œโ”€โ”€ images/
โ”‚   โ””โ”€โ”€ fonts/
โ”œโ”€โ”€ components/            # Vue components
โ”‚   โ”œโ”€โ”€ UI/
โ”‚   โ”‚   โ”œโ”€โ”€ AppHeader.vue
โ”‚   โ”‚   โ”œโ”€โ”€ AppFooter.vue
โ”‚   โ”‚   โ””โ”€โ”€ AppNavigation.vue
โ”‚   โ”œโ”€โ”€ Form/
โ”‚   โ”‚   โ”œโ”€โ”€ ContactForm.vue
โ”‚   โ”‚   โ””โ”€โ”€ SearchForm.vue
โ”‚   โ””โ”€โ”€ Layout/
โ”‚       โ”œโ”€โ”€ DefaultLayout.vue
โ”‚       โ””โ”€โ”€ AdminLayout.vue
โ”œโ”€โ”€ composables/           # Vue composables
โ”‚   โ”œโ”€โ”€ useAuth.js
โ”‚   โ”œโ”€โ”€ useApi.js
โ”‚   โ””โ”€โ”€ useTheme.js
โ”œโ”€โ”€ layouts/               # Layout components
โ”‚   โ”œโ”€โ”€ default.vue
โ”‚   โ”œโ”€โ”€ admin.vue
โ”‚   โ””โ”€โ”€ error.vue
โ”œโ”€โ”€ middleware/            # Route middleware
โ”‚   โ”œโ”€โ”€ auth.js
โ”‚   โ””โ”€โ”€ admin.js
โ”œโ”€โ”€ pages/                 # File-based routing
โ”‚   โ”œโ”€โ”€ index.vue
โ”‚   โ”œโ”€โ”€ about.vue
โ”‚   โ”œโ”€โ”€ contact.vue
โ”‚   โ”œโ”€โ”€ admin/
โ”‚   โ”‚   โ”œโ”€โ”€ index.vue
โ”‚   โ”‚   โ”œโ”€โ”€ users.vue
โ”‚   โ”‚   โ””โ”€โ”€ settings.vue
โ”‚   โ””โ”€โ”€ blog/
โ”‚       โ”œโ”€โ”€ index.vue
โ”‚       โ””โ”€โ”€ [slug].vue
โ”œโ”€โ”€ plugins/               # Nuxt plugins
โ”‚   โ”œโ”€โ”€ element-plus.client.js
โ”‚   โ””โ”€โ”€ api.js
โ”œโ”€โ”€ public/                # Static files
โ”œโ”€โ”€ server/                # Server-side code
โ”‚   โ””โ”€โ”€ api/
โ”‚       โ”œโ”€โ”€ users.js
โ”‚       โ””โ”€โ”€ posts.js
โ”œโ”€โ”€ stores/                # Pinia stores
โ”‚   โ”œโ”€โ”€ auth.js
โ”‚   โ”œโ”€โ”€ user.js
โ”‚   โ””โ”€โ”€ theme.js
โ”œโ”€โ”€ nuxt.config.ts         # Nuxt configuration
โ”œโ”€โ”€ app.vue                # Root component
โ””โ”€โ”€ package.json

Nuxt Configuration โ€‹

Basic Configuration โ€‹

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  // Enable SSR
  ssr: true,
  
  // Modules
  modules: [
    '@element-plus/nuxt',
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss', // Optional
    '@nuxt/image' // Optional for image optimization
  ],
  
  // Element Plus configuration
  elementPlus: {
    /** Options */
    themes: ['dark'],
    importStyle: 'scss', // or 'css'
    icon: 'ElIcon',
  },
  
  // CSS configuration
  css: [
    'element-plus/dist/index.css',
    '~/assets/css/main.scss'
  ],
  
  // Build configuration
  build: {
    transpile: ['element-plus']
  },
  
  // Vite configuration
  vite: {
    optimizeDeps: {
      include: [
        'element-plus/es',
        'element-plus/es/components/*/style/css',
        '@element-plus/icons-vue'
      ]
    }
  },
  
  // Runtime config
  runtimeConfig: {
    // Private keys (only available on server-side)
    apiSecret: '123',
    
    // Public keys (exposed to client-side)
    public: {
      apiBase: '/api',
      appName: 'Element Plus Nuxt App'
    }
  },
  
  // App configuration
  app: {
    head: {
      title: 'Element Plus Nuxt App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: 'Element Plus application built with Nuxt.js' }
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
      ]
    }
  },
  
  // Nitro configuration for server
  nitro: {
    preset: 'node-server' // or 'vercel', 'netlify', etc.
  }
})

Advanced Configuration โ€‹

typescript
// nuxt.config.ts - Advanced setup
export default defineNuxtConfig({
  // Performance optimizations
  experimental: {
    payloadExtraction: false, // Disable payload extraction for better performance
    inlineSSRStyles: false,   // Disable inline styles for better caching
  },
  
  // Route rules for different rendering strategies
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },
    
    // Admin dashboard rendered on-demand, revalidates in background
    '/admin/**': { ssr: true },
    
    // Blog posts generated on-demand, cached for 1 hour
    '/blog/**': { isr: 3600 },
    
    // API routes
    '/api/**': { cors: true, headers: { 'Access-Control-Allow-Origin': '*' } }
  },
  
  // Element Plus auto-import configuration
  elementPlus: {
    importStyle: 'scss',
    themes: ['dark'],
    
    // Custom component resolution
    include: ['**/*.vue', '**/*.md'],
    exclude: ['**/node_modules/**'],
    
    // Custom icon configuration
    icon: {
      autoInstall: true,
    },
    
    // Namespace configuration
    namespace: 'El'
  },
  
  // Custom webpack/vite configuration
  vite: {
    define: {
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
    },
    
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `
            @use "~/assets/scss/element-variables.scss" as *;
            @use "~/assets/scss/mixins.scss" as *;
          `
        }
      }
    }
  }
})

Component Integration โ€‹

Root App Component โ€‹

vue
<!-- app.vue -->
<template>
  <div id="app">
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

<script setup>
// Global app configuration
useHead({
  titleTemplate: '%s - Element Plus Nuxt App',
  meta: [
    { name: 'description', content: 'Modern Vue.js application with Element Plus and Nuxt.js' }
  ]
})

// Global error handling
const { $router } = useNuxtApp()

onErrorCaptured((error) => {
  console.error('Global error:', error)
  // You can send error to monitoring service here
  return false
})
</script>

<style>
/* Global styles */
html {
  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'ๅพฎ่ฝฏ้›…้ป‘', Arial, sans-serif;
}

body {
  margin: 0;
  padding: 0;
}

#app {
  min-height: 100vh;
}
</style>

Layout Components โ€‹

vue
<!-- layouts/default.vue -->
<template>
  <el-container class="app-container">
    <el-header class="app-header">
      <AppNavigation />
    </el-header>
    
    <el-main class="app-main">
      <slot />
    </el-main>
    
    <el-footer class="app-footer">
      <AppFooter />
    </el-footer>
  </el-container>
</template>

<script setup>
// Layout-specific meta
useHead({
  bodyAttrs: {
    class: 'default-layout'
  }
})

// Layout-specific composables
const { user } = useAuth()
const { theme } = useTheme()

// Provide layout context
provide('layout', 'default')
</script>

<style scoped>
.app-container {
  min-height: 100vh;
}

.app-header {
  background-color: var(--el-color-primary);
  color: white;
  padding: 0 20px;
}

.app-main {
  padding: 20px;
  min-height: calc(100vh - 120px);
}

.app-footer {
  background-color: var(--el-bg-color-page);
  text-align: center;
  padding: 20px;
}
</style>

Page Components โ€‹

vue
<!-- pages/index.vue -->
<template>
  <div class="home-page">
    <!-- SEO Meta Tags -->
    <Head>
      <Title>Home - Element Plus Nuxt App</Title>
      <Meta name="description" content="Welcome to our Element Plus Nuxt.js application" />
      <Meta property="og:title" content="Home - Element Plus Nuxt App" />
      <Meta property="og:description" content="Welcome to our Element Plus Nuxt.js application" />
    </Head>
    
    <!-- Hero Section -->
    <section class="hero">
      <el-row :gutter="20" align="middle">
        <el-col :span="12">
          <h1 class="hero-title">{{ pageData.title }}</h1>
          <p class="hero-description">{{ pageData.description }}</p>
          <el-button type="primary" size="large" @click="navigateToAbout">
            Get Started
            <el-icon class="el-icon--right"><ArrowRight /></el-icon>
          </el-button>
        </el-col>
        <el-col :span="12">
          <el-image 
            :src="pageData.heroImage" 
            alt="Hero Image"
            fit="cover"
            class="hero-image"
          />
        </el-col>
      </el-row>
    </section>
    
    <!-- Features Section -->
    <section class="features">
      <h2>Features</h2>
      <el-row :gutter="20">
        <el-col 
          v-for="feature in pageData.features" 
          :key="feature.id"
          :span="8"
        >
          <el-card class="feature-card" shadow="hover">
            <template #header>
              <div class="feature-header">
                <el-icon :size="24" :color="feature.color">
                  <component :is="feature.icon" />
                </el-icon>
                <h3>{{ feature.title }}</h3>
              </div>
            </template>
            <p>{{ feature.description }}</p>
          </el-card>
        </el-col>
      </el-row>
    </section>
    
    <!-- Stats Section -->
    <section class="stats">
      <el-row :gutter="20">
        <el-col 
          v-for="stat in pageData.stats" 
          :key="stat.label"
          :span="6"
        >
          <el-statistic 
            :value="stat.value"
            :title="stat.label"
            :suffix="stat.suffix"
          />
        </el-col>
      </el-row>
    </section>
  </div>
</template>

<script setup>
import { ArrowRight, Star, Trophy, Users, Rocket } from '@element-plus/icons-vue'

// Page data - could come from CMS or API
const { data: pageData } = await useFetch('/api/home-page')

// Or static data for demo
const pageData = ref({
  title: 'Welcome to Element Plus Nuxt',
  description: 'Build amazing applications with Vue.js, Nuxt.js, and Element Plus',
  heroImage: '/images/hero.jpg',
  features: [
    {
      id: 1,
      title: 'Server-Side Rendering',
      description: 'Built-in SSR support for better SEO and performance',
      icon: 'Rocket',
      color: '#409EFF'
    },
    {
      id: 2,
      title: 'Rich Components',
      description: 'Comprehensive set of high-quality Vue components',
      icon: 'Star',
      color: '#67C23A'
    },
    {
      id: 3,
      title: 'TypeScript Support',
      description: 'First-class TypeScript support out of the box',
      icon: 'Trophy',
      color: '#E6A23C'
    }
  ],
  stats: [
    { label: 'Components', value: 80, suffix: '+' },
    { label: 'Downloads', value: 2.5, suffix: 'M+' },
    { label: 'GitHub Stars', value: 20, suffix: 'K+' },
    { label: 'Contributors', value: 300, suffix: '+' }
  ]
})

// Navigation method
const navigateToAbout = () => {
  navigateTo('/about')
}

// Page-specific meta
useHead({
  title: 'Home',
  meta: [
    { name: 'keywords', content: 'Vue.js, Nuxt.js, Element Plus, SSR' }
  ]
})
</script>

<style scoped>
.home-page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

.hero {
  padding: 60px 0;
  text-align: center;
}

.hero-title {
  font-size: 3rem;
  font-weight: bold;
  margin-bottom: 1rem;
  color: var(--el-text-color-primary);
}

.hero-description {
  font-size: 1.2rem;
  color: var(--el-text-color-regular);
  margin-bottom: 2rem;
}

.hero-image {
  width: 100%;
  height: 400px;
  border-radius: 8px;
}

.features {
  padding: 60px 0;
}

.features h2 {
  text-align: center;
  margin-bottom: 40px;
  font-size: 2.5rem;
}

.feature-card {
  height: 100%;
  transition: transform 0.3s ease;
}

.feature-card:hover {
  transform: translateY(-5px);
}

.feature-header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.feature-header h3 {
  margin: 0;
  font-size: 1.2rem;
}

.stats {
  padding: 60px 0;
  background-color: var(--el-bg-color-page);
  border-radius: 8px;
  margin: 40px 0;
}

@media (max-width: 768px) {
  .hero-title {
    font-size: 2rem;
  }
  
  .hero-description {
    font-size: 1rem;
  }
}
</style>

Composables and Utilities โ€‹

Authentication Composable โ€‹

javascript
// composables/useAuth.js
export const useAuth = () => {
  const user = useState('auth.user', () => null)
  const isLoggedIn = computed(() => !!user.value)
  
  const login = async (credentials) => {
    try {
      const { data } = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials
      })
      
      user.value = data.user
      
      // Set auth cookie
      const token = useCookie('auth-token', {
        default: () => null,
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 24 * 7 // 7 days
      })
      
      token.value = data.token
      
      return { success: true }
    } catch (error) {
      return { success: false, error: error.message }
    }
  }
  
  const logout = async () => {
    try {
      await $fetch('/api/auth/logout', { method: 'POST' })
      
      user.value = null
      
      const token = useCookie('auth-token')
      token.value = null
      
      await navigateTo('/login')
    } catch (error) {
      console.error('Logout error:', error)
    }
  }
  
  const fetchUser = async () => {
    try {
      const data = await $fetch('/api/auth/me')
      user.value = data.user
    } catch (error) {
      user.value = null
    }
  }
  
  return {
    user: readonly(user),
    isLoggedIn,
    login,
    logout,
    fetchUser
  }
}

API Composable โ€‹

javascript
// composables/useApi.js
export const useApi = () => {
  const config = useRuntimeConfig()
  
  const api = $fetch.create({
    baseURL: config.public.apiBase,
    
    onRequest({ request, options }) {
      // Add auth token
      const token = useCookie('auth-token')
      if (token.value) {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${token.value}`
        }
      }
    },
    
    onRequestError({ request, options, error }) {
      console.error('Request error:', error)
    },
    
    onResponse({ request, response, options }) {
      // Handle successful responses
    },
    
    onResponseError({ request, response, options }) {
      // Handle error responses
      if (response.status === 401) {
        // Redirect to login
        navigateTo('/login')
      }
    }
  })
  
  return {
    api
  }
}

Theme Composable โ€‹

javascript
// composables/useTheme.js
export const useTheme = () => {
  const theme = useState('theme', () => 'light')
  
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
    
    // Update document class
    if (process.client) {
      document.documentElement.className = theme.value
    }
    
    // Save to localStorage
    const themeCookie = useCookie('theme', {
      default: () => 'light',
      maxAge: 60 * 60 * 24 * 365 // 1 year
    })
    themeCookie.value = theme.value
  }
  
  const setTheme = (newTheme) => {
    theme.value = newTheme
    
    if (process.client) {
      document.documentElement.className = newTheme
    }
    
    const themeCookie = useCookie('theme')
    themeCookie.value = newTheme
  }
  
  // Initialize theme on client
  onMounted(() => {
    const savedTheme = useCookie('theme').value
    if (savedTheme) {
      setTheme(savedTheme)
    }
  })
  
  return {
    theme: readonly(theme),
    toggleTheme,
    setTheme
  }
}

Server API Routes โ€‹

Authentication API โ€‹

javascript
// server/api/auth/login.post.js
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  // Validate credentials
  const { email, password } = body
  
  if (!email || !password) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email and password are required'
    })
  }
  
  // Authenticate user (replace with your auth logic)
  const user = await authenticateUser(email, password)
  
  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid credentials'
    })
  }
  
  // Generate JWT token
  const token = generateJWT(user)
  
  // Set secure cookie
  setCookie(event, 'auth-token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7 // 7 days
  })
  
  return {
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role
    },
    token
  }
})

// Helper functions
async function authenticateUser(email, password) {
  // Replace with your authentication logic
  // This could be database lookup, LDAP, etc.
  return {
    id: 1,
    email,
    name: 'John Doe',
    role: 'user'
  }
}

function generateJWT(user) {
  // Replace with your JWT generation logic
  return 'jwt-token-here'
}

Data API โ€‹

javascript
// server/api/users.get.js
export default defineEventHandler(async (event) => {
  // Get query parameters
  const query = getQuery(event)
  const { page = 1, limit = 10, search = '' } = query
  
  // Validate authentication
  const token = getCookie(event, 'auth-token')
  if (!token) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Authentication required'
    })
  }
  
  // Fetch users (replace with your data source)
  const users = await fetchUsers({
    page: parseInt(page),
    limit: parseInt(limit),
    search
  })
  
  return {
    data: users,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total: users.length
    }
  }
})

async function fetchUsers(options) {
  // Replace with your data fetching logic
  return [
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
  ]
}

Middleware โ€‹

Authentication Middleware โ€‹

javascript
// middleware/auth.js
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuth()
  
  if (!isLoggedIn.value) {
    return navigateTo('/login')
  }
})

Admin Middleware โ€‹

javascript
// middleware/admin.js
export default defineNuxtRouteMiddleware((to, from) => {
  const { user } = useAuth()
  
  if (!user.value || user.value.role !== 'admin') {
    throw createError({
      statusCode: 403,
      statusMessage: 'Access denied'
    })
  }
})

Plugins โ€‹

Element Plus Plugin โ€‹

javascript
// plugins/element-plus.client.js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(ElementPlus)
  
  // Register all icons
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    nuxtApp.vueApp.component(key, component)
  }
})

Performance Optimization โ€‹

Lazy Loading Components โ€‹

vue
<template>
  <div>
    <!-- Lazy load heavy components -->
    <LazyDataTable v-if="showTable" :data="tableData" />
    <LazyChart v-if="showChart" :data="chartData" />
  </div>
</template>

<script setup>
// Components are automatically lazy-loaded with 'Lazy' prefix
const showTable = ref(false)
const showChart = ref(false)

// Load components when needed
onMounted(() => {
  // Load table after initial render
  nextTick(() => {
    showTable.value = true
  })
  
  // Load chart after user interaction
  setTimeout(() => {
    showChart.value = true
  }, 2000)
})
</script>

Image Optimization โ€‹

vue
<template>
  <div>
    <!-- Optimized images with Nuxt Image -->
    <NuxtImg
      src="/images/hero.jpg"
      alt="Hero Image"
      width="800"
      height="400"
      format="webp"
      quality="80"
      loading="lazy"
      placeholder
    />
    
    <!-- Responsive images -->
    <NuxtPicture
      src="/images/gallery/image1.jpg"
      alt="Gallery Image"
      :img-attrs="{
        class: 'gallery-image',
        style: 'display: block'
      }"
      sizes="sm:100vw md:50vw lg:400px"
    />
  </div>
</template>

Deployment โ€‹

Build Configuration โ€‹

bash
# Build for production
npm run build

# Preview production build
npm run preview

# Generate static site (if using SSG)
npm run generate

Environment Variables โ€‹

bash
# .env
NUXT_API_SECRET=your-secret-key
NUXT_PUBLIC_API_BASE=https://api.example.com
NUXT_PUBLIC_APP_NAME="Element Plus Nuxt App"

Docker Deployment โ€‹

dockerfile
# Dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
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"]

Best Practices โ€‹

Performance โ€‹

  1. Use Route Rules: Configure different rendering strategies per route
  2. Optimize Images: Use Nuxt Image for automatic optimization
  3. Lazy Load Components: Use the Lazy prefix for heavy components
  4. Cache API Responses: Implement proper caching strategies
  5. Bundle Analysis: Regularly analyze bundle size

SEO โ€‹

  1. Meta Tags: Use useHead() for dynamic meta tags
  2. Structured Data: Add JSON-LD structured data
  3. Sitemap: Generate sitemap automatically
  4. Robots.txt: Configure search engine crawling

Security โ€‹

  1. Environment Variables: Keep secrets in environment variables
  2. CSRF Protection: Implement CSRF protection for forms
  3. Rate Limiting: Add rate limiting to API routes
  4. Input Validation: Validate all user inputs

Development โ€‹

  1. TypeScript: Use TypeScript for better development experience
  2. ESLint/Prettier: Maintain consistent code style
  3. Testing: Write unit and integration tests
  4. Documentation: Document components and APIs

This comprehensive guide provides everything needed to build production-ready applications with Element Plus and Nuxt.js, leveraging the full power of server-side rendering while maintaining excellent developer experience.

Element Plus Study Guide