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 โ
- Zero Configuration SSR: Built-in SSR support without complex setup
- Automatic Code Splitting: Route-based code splitting out of the box
- File-based Routing: Automatic route generation from file structure
- Built-in Performance Optimizations: Image optimization, lazy loading, etc.
- Rich Ecosystem: Extensive module ecosystem for common features
- TypeScript Support: First-class TypeScript integration
Nuxt.js vs Custom SSR Setup โ
Feature | Custom SSR | Nuxt.js |
---|---|---|
Setup Complexity | High | Low |
Configuration | Manual | Convention-based |
Code Splitting | Manual | Automatic |
SEO Features | Manual | Built-in |
Performance | Custom optimization | Optimized by default |
Learning Curve | Steep | Moderate |
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 โ
- Use Route Rules: Configure different rendering strategies per route
- Optimize Images: Use Nuxt Image for automatic optimization
- Lazy Load Components: Use the
Lazy
prefix for heavy components - Cache API Responses: Implement proper caching strategies
- Bundle Analysis: Regularly analyze bundle size
SEO โ
- Meta Tags: Use
useHead()
for dynamic meta tags - Structured Data: Add JSON-LD structured data
- Sitemap: Generate sitemap automatically
- Robots.txt: Configure search engine crawling
Security โ
- Environment Variables: Keep secrets in environment variables
- CSRF Protection: Implement CSRF protection for forms
- Rate Limiting: Add rate limiting to API routes
- Input Validation: Validate all user inputs
Development โ
- TypeScript: Use TypeScript for better development experience
- ESLint/Prettier: Maintain consistent code style
- Testing: Write unit and integration tests
- 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.