๐Ÿ’พThananjhayan
โ† Back to blog

Building a Personal Site with Nuxt, Content, and Studio

May 4, 2026

Personal websites are having a moment again. After a decade of feeds and walled gardens, more developers are returning to their own corner of the web โ€” a place where the writing is not optimized for an algorithm and the design reflects the person, not a template. This post is a long, opinionated walkthrough of how I rebuilt mine using Nuxt 4, Nuxt Content v3, Nuxt UI, and Nuxt Studio, and how I deployed it to Vercel without any custom server code.

Why Nuxt

I considered Astro, Next.js, and SvelteKit before landing on Nuxt. Each of these frameworks is excellent and any of them would have produced a perfectly good site. The reason I picked Nuxt is mostly about ergonomics. Vue's <script setup> syntax feels closer to writing plain JavaScript than React's JSX does, the auto-imports remove a tremendous amount of boilerplate, and the Nuxt module ecosystem covers almost everything you'd want out of the box.

Auto-imports and the developer experience

There is a class of friction in modern web development that I'd call "import tax": every time you reach for a component or a composable you have to go find it, type its path, and watch your editor's import block grow. Nuxt removes this tax for components, composables, and utilities under the app/ directory. When I write useAsyncData(...) or <TableOfContents />, the framework knows what I mean. It is a small thing but it adds up over a weekend project.

The module system

A lot of the reasons people reach for Next.js โ€” image optimization, SEO helpers, content management โ€” exist in Nuxt as first-party modules. Drop @nuxt/image into the modules array and the <NuxtImg> component just works. Add @nuxt/content and you have a Markdown-driven CMS without a server. Add @nuxt/ui and you get a Tailwind-based design system that includes both a component library and a theming layer.

Choosing the content layer

Content was the most interesting decision. I had three real options:

  1. Markdown files in the repo, parsed at build time.
  2. A hosted headless CMS like Sanity or Contentful.
  3. A database-backed admin like Payload.

For a personal site I always come back to plain Markdown. It is portable, it diffs nicely in pull requests, it survives every framework migration I have ever done, and it does not lock me into a vendor.

Why Nuxt Content v3

Nuxt Content v3 is the first version of the module that uses a real database under the hood โ€” a local SQLite file generated at build time from your Markdown sources. That seemingly small change unlocks a lot:

  • You can query content like a database, with where, order, and limit.
  • Collections give you schemas with Zod validation.
  • Studio integration is automatic, with no extra configuration.

In practice this means my homepage's "latest five blog posts" list is one line:

queryCollection('blog').order('date', 'DESC').limit(5).all()

No globbing, no manual sorting, no frontmatter parsing in user code.

Schemas with Zod

One thing I want to call out is the schema feature. Defining a Zod schema for each collection means the build will fail if I publish a post without a date or a title. This is the kind of constraint that pays for itself the first time you forget to add a frontmatter field at 11pm on a Friday.

The visual design

I wanted something quiet. The dominant aesthetic of modern developer sites is high-contrast monochrome with one accent color, and I think it has held up because it gets out of the way of the writing.

Layout

The layout is a two-column shell on desktop: a sticky sidebar on the left with my name, a short bio, a navigation list, and some social links; a flexible main column on the right that holds the actual page content. On article pages I add a third column for a table of contents. On screens narrower than the medium breakpoint the sidebar collapses behind a hamburger button.

This is a tired pattern at this point โ€” Tania Rascia, Lee Robinson, Josh Comeau, and many others have variations of it โ€” but it is tired because it works. The sidebar gives the reader a persistent sense of place, and the narrow content column keeps line lengths in the comfortable 60โ€“80 character range.

Typography

I am using the system font stack. Custom fonts are great but they come with a cost: a network request, a flash of unstyled text, and an aesthetic that ages faster than you think. The system stack is invisible, which is what I want from body text.

Color

Two palettes โ€” one for dark mode and one for light. Six tokens each: --bg, --bg-elevated, --fg, --fg-muted, --border, --link. That is enough to compose every element on the site without ever reaching for a raw hex value in a component. When I want to retune the palette I edit six lines.

Implementing the table of contents

The TOC was one of the small features that turned out to be more interesting than expected. Nuxt Content v3 exposes a body.toc.links array on every parsed page, with each link's id, text, and depth. Rendering it is trivial. Making it useful is less trivial.

Highlighting the active section

A good TOC highlights the section the reader is currently viewing. The naive implementation listens to scroll events and computes which heading is nearest to the top of the viewport, but it churns through the main thread on every frame. The better implementation uses IntersectionObserver. You give it a list of heading elements, a margin that approximates the "active zone" of the viewport, and it tells you which headings are visible. The implementation in this site is about twenty lines.

Smooth scrolling and URL state

Two more touches: clicking a link smoothly scrolls to the section instead of jumping, and the URL hash updates via history.replaceState so the reader can copy a link to a specific section. Neither is a feature most readers will consciously notice, which is the point.

Deploying to Vercel

Vercel is the path of least resistance for Nuxt right now. The Nitro server that powers Nuxt has a first-class Vercel preset, which means there is no custom configuration and no serverless adapter to write yourself.

The build

Setting nitro.preset to 'vercel' in nuxt.config.ts tells the build to emit a .vercel/output/ directory in the format Vercel expects. Push the repo, import it in the dashboard, and the first deploy goes out without any additional setup.

Environment

The only environment variable I have set is a custom domain. There is no database, no API key, no auth provider. Static content sites have remained one of the great pleasures of the modern web exactly because they do not require any of those things.

What I would do differently

A few notes for next time.

Start with the content

I spent the first afternoon polishing the layout. I should have spent it writing two real blog posts. A site without content is a design system, not a publication, and design systems are more fun to build than to live with.

Decide on dark mode early

Adding a dark mode after the fact is more work than building both modes in parallel from the start. Every component has to be checked twice. If you know you want a dark mode, design with both palettes from day one.

Keep the dependency footprint small

This site uses Nuxt, Nuxt Content, Nuxt UI, Nuxt Image, and Tailwind. That is already five large dependencies. Every additional module is a future migration. I will not be adding analytics, comments, or a newsletter widget until I am sure I will keep using them.

Closing thoughts

Personal sites do not need to be perfect. They need to exist, they need to be yours, and they need to be easy enough to update that you actually update them. Pick a stack that you enjoy, write a layout you can live with for a year, and then stop tweaking it and start writing.

If you have made it this far, thank you for reading. I will be writing more about the day-to-day of working in this stack โ€” small wins, sharp edges, and the occasional rant. See you in the next post.

๐Ÿ’ฌ Comments