Blogs
Handling Content in Nuxt
NuxtModulesIf you're building a blog, documentation site, or any content-heavy app with Nuxt, you'll want to look at
@nuxt/content. It turns your markdown files into a queryable data layer with zero database setup. In this post, we'll go from a basic markdown page to a full content list with filtering and navigation.Setting Things Up
First, let's add the module to an existing Nuxt project:npx nuxi module add contentThis registers
@nuxt/contentin yournuxt.config.tsautomatically:// nuxt.config.ts export default defineNuxtConfig({ modules: ['@nuxt/content'], });Now create a
content/directory at the root of your project (so outsideappfolder if you’re using Nuxt v4). Any.mdfile you place in here becomes queryable content.Rendering a Single Page
Let's start simple. Create a markdown file:
<!-- content/hello.md --> --- title: Hello World description: My first content page date: 2026-03-20 --- # Hello World This is a content page rendered by `@nuxt/content`.To render it, we need a catch-all route. Create a page with the
<ContentDoc>component:<!-- pages/[...slug].vue --> <template> <main> <ContentDoc /> </main> </template>
That's it. Navigate to/helloand you'll see your markdown rendered as HTML. The<ContentDoc>component handles fetching and rendering based on the current route path.
Customizing the Rendered OutputThe default rendering works, but you probably want control over the layout. We can use the
v-slotto access the document data:<!-- pages/[...slug].vue --> <template> <main> <ContentDoc v-slot="{ doc }"> <article> <h1>{{ doc.title }}</h1> <p class="text-gray-500">{{ doc.description }}</p> <time>{{ new Date(doc.date).toLocaleDateString() }}</time> <ContentRenderer :value="doc" /> </article> </ContentDoc> </main> </template>Here we pull the frontmatter fields (
title,description,date) out of the document and render them ourselves. The<ContentRenderer>component takes care of the actual markdown body.Querying Content with queryContent()
Rendering a single page is fine, but the real power comes from querying. Let's build a blog index that lists all posts.
Create a few more markdown files in
content/blog/:<!-- content/blog/first-post.md --> --- title: First Post description: Getting started with Nuxt Content date: 2026-03-15 tags: ["nuxt", "getting-started"] --- The body of the first post.<!-- content/blog/second-post.md --> --- title: Second Post description: Going deeper with queries date: 2026-03-20 tags: ["nuxt", "advanced"] --- The body of the second post.Now let's query them:
<!-- pages/blog/index.vue --> <script setup lang="ts"> const { data: posts } = await useAsyncData('blog-posts', () => queryContent('blog') .sort({ date: -1 }) .find() ); </script> <template> <main> <h1>Blog</h1> <ul> <li v-for="post in posts" :key="post._path"> <NuxtLink :to="post._path"> <h2>{{ post.title }}</h2> <p>{{ post.description }}</p> </NuxtLink> </li> </ul> </main> </template>queryContent('blog')scopes the query to thecontent/blog/directory. We sort by date descending and call.find()to get all matching documents. Each document comes with_pathwhich maps directly to the route.Wrapping the query in
useAsyncDatais important. It makes sure the data is fetched on the server during SSR and properly hydrated on the client.Adding Filters
Let's say we want to filter posts by tag. We can chain
.where()onto the query:<!-- pages/blog/index.vue --> <script setup lang="ts"> const route = useRoute(); const activeTag = computed(() => route.query.tag as string | undefined); const { data: posts } = await useAsyncData( `blog-posts-${activeTag.value || 'all'}`, () => { const query = queryContent('blog').sort({ date: -1 }); if (activeTag.value) { query.where({ tags: { $contains: activeTag.value } }); } return query.find(); }, { watch: [activeTag] } ); </script> <template> <main> <h1>Blog</h1> <nav> <NuxtLink to="/blog">All</NuxtLink> <NuxtLink to="/blog?tag=nuxt">Nuxt</NuxtLink> <NuxtLink to="/blog?tag=advanced">Advanced</NuxtLink> </nav> <ul> <li v-for="post in posts" :key="post._path"> <NuxtLink :to="post._path"> <h2>{{ post.title }}</h2> <p>{{ post.description }}</p> </NuxtLink> </li> </ul> </main> </template>A few things to note here. The
$containsoperator checks if thetagsarray includes the given value. We pass{ watch: [activeTag] }touseAsyncDataso the query re-runs when the tag filter changes. The cache key includes the active tag to avoid stale data across filters.Selecting Only What You Need
When listing posts, you don't need the full markdown body. Use
.only()to select specific fields:const { data: posts } = await useAsyncData('blog-list', () => queryContent('blog') .only(['title', 'description', 'date', '_path', 'tags']) .sort({ date: -1 }) .find() );This keeps the payload small, especially when you have a lot of content.
Building Content Navigation
For documentation sites, you often want a sidebar that reflects the content structure. Nuxt Content provides
fetchContentNavigation()for this:<!-- components/ContentSidebar.vue --> <script setup lang="ts"> const { data: navigation } = await useAsyncData('content-nav', () => fetchContentNavigation() ); </script> <template> <aside> <ul v-if="navigation"> <li v-for="item in navigation" :key="item._path"> <NuxtLink :to="item._path">{{ item.title }}</NuxtLink> <ul v-if="item.children?.length"> <li v-for="child in item.children" :key="child._path"> <NuxtLink :to="child._path">{{ child.title }}</NuxtLink> </li> </ul> </li> </ul> </aside> </template>The navigation tree is auto-generated from your
content/directory structure. Folders become parent items and markdown files become children. Thetitlefield comes from each file's frontmatter.Using Components Inside Markdown
One of the best features of
@nuxt/contentis that you can use Vue components directly in your markdown files. Create a component incomponents/content/:<!-- components/content/Callout.vue --> <script setup lang="ts"> defineProps<{ type: 'info' | 'warning' | 'tip'; }>(); </script> <template> <div :class="`callout callout-${type}`"> <slot /> </div> </template>Then use it in any markdown file:
<!-- content/blog/third-post.md --> Here is some regular markdown. ::callout{type="tip"} You can use Vue components directly inside markdown files. :: Back to regular markdown.The
::component-name{props}syntax is part of MDC (Markdown Components), which@nuxt/contentsupports out of the box.Key Takeaways
@nuxt/contentturns markdown files in thecontent/directory into a queryable data layer<ContentDoc>handles single-page rendering, whilequeryContent()gives you full query controlAlways wrap content queries in
useAsyncDatafor proper SSR and hydrationUse
.only()when listing content to keep payloads smallfetchContentNavigation()auto-generates navigation from your directory structureMDC syntax lets you embed Vue components directly in markdown
Conclusion
We covered the core workflow for handling content in Nuxt: rendering single pages, building queryable lists with filtering, generating navigation, and embedding Vue components in markdown. The module does a lot of the heavy lifting so you can focus on the content itself.
I hope this post has been helpful. Please share any questions or comments. Happy coding!
Building a Full-Stack app with Nuxt and the Supabase MCP
VueNuxtYour AI agent could create database tables, write server routes and build the UI all from a single conversation.
It’s not a hypothetical scenario. With agentic AI and the Supabase MCP, your AI agent gets direct access to your database. It creates schemas, sets up row-level security, generates API routes that match the schema it just built, and constructs a frontend that fits it all together. So you can build full stack with one conversation.
Here's how to build a task manager with Nuxt and Supabase, guided entirely by an AI agent.
Your Agent's Database Superpower with Supabase MCP
MCP (Model Context Protocol) is a standard that gives AI agents access to external tools and services. The Supabase MCP gives your agent the ability to interact with your Supabase project directly and create tables, manage schemas, run queries, and configure policies.
Configure it in your Claude Code settings:{ "mcpServers": { "supabase": { "command": "npx", "args": [ "-y", "@supabase/mcp-server-supabase@latest", "--access-token", "YOUR_SUPABASE_ACCESS_TOKEN" ] } } }
Once connected, the agent can "see" your database the same way you see it in the Supabase dashboard. It can list tables, inspect columns, create new schemas, and apply RLS policies. The agent doesn't just generate SQL for you to copy-paste, it has the power to execute it too.
This is the key difference between agentic AI and a simple code generator. The agent acts on your behalf across multiple systems and maintains context the entire time.
Schema Design Through ConversationStart with the data model. Instead of opening the Supabase dashboard and clicking through the table editor, describe what you need:
"Create a tasks table with columns: id (uuid, primary key, default gen_random_uuid()), title (text, not null), description (text), status (text, default 'todo', check constraint for 'todo', 'in_progress', 'done'), priority (text, default 'medium', check constraint for 'low', 'medium', 'high'), due_date (timestamptz), created_at (timestamptz, default now()), user_id (uuid, references auth.users)."
The agent calls the Supabase MCP, creates the table, and confirms the schema. You can verify it immediately: "Show me the current schema for the tasks table.”
Now add security:"Enable row-level security on the tasks table. Add policies so authenticated users can only select, insert, update, and delete their own tasks where user_id matches auth.uid()."
The agent applies four RLS policies through the MCP. You don’t have to maintain SQL files or run migrations manually. The database is ready.
Adding Server Routes and API LayerWith the schema in place, the agent already knows your data structure. This means the API layer it generates will match the database exactly (so no documentation drift and no type mismatches).
"Create Nuxt server routes for CRUD operations on the tasks table. Put them in server/api/tasks/. Use the Supabase client from @supabase/supabase-js."
The agent generates a complete set of routes. Here's what the list endpoint looks like:// server/api/tasks/index.get.ts import { serverSupabaseClient, serverSupabaseUser } from '#supabase/server' export default defineEventHandler(async (event) => { const client = await serverSupabaseClient(event) const user = await serverSupabaseUser(event) const { data, error } = await client .from('tasks') .select('*') .eq('user_id', user.id) .order('created_at', { ascending: false }) if (error) { throw createError({ statusCode: 500, message: error.message }) } return data })
Notice: the column names match the schema. Theuser_idfilter matches the RLS policy logic. The ordering usescreated_at, which the agent knows exists because it created the column.
Building the UIFor the frontend, the agent has full context: it knows the schema, the API routes, and the data shapes.
"Create a TaskBoard component with three columns: Todo, In Progress, and Done. Fetch tasks from /api/tasks and group them by status. Use Tailwind CSS for styling."<script setup lang="ts"> interface Task { id: string title: string description: string | null status: 'todo' | 'in_progress' | 'done' priority: 'low' | 'medium' | 'high' due_date: string | null created_at: string } const { data: tasks, refresh } = await useFetch<Task[]>('/api/tasks') const columns = computed(() => ({ todo: tasks.value?.filter(t => t.status === 'todo') ?? [], in_progress: tasks.value?.filter(t => t.status === 'in_progress') ?? [], done: tasks.value?.filter(t => t.status === 'done') ?? [], })) async function updateStatus(taskId: string, status: Task['status']) { await $fetch(`/api/tasks/${taskId}`, { method: 'PATCH', body: { status }, }) await refresh() } </script> <template> <div class="grid grid-cols-3 gap-6"> <div v-for="(col, key) in columns" :key="key" class="space-y-4"> <h2 class="text-lg font-semibold capitalize"> {{ key.replace('_', ' ') }} </h2> <div v-for="task in col" :key="task.id" class="rounded-lg border p-4 shadow-sm" > <h3 class="font-medium">{{ task.title }}</h3> <p v-if="task.description" class="mt-1 text-sm text-gray-600"> {{ task.description }} </p> <span class="mt-2 inline-block rounded-full px-2 py-1 text-xs" :class="{ 'bg-red-100 text-red-700': task.priority === 'high', 'bg-yellow-100 text-yellow-700': task.priority === 'medium', 'bg-green-100 text-green-700': task.priority === 'low', }" > {{ task.priority }} </span> </div> </div> </div> </template>
TheTaskinterface matches the database schema exactly and the status values correspond to the check constraint. The priority badge colors are a design decision the agent made but it’s something you can refine with a follow-up prompt.
Want drag-and-drop? "Add drag-and-drop with vuedraggable so users can move tasks between columns. Call updateStatus when a task is dropped." The agent extends what's already there.
The Agentic WorkflowLook at what just happened across one conversation:
Database schema created through the Supabase MCP — no SQL files, no dashboard clicking
Security policies applied directly to the database
API routes generated with full schema awareness
Frontend components built with types matching the database
Each step informed the next and the agent didn't forget the schema when writing routes. It didn't forget the API shape when building components. MCPs are what make this continuity possible because the agent interacts with real services and carries that context forward.
What This Means for YouThe Supabase MCP is one example. The pattern is what matters. Stripe MCP for payment flows. GitHub MCP for repository management. Any service with an MCP server becomes something your AI agent can operate directly.
Full-stack development doesn't get simpler but the mechanical parts get faster. You still need to know what RLS policies your app needs, what data model supports your features, how Nuxt server routes work. The agent handles the translation from your decisions to working code and configured services.Building a Blog app with Nuxt Content and Agentic AI
VueNuxtYou can go from zero to a fully styled Nuxt Content blog in under 30 minutes, but you have to know how to talk to your AI agent.
Not "autocomplete" or Copilot suggesting the next line while you type. Agentic AI is where you describe what you want, and the agent scaffolds your project, installs dependencies, writes components, and iterates based on your feedback. All in one conversation.
If you're a developer in 2026, this changes how you work. Here's how:
What Are MCPs?The thing that makes agentic AI actually useful for real projects is MCP (Model Context Protocol).
Think of MCPs as plugins for your AI agent. They give the agent access to tools and data it wouldn't otherwise have: maybe your filesystem, a database, documentation, deployment services. Without MCPs, the agent is guessing about your project structure. With them, it knows.Here's what an MCP configuration looks like in Claude Code:
{ "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@anthropic-ai/mcp-filesystem", "/path/to/your/project" ] } } }That's it. Now the agent can read your project files, understand your directory structure, and make informed decisions about where to put things and how to name them.
Setting Up: Nuxt + Claude CodeYou'll need Node.js (best is to use the LTS version), and Claude Code installed (
npm install -g @anthropic-ai/claude-code). That's the entire prerequisites list.
Open your terminal, navigate to where you want your project, and start a conversation:claude "Create a Nuxt 4 app with Nuxt Content module for a blog project. Use TypeScript, add Tailwind CSS for styling."
The agent doesn't just generate a file and hand it to you. It runs the commands:npx nuxi init, installs@nuxt/contentand@nuxtjs/tailwindcss, configuresnuxt.config.ts, creates the content directory, and sets up the initial file structure. You watch it happen in real-time.
Guiding the Agent to Building ComponentsHere's where the skill shift happens. You're not writing code, you're directing code. And the quality of your direction determines the quality of the output.
A vague prompt:
"Make a blog page"
A good prompt:"Create a BlogList component in components/blog/BlogList.vue that uses queryContent() to fetch all markdown posts from content/blog/. Display them as cards showing the title, published date, and a 2-line excerpt. Sort by date descending. Use Tailwind for styling."
The second prompt gives the agent everything it needs. It knows the file path, the data source, the display requirements, and the styling approach. The generated component:<script setup lang="ts"> const { data: posts } = await useAsyncData('blog-posts', () => queryContent('blog') .sort({ date: -1 }) .find() ) </script> <template> <div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <NuxtLink v-for="post in posts" :key="post._path" :to="post._path" class="block rounded-lg border p-6 hover:shadow-lg transition-shadow" > <time class="text-sm text-gray-500"> {{ new Date(post.date).toLocaleDateString() }} </time> <h2 class="mt-2 text-xl font-semibold">{{ post.title }}</h2> <p class="mt-2 text-gray-600 line-clamp-2">{{ post.description }}</p> </NuxtLink> </div> </template>
And now you can iterate to add tag filtering or show tag chips above the grid, clicking one filters the posts. The agent reads the existing component, understands its structure, and extends it. No context is lost here and there is no need to start over.
This is the core loop: prompt, review, refine. You bring the product vision. The agent handles the implementation.
The MCP Advantage: Context-Aware Development
Here's what separates agentic AI with MCPs from a chatbot that generates code snippets.
When you ask the agent to "add a dark mode toggle," it doesn't generate a generic toggle component. It uses MCP tools to read yournuxt.config.ts, checks that you're using Tailwind, reads your existing layout component, and generates a toggle that uses Tailwind'sdark:variant withuseColorMode()from@nuxtjs/color-modewhich it also installs and configures.
Without MCP context, you get generic code you have to adapt. With it, you get code that fits your project.
The agent reads your conventions. If your existing components usedefinePropswith TypeScript interfaces, the new ones will too. If you use<script setup>, so will the generated code. The game changing factor is that MCPs give the agent eyes into your codebase.
Final touches with Deploying and PolishingThe last mile is where agentic AI really shines, because it's the part developers usually rush through.
"Add SEO meta tags to all blog pages using useHead(). Use the post title for og:title, description for og:description.""Add an RSS feed using the @nuxt/content hooks.""Configure the app for Vercel deployment with proper nitro preset."
Each of these is a 30-second prompt that would otherwise be 15 minutes of documentation reading and boilerplate writing. Now the agent handles the wiring and you make the decisions.
What This Means for DevelopersAgentic AI doesn't replace your expertise but it amplifies it. The better you understand Nuxt's (or any other framework’s) conventions, the better you can guide the agent. You still need to know that
queryContent()exists, thatuseHead()handles meta tags, that Nitro presets control deployment targets.
The difference is that you no longer spend time on the mechanical translation of knowledge into code. You spend it on thinking about what to build, not how to type it.
MCPs are the bridge between "generic AI that writes JavaScript" and "an AI that understands your Nuxt project." Set them up. and investigate in writing good prompts.Nuxt UI: Unapologetically complete
ModulesWeb development moves very fast these days and honestly, picking the right UI setup can either make your life chill or total chaos. What Nuxt UI offers is basically a UI plug-and-play kit for Nuxt and Vue apps. It smashes together Tailwind CSS, Reka UI, and a ton of useful components into one neat package so you don’t end up gluing 5 different libraries like a mad scientist.
So… what even is Nuxt UI?
It’s an open-source UI library with 100+ ready-to-use and tweakable components, built with Nuxt in mind so it just feels natural, like it belongs there. No alien dependencies, no weird setup rituals. Just vibes.
Why it’s so popularYou get EVERYTHING in one box
Buttons, forms, modals, dashboards, icons: all styled, all accessible, all ready. Here’s a bigger list:Inputs, selects, toggles, checkboxes
Nav bars, sidebars, tabs, tables
Modals, popovers, toast alerts
Entire landing page and dashboard layouts
Even chat UI bits if you’re feeling fancy
And yes, keyboard and screen reader support are built-in. You love to see it.
Accessibility isn’t your problem anymore
Proper ARIA tags, focus handling, keyboard support, all done. You don’t have to manually fight for your app to be usable for everyone, it just is by default.TypeScript actually works with it
Which means no more guessing what props a component wants. Your editor will hold your hand:Auto-complete
Type checking
Fewer “why is this broken in production” moments
Theming is very easy
It uses CSS variables for all design tokens, so switching stuff like colors or dark mode is a breeze. No complex design token spreadsheets, just change a value and boom, your app looks different everywhere.First class support for Nuxt
Auto-imports? Yes. SSR support? Obviously. Plays nice with all Nuxt optimizations? For sure.
It even works smoothly with popular Nuxt addons like:Content module
Color mode for dark/light toggle
Iconify for like 200k icons
i18n for multi-language stuff
Just install the module and add it to your config file and that’s literally it. No extra config for Tailwind, icons, theme, etc. It handles all that jazz for you so you can go make stuff that matters.
It's better than the usual “install 10 packages” approach
Normally when starting a Nuxt app, you have to:Install Tailwind
Install icons
Install a headless UI kit
Install dark mode helpers
Install form validation tools
Pray they all get along
Wire it all together manually
Cry a little
Which means:Fewer updates to keep track of
Everything actually matches design-wise
Smaller final bundle because it auto tree-shakes
You start shipping faster than your coffee cools
So to sum it up, Nuxt UI is desirable because it saves time. It was made with developer experience in mind and everything looks clean with proper defaults. Do won’t have NPM dependency headaches from it and it let’s you focus on building your app without stress.
So the question isn't whether Nuxt UI is worth using, but rather: can you afford not to use it?Nuxt Hints: A new core module dropped right before the end of the year
ModulesIf you’ve ever worked with Server Side Rendered applications, you’ve quickly realized hydration errors can be a pain. What’s even more painful is finding where the errors come from and how to resolve them. Well, look no further! The new nuxt/hints module can be just the right tool for you to use!
Nuxt Hints gives real-time feedback on your app’s performance, accessibility, and security and it does it directly inside your browser via the devtools. While you could still manually audit your site (e.g. running external tooling, code reviews, etc.), with nuxt/hints you get actionable insights as you build. You spot slow-loading third-party scripts, hydration mismatches, or suboptimal layouts for web vitals.
Before diving deeper into it, let’s make some things clear:Nuxt Hints integrates directly into the Nuxt DevTools, so the insights are not based on production information (but they can still be very useful). Moreover, this module probably becomes more useful for bigger and more complex projects.
What to expect from nuxt/hints
Once you installed the module (and added it to your nuxt config file), just run your application normally and open DevTools. Click on the Hints icon and the hints panel will appear, summarizing detected issues:
Real-time Web Vitals metrics (LCP, INP, CLS) + element-level attribution & advice for improvement.
Hydration mismatch detection: side-by-side diff between server-rendered HTML and client-hydrated HTML to help debug mismatches. What’s really valuable here is that you can click on the component that’s causing the issue and jump directly to the source code to check out the implementation!
Third-party script auditing: lists external scripts, shows their performance/impact, and gives security/performance warning or suggestions.
Lastly, Nuxt Hints is still in its early days. As it matures beyond alpha, this module has the potential to become an integral part of DevTool or it could go a totally different way.
For any modern Nuxt project, Hints is a “nice to have” guardrail. It helps you catch common issues early, avoid regressions, and keep quality high without leaving your editor.Special Nuxt Features on an opt-in basis
NuxtNuxt is configured with sensible but extendable defaults. TheÂ
nuxt.config.ts file supports an opt-inÂfeatures namespace to enable or disable specific runtime behaviors, alongside aÂfuture namespace to adopt upcoming framework defaults and experimental multi-app support. It is located at the root of a Nuxt project and can be used to add custom scripts, register modules or change rendering modes.In this article, we’re going to check some “extra” options that go further and could help unlock new possibilities.
Features
The features block controls optional runtime behaviors in your application:
devLogs
Streams SSR server logs to the browser during development.
Default: true in dev
Can be set to 'silent' to suppress logs
export default defineNuxtConfig({ features: { devLogs: true // | 'silent' } })
inlineStylesInlines component CSS into rendered HTML (Vite only).
Default:Â (id) => id.includes('.vue')
Can be set to false or a callback for per-component control
export default defineNuxtConfig({ features: { inlineStyles: false // | (id) => boolean } })
noScriptsDisables injection of Nuxt client scripts and JS resource hints.
Options:
true or 'production' – disable in production
'all' – disable in both dev & prod
false – leave enabled
export default defineNuxtConfig({ features: { noScripts: true, // or 'production' | 'all' | false }, })
FutureEnable upcoming defaults and experimental core changes under future:
compatibilityVersion
Opt into defaults of a future major release (e.g., v5)
export default defineNuxtConfig({ future: { compatibilityVersion: 5 } })
multiAppEarly access to experimental multi-app support within a single Nuxt instance
export default defineNuxtConfig({ future: { multiApp: true } })
Experimental FeaturesasyncContext
Enables native async context to be accessible for nested composables in Nuxt and Nitro, reducing "Nuxt instance is unavailable" errors
export default defineNuxtConfig({ experimental: { asyncContext: true, }, })
buildCacheCaches Nuxt build artifacts based on a hash of the configuration and source files for faster rebuilds
export default defineNuxtConfig({ experimental: { buildCache: true, }, })
cookieStoreEnables CookieStore support to listen for cookie updates and refresh useCookie ref values:
export default defineNuxtConfig({ experimental: { cookieStore: true, }, })
purgeCachedDataNuxt will automatically purge cached data from useAsyncData and nuxtApp.static.data to prevent memory leaks. But you can decide to clean up Nuxt static and asyncData caches on route navigation.
export default defineNuxtConfig({ experimental: { purgeCachedData: false, // enabled by default }, })Exploring Server Components in Nuxt
NuxtAs developers, we know how important it is to reduce the amount of JavaScript we ship to the browser. And since Nuxt has been around, it has showed how powerful SSR can be for building performant applications.
Nuxt offers experimental features like Server Components to take our game to the next level but before using them, it’s important to take a look back at how hydration works and how Nuxt handles Server Side Rendering.
When you build a typical Nuxt application with SSR enabled, here's what happens:
On the Server:
Vue renders your components to static HTML
The server generates the complete DOM structure
This HTML is sent to the browser immediately
On the Client (Hydration):
The browser receives static HTML and displays it instantly
JavaScript bundles download in the background
Vue "hydrates" the static HTML by creating the virtual DOM, matching it to existing DOM nodes, and attaching event listeners
The page becomes fully interactive
This hydration process is necessary but comes with overhead. Every component needs to be downloaded as JavaScript, executed on the client, and "woken up" to become interactive. The JavaScript for your entire application must be downloaded and parsed even if most of it renders static content.
How Sever Components can helpServer components (also called component islands in Nuxt) fundamentally change this equation. They allow you to mark specific components to only render on the server and never hydrate on the client. They have some key benefits:
Reduced JavaScript Bundle Size:Â Server components and their dependencies are completely excluded from the client bundle. If you have a component that uses a heavy markdown parser or syntax highlighter, that entire library stays on the server.
Zero Hydration Overhead:Â Since server components don't hydrate, there's no virtual DOM creation, no event listener attachment, and no reconciliation work on the client. The component is truly static HTML.
Secure Server-Only Logic:Â You can safely include database queries, API keys, or other sensitive operations directly in server components without worrying about exposing them to the client.
Works with Static Sites:Â Despite the name, server components work perfectly with static site generation (SSG). Nuxt prerenders them at build time, so you can use them even on static hosting platforms like GitHub Pages or Netlify.
When and where to use them
Server components shine in specific scenarios where you have expensive rendering logic but don't need client-side interactivity. For example:
Syntax Highlighting and Code Blocks:Â Libraries like Shiki can add significant weight to your bundle. A syntax highlighter component is perfect as a server component since the highlighted code is just static HTML.
Markdown Rendering:Â Parsing and rendering markdown with MDC syntax or other parsers can be done entirely on the server. The rendered HTML is sent to the client without shipping the parser.
Static Footers and Headers:Â Navigation bars, footers, or any layout component that displays static content are excellent candidates. They add zero JavaScript but still render correctly.
Content from APIs:Â If you're fetching data from a CMS or database to display read-only content, server components eliminate the need to ship that fetching logic to the client.
Implementing Server ComponentsIn Nuxt 4, server components are still experimental but increasingly stable. Here's how to use them:
// nuxt.config.ts export default defineNuxtConfig({ experimental: { componentIslands: true } }Simply add theÂ
.server.vue suffix to your component file:components/ HighlightedCode.server.vue SiteFooter.server.vue MarkdownRenderer.server.vueUse it like any component:
<template> <div> <HighlightedCode :code="myCode" language="typescript" /> <SiteFooter /> </div> </template>
That's it! No special syntax in your template. The component renders on the server, and Nuxt sends only the HTML to the client.A final note
While reducing JavaScript bundle size is the most tangible benefit, server components offer something deeper:Â architectural clarity. They force you to think about data flow, component boundaries, and where state truly lives. This discipline makes your applications more maintainable. When you see aÂ
.server.vue component, you immediately know it has no client-side dependencies, no reactivity, no event handlers. It's self-documenting architecture.The convergence of server and client rendering represents the maturation of the JavaScript ecosystem. We're moving past the "SSR vs CSR" debates toward hybrid approaches that acknowledge both have their place.
Serverless functions in Nuxt: backend power without a backend
NuxtServerless functions are tiny pieces of code that run on demand in the cloud. You don’t provision servers or infrastructure, or write scaling rules. Cloud providers handle the underlying infrastructure, automatically scaling resources based on demand.Â
They’re perfect for lightweight APIs, form handlers, webhooks, simple auth endpoints, and glue code.
How Nuxt does it (Nitro + H3)Nuxt has built-in support for serverless functions via Nitro (its server engine). Nitro compiles your server code to many targets (Node, Vercel, Netlify, Cloudflare Workers, etc.) and runs on top of H3, a tiny, fast HTTP framework.
You write your server-side logic inside the
server/api/folder. Each file becomes an API endpoint automatically.// server/api/hello.ts export default defineEventHandler(() => { return { message: 'Hello from serverless' } })
This function will respond toGET /api/hellowith a JSON object. That’s it. Visit` /api/hello` in your browser or useFetch() from your frontend, and you get:{ "message": "Hello from serverless" }
Calling Your APIs from the FrontendYou can use
useFetch(),useAsyncData(), or$fetch()to call these functions in your Vue components or pages.<script setup> const { data } = await useFetch('/api/hello') </script> <template> <p>{{ data.message }}</p> </template>
DeploymentNuxt works with Vercel, Netlify, Cloudflare, etc. Just push your code and your functions will auto-deploy as serverless endpoints.
Vercel → Serverless Functions
Netlify → Netlify Functions
Cloudflare → Workers
You don’t need to configure anything manually — just push your code.
Quick Tips to Not Shoot Yourself in the FootDon’t trust inputs, always validate.
Use environment variables for anything secret.
Keep functions small and focused.
Handle errors properly.
Don’t block the event loop with long-running tasks (offload to queues if needed).
Common Use CasesForm submissions
Email sending
API proxies
Auth endpoints
Data fetching or caching
Webhooks
With Nuxt and Nitro, serverless functions are just regular files that do backend magic. No setup, no stress. Whether you need a contact form endpoint, a webhook handler, or a simple API proxy, you can build it in minutes.No server management required.
Data Fetching Game in Nuxt: Advanced Level
NuxtNuxt's
useFetchanduseAsyncDatacomposables are already powerful, but they got some seriously handy options that can level up how you handle data in your app.Here are a few advanced options that make a real difference in performance, UX, and network efficiency:
1.dedupe: Avoid Duplicate RequestsNuxt by default cancels any request with the same key before starting a new one. This means if you have multiple components that fetch the same data, you’ll see unnecessary requests and a lot of canceled ones. With the option
dedupe: 'defer’if an identical request is already pending, Nuxt won’t refetch and waits for the current one to finish.Âconst { data } = await useFetch('/api/products', { dedupe: 'defer' // default value: cancel });
2.retryandretryDelay: Resilience for Unstable EndpointsNot all users have stable connection or fast Wi-Fi. For flaky APIs, edge functions, or when you want to add basic fault tolerance without custom logic, you can use Nuxt’s
retrymechanism.const { data } = await useAsyncData('user', () => $fetch('/api/user'), { retry: 3, // number of retries after first request fails retryDelay: 1000 // milliseconds delay between retries });
3.delay: Stagger Fetches for Smoother UXThis option adds a delay before making the request. You can use it to debounce fetches triggered by user input (though better handled via watch + debounce). It’s’ useful when you want to avoid flashing loading states for super quick page transitions.
const { data } = await useFetch('/api/settings', { delay: 300. // milliseconds before starting fetch });Pro tip:
Always provide a key if you're using immediate: false, lazy, or working with reactive URLs to ensure deduplication and caching behave predictably.
These options let you finely tune your app's data layer for performance, UX, and network behavior. Mastering them in useFetch and useAsyncData helps you write cleaner, more efficient, and more resilient Nuxt apps.
Nuxt Essentials: A Practical Cheatsheet for Beginners & Returners
NuxtNuxt’s been around for a while -built on top of Vue- and with Nuxt 4 just around the corner, it’s a good time to revisit some of the basics. If you're new to Nuxt (or Vue), or just catching up after a break, here’s what people are actually talking about when they say things like 'Vite', 'Pinia', or 'Nitro'.
Folder structure in NuxtFolder
Purpose
pages/
Auto-routes based on file names
components/
Vue components
layouts/
Custom layouts
middleware/
Functions run before rendering page
plugins/
Inject functions/libraries
composables/
Auto-imported composables
Key tools in Nuxt ecosystemTool
Purpose
Nitro
Server engine (used for SSR, server APIs, middleware)
Vite
Frontend bundler (use for Dev server, build optimization)
Pinia
State management system
Now let's get into detail (only a bit)
Plugins:Plugins in Nuxt are a way to extend your app’s functionality by injecting features, libraries, or custom logic into the app context. They run before your Vue components are created. They run globally and are available everywhere without having to import it manually each time.
In Nuxt 3, you can create a plugin like:// /plugins/myPlugin.ts export default defineNuxtPlugin(nuxtApp => { nuxtApp.provide('hello', () => 'Hello from plugin!') })
And use it like:const hello = inject('hello')
Composables:In short: Composables = Reusable logic using Composition API
Let's look at a simple composable example:// composables/useCounter.ts export const useCounter = () => { const count = ref(0) const increment = () => count.value++ return { count, increment } }
You can use it in your vue component like:<script setup> const { count, increment } = useCounter() </script> <template> <button @click="increment">Clicked {{ count }} times</button> </template>
Middleware:// middleware/auth.global.ts export default defineNuxtRouteMiddleware((to, from) => { const user = useUser() if (!user.value) return navigateTo('/login') })
What is Nitro?Nitro is a lightweight universal server engine built by the Nuxt team. It’s what powers SSR (Server-Side Rendering), API routes, and deployment adapters in Nuxt 3.
Nitro: Next Generation Server Toolkit
What is Vite?Vite is a (blazing fast) modern frontend bundler with tree shaking, instant dev start and fast Hot Module Replacement.
Vite: the build tool for web
What's Pinia then?Pinia is the official state management library for Vue 3, and by extension, Nuxt 3.
Think of Pinia as Vuex’s leaner, friendlier successor. You define a store using "defineStore()" and use it in components without boilerplate.
It's lightweight and modular and there's first-class support for it in Nuxt (meaning it's officially integrated, fully supported, and works seamlessly out of the box)
The store for Vue.js
I've heard of Rollup and Rolldown, what are they?Rollup is a JavaScript module bundler (like Webpack or Vite's underlying engine for builds), known for being simple, fast, and tree-shake-friendly. It takes all your import/export JavaScript (or TypeScript) files, bundles them into one or more optimized output files.
In our context, Vite uses Rollup under the hood for production builds.
Rolldown is a newer project, currently under development (time of writing this article: June/July 2025).It's a Rollup-compatible bundler written in Rust, aiming to be blazing fast (like esbuild or Turbopack).
Powering Netherland’s largest real estate platform
VueAs published on "the official State of Vue.js report of 2025" earlier this year, I did a case study to showcase how Funda (real estate platform in the NL) makes use of Vue and Nuxt.
Little intro about Funda?
Funda is the Netherlands' leading real estate listing platform, serving millions of users daily. With hundreds of thousands of property listings and approximately 5 million daily searches, the platform demands exceptional performance, scalability, and search capabilities. Given the possible combination of search parameters and filters to cater to the users’ needs, this isn’t a trivial task. In addition, search engine optimization (SEO) is critical for ensuring visibility in property searches and maintaining a competitive edge for brokers.
How did we use Vue and Nuxt? And why?
Our web application combines modern technologies throughout its stack. For the frontend, we rely on Nuxt 3 with TypeScript and use Tailwind CSS as our CSS framework of choice, which also helps us write consistent styles according to our design system. The backend relies on the .NET framework, incorporating Elasticsearch to handle search functionality. The entire solution is hosted on the Microsoft Azure cloud platform.Beyond that, essential performance defaults, such as automatic code-splitting by route or predefined cache headers for assets, helped us get a good baseline. The built-in filesystem-based routing structure is also a big plus, as it makes navigation between listings and pages much simpler than solely using Vue Router (which is possible in Nuxt, too, if needed).
One key feature that makes domain-driven design possible is Nuxt's layer system. This allows us to split our codebase into multiple layers representing a domain. Each layer can have its own BFF and export its components. This makes it easy to separate concerns and keep the codebase clean and maintainable. Further, the layer system makes it easy to auto-import composables, components, and utilities, which is a big plus. Eventually, we benefit from further enhancements the Nuxt team ships consistently, from performance to developer experience improvements.
Nuxt 4 is coming out, what does it mean?
NuxtNuxt 3 was released in November 2022, about four years after Nuxt 2 originally launched. That’s four long years of development, beta cycles, and a full rewrite using Vue 3, Vite, and Nitro. The result? A versatile framework with a modern stack and delightful developer experience that got the frontend community excited.
Then in 2024, we started hearing rumors that Nuxt 4 was coming in June (but which June? Aha!).Flash forward to June 2025, and Nuxt 4 (alpha) is officially here — with a stable release promised by the end of the month. Cue the holy music.
What to expect from Nuxt 4?
You can read all about the new project structure and enhanced data fetching in official Nuxt articles (link) but what I want to highlight is how stress-free the migration from Nuxt 3 to Nuxt 4 is going to be.
We all remember the dark days of rewriting our Nuxt 2 projects to work with Nuxt 3. That was painful. But this time? Not the case.
As Daniel Roe has mentioned on multiple occasions, Nuxt 4 is not about “hype”, it’s about consistency and developer experience.
Nuxt 3 already felt solid and stable over the past couple years (and usage has been growing) Nuxt 4 just takes that solid foundation and makes it even more polished and thoughtfully engineered.
But Nuxt 4 is not a Pandora’s box for many of us. The new features were already available via compatibility mode, which makes the adaptation more straightforward, smooth and predictable.
But - Nuxt 5 is coming soon!
Yup, Nuxt 5 is already in the conversation. It’s possible we’ll have another major upgrade in 6 month or so, pending Nitro v3. And the question becomes whether to migrate a Nuxt 3 package to Nuxt 4 now or wait for Nuxt 5?
I think it’s definitely worth migrating to Nuxt 4 now. There is no major architectural overhaul and most modules require minimal changes to be compatible.Moreover, Core Nuxt modules are already using Nuxt 4 conventions and the community reception has been very positive so far.
PS. I really enjoyed watching this video from Alexander Lichter: TWO Major Nuxt versions coming? All you need to know about Nuxt 4 and Nuxt 5