We ship new Nuxt projects every few months, and every time we onboard a team member, the same question surfaces: "Where do we add UI components, image optimization, and SEO?" The answer invariably points to Nuxt modules. Understanding how to use and build them isn't optional for production teams—it's fundamental to how we extend Nuxt quickly and keep our codebases maintainable across projects.
Nuxt modules are packages that hook into the Nuxt build process and runtime to add features without duplicating boilerplate. They're the backbone of how the ecosystem works, and knowing when to reach for an existing module versus building your own will save your team days of wiring.
What Nuxt modules are and why they matter
A Nuxt module is a function that runs during the build phase to configure, extend, or inject features into your Nuxt application. Think of it as middleware for your project setup: it can add auto-imports, register plugins, inject composables, configure webpack loaders, or hook into lifecycle events. Without modules, you'd manually wire every feature into nuxt.config, repeating the same setup across projects.
In production, we rely on modules to handle cross-cutting concerns: image optimization, SEO tags, analytics, form validation, UI component libraries. Using a well-maintained module means we get updates, bug fixes, and optimizations for free. Writing our own modules for shared business logic keeps that logic DRY across multiple Nuxt apps in our organization.
Nuxt UI: production-ready components out of the box
Nuxt UI is a headless component library built on Headless UI and Tailwind CSS. We install it when we need a solid foundation of buttons, modals, forms, and tables without designing from scratch.
npx nuxi module add uiThat single command adds Nuxt UI to your nuxt.config and installs dependencies. You immediately have access to components like <UButton />, <UModal />, and <UForm /> without manual imports.
Real scenario: a new SaaS dashboard needs a login form, a data table with sorting, and a settings modal. With Nuxt UI, we build it in hours instead of days because the components are already styled, accessible, and handle common patterns. Here's a quick example:
<template>
<div class="p-6">
<UCard>
<template #header>
<h1>Users</h1>
</template>
<UTable :rows="users" :columns="columns" />
<template #footer>
<UButton @click="openCreateModal">Add User</UButton>
</template>
</UCard>
</div>
</template>
<script setup lang="ts">
const users = ref([])
const columns = [{
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const openCreateModal = () => {
// Modal logic
}
</script>Configuration is minimal. Nuxt UI respects your Tailwind setup automatically. You can customize colors and spacing through nuxt.config or on individual components.
Nuxt Content: markdown-first content management
Nuxt Content lets us write blog posts, documentation, or changelog entries in markdown and query them as structured data. The module automatically parses frontmatter, handles file-based routing, and provides a composable API.
npx nuxi module add contentCreate a content/ directory and add markdown files:
content/
blog/
day-43-nuxt-modules.md
getting-started.md
docs/
installation.mdA markdown file with frontmatter looks like this:
---
title: Nuxt Modules Explained
publishedAt: 2024-01-15
tags: [nuxt, modules, production]
---
# Nuxt Modules Explained
Your content here...Now query it with queryContent():
<template>
<div>
<h1>Blog</h1>
<article v-for="post in posts" :key="post._id">
<h2>{{ post.title }}</h2>
<time>{{ formatDate(post.publishedAt) }}</time>
<p>{{ post.description }}</p>
<NuxtLink :to="post._path">Read more</NuxtLink>
</article>
</div>
</template>
<script setup lang="ts">
const { data: posts } = await useAsyncData('blog', () =>
queryContent('blog').sort({ publishedAt: -1 }).find()
)
</script>Nuxt Content handles rendering markdown to HTML, parsing code blocks for syntax highlighting (via Shiki), and extracting a table of contents. This is game-changing for content-heavy apps: marketing sites, knowledge bases, product blogs. We've shipped three company blogs using it, and the developer experience is unmatched.
Nuxt Image: optimized images at scale
Nuxt Image replaces raw <img> tags with an intelligent component that optimizes, caches, and serves images in modern formats. It integrates with Cloudinary, Imgix, or your own image service.
npx nuxi module add imageConfigure in nuxt.config:
export default defineNuxtConfig({
image: {
provider: 'cloudinary',
cloudinary: {
baseURL: 'https://res.cloudinary.com/your-cloud/image/upload/'
}
}
})Then use <NuxtImg /> instead of <img />:
<template>
<NuxtImg
src="my-image.jpg"
width="800"
height="600"
alt="Product photo"
:preload="true"
/>
</template>The module automatically generates srcsets, lazy-loads images, and serves WebP to browsers that support it. For e-commerce apps handling thousands of product images, this reduces payload by 40–60%. We've watched Core Web Vitals improve dramatically after adding Nuxt Image to legacy projects.
Nuxt SEO: meta tags that actually work
Nuxt SEO (or @nuxtjs/seo) simplifies adding meta tags, Open Graph data, and structured schema markup for search engines and social sharing. Without it, you manually manage dozens of tags across layouts and pages.
npx nuxi module add seoDefine site-wide SEO in nuxt.config:
export default defineNuxtConfig({
seo: {
siteName: 'My SaaS',
description: 'Production-grade SaaS platform',
titleTemplate: '%s | My SaaS',
twitterCard: 'summary_large_image'
}
})Override per-page with defineRouteRules() or in middleware:
// pages/blog/[slug].vue
definePageMeta({
title: 'My Blog Post',
description: 'A deep dive into Vue.js',
ogImage: 'https://example.com/og-image.jpg',
robots: 'index, follow'
})
<template>
<article>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</article>
</template>The module injects structured data, handles image metadata, and ensures Twitter and LinkedIn preview your content correctly. For B2B SaaS, proper SEO setup directly impacts inbound marketing and discoverability.
Installing modules with nuxi module add
The modern way to add a module is the nuxi module add command. It installs the package, adds it to nuxt.config, and runs any setup wizards. Compare this to manually editing nuxt.config:
npx nuxi module add ui
npx nuxi module add content
npx nuxi module add image
npx nuxi module add seoVersus the old way (still works, but more manual):
npm install @nuxt/ui @nuxt/content @nuxt/image @nuxtjs/seoThen manually add to nuxt.config:
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxt/content',
'@nuxt/image',
'@nuxtjs/seo'
]
})The nuxi approach is faster and less error-prone. Always use it when available.
Configuring modules in nuxt.config
Once installed, modules are configured in your nuxt.config.ts file. Configuration can be simple (a boolean) or complex (nested objects with callbacks).
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxt/content',
'@nuxt/image',
'@nuxtjs/seo'
],
// Per-module configuration
ui: {
icons: ['heroicons', 'mdi'],
safelistColors: ['red', 'blue', 'green']
},
content: {
markdown: {
tags: {
p: 'ContentParagraph'
}
}
},
image: {
provider: 'cloudinary',
cloudinary: {
baseURL: 'https://res.cloudinary.com/my-cloud/image/upload/'
},
quality: 80,
format: ['webp', 'jpg']
},
seo: {
redirectToCanonicalSiteUrl: true,
strict: true
}
})Each module documents its configuration options. Don't guess—check the module's README. Most modules also accept environment variables for sensitive data (API keys, cloud URLs).
Writing your own modules for shared logic
After building your fifth Nuxt app, you'll notice patterns. Maybe you always need a unified logging system, a custom auth middleware, or a shared API client. This is where writing your own module pays dividends.
A minimal Nuxt module is a function that receives the Nuxt instance and applies transformations:
// modules/shared-auth.ts
export default defineNuxtModule({
meta: {
name: 'shared-auth',
configKey: 'sharedAuth'
},
defaults: {
apiBaseUrl: 'https://api.example.com'
},
setup(options, nuxt) {
// Add a plugin
addPlugin(resolve('./runtime/auth.plugin.ts'))
// Add a composable
addImports({
name: 'useAuthUser',
from: resolve('./runtime/composables/auth.ts')
})
// Inject configuration
nuxt.options.runtimeConfig.public.authApiUrl = options.apiBaseUrl
}
})Create a corresponding plugin and composable:
// modules/runtime/auth.plugin.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
// Set up interceptors, listeners, etc.
console.log('Auth initialized with:', config.public.authApiUrl)
})
// modules/runtime/composables/auth.ts
export const useAuthUser = () => {
const user = useState('auth.user', () => null)
const isAuthenticated = computed(() => !!user.value)
return { user, isAuthenticated }
}Register the module in nuxt.config:
export default defineNuxtConfig({
modules: [
'./modules/shared-auth.ts'
],
sharedAuth: {
apiBaseUrl: 'https://api.acme.com'
}
})Now every app that imports this module has the auth composable, the plugin, and the configuration baked in. If you fix a bug in the auth logic, you update the module once and all apps using it get the fix. This scales much better than copy-pasting across projects.
For teams maintaining multiple Nuxt apps, creating an internal module package (published to npm or a private registry) is standard practice. We've built modules for analytics tracking, error handling, form validation, and API integration, all packaged as @acme/nuxt-* modules and versioned like any other dependency.
Common mistakes
- Not checking module compatibility: Modules are versioned against specific Nuxt versions. Always check the module's README for version constraints. Installing a Nuxt 3 module in a Nuxt 2 project will fail silently in confusing ways.
- Over-configuring too early: Modules have sensible defaults. Don't customize until you need to. This keeps your
nuxt.configreadable and reduces future maintenance. - Reinventing the wheel: Before writing a module, search npm. The ecosystem is mature;
@nuxt/ui,@nuxt/content, and@nuxtjs/seosolve 80% of common needs. Only write a module if you're solving a team-specific problem. - Forgetting runtime configuration: If your module needs API keys or URLs that change per environment, use
runtimeConfig, not static config. This prevents baking secrets into your build. - Not testing modules in isolation: If you write a module, test it separately before using it in your app. A broken module can fail the entire build, so testing in isolation catches issues early.
Key takeaways
- Nuxt modules hook into the build process to add features without boilerplate; they're essential for production teams.
- Nuxt UI, Content, Image, and SEO are industry-standard modules that handle UI components, markdown content, image optimization, and search engine metadata respectively.
- Install modules with
npx nuxi module add, not manual installation; it's faster and handles setup automatically. - Configure modules in
nuxt.configwith per-module options; check each module's documentation for available settings. - Write your own modules for shared business logic across multiple Nuxt apps; publish internally and version them like regular dependencies.
- Always verify module compatibility with your Nuxt version before adding; use sensible defaults and only customize when necessary.






Comments
Loading comments…