Skip to main content
luke@terminal:/blog$ ls
luke@terminal:/blog$ cat fixing-velite-watch-mode-irony.md

I Fixed Velite Watch Mode Problem (And Immediately Broke It Again)

Created: 2026-03-23 | 3 min read

I just ran blog review agent on my Adding Syntax Highlighting to Velite with Shiki post. Made a few revisions to content including fixing tutorial framing, added version numbers, corrected CSS classes. Committed everything to git.

Then I refreshed http://localhost:3000.

Still seeing old content. Weird. This never happens.

The Confusion

Honestly, this annoyed me more than it should have. I had just spent all this time fixing the post, and now I can't even see the changes?

Velite was supposed to watch files automatically, like some content management systems do. Velite doesn't watch by default - you have to configure it. I'd written about this exact issue in a previous post. The irony was not lost on me.

So I restarted the dev server, cleared cache, expecting everything to work. The new post showed up, but Velite still wasn't watching files. Changes only appeared when I restarted the server. This definitely is a regression on what I had previously configured.

What's Actually Going On?

Velite's docs say it supports watch mode, but it needs to be configured properly. The integration with Next.js happens through next.config.ts - that's where Velite gets initialized. If watch mode isn't set up there, Velite just runs once when the server starts and never again.

I opened next.config.ts at the root of my project. Here's what the file looked like:

import type { NextConfig } from 'next/config'

const nextConfig: NextConfig = {
  // some other config...
}

export default nextConfig

Oh. The Velite integration code was completely missing.

I found the Velite integration code in their Next.js docs. Here's what should have been in next.config.ts:

import type { NextConfig } from 'next/config'

const nextConfig: NextConfig = {
  // some other config...
}

// Velite integration
const isDev = process.env.NODE_ENV === 'development'
const isBuild = process.env.NODE_ENV === 'production'
if (!process.env.VELITE_STARTED && (isDev || isBuild)) {
  process.env.VELITE_STARTED = '1'
  import('velite').then(m => m.build({ watch: isDev, clean: !isDev }))
}

export default nextConfig

But there was nothing there. Just Next.js config.

So that's why watch mode wasn't working. The code to start Velite at all was gone.

Why Was It Removed?

I checked git history to see when it disappeared. Found commit 1afe3e2 with message "Duplicate Velite build issue" that had removed all the Velite integration code. I figured I must have made a mistake.

So I restored the code from git. Velite was running again. Watch mode was working. Changes appeared instantly.

Then I remembered what the duplicate build issue actually was.

The Double Build Problem

When I deployed to Vercel, I was seeing Velite build twice in the logs:

[VELITE] building...
[VELITE] building... again

The logs showed Velite building twice. I still don't know the exact root cause, but what I observed was: Next.js was triggering Velite during its build process, and something else (Vercel's Velite integration?) was also triggering a build. Two builds were happening.

The original code was running Velite in both development (isDev) and production (isBuild). In production, when isBuild is true, Next.js runs during the build process. If Vercel also has its own Velite integration, both would be triggering Velite builds during deployment. That's inefficient. So commit 1afe3e2 removed the Velite integration entirely to stop the double builds.

The problem with that approach? It also broke watch mode in development because the commit removed ALL the integration code.

The Actual Fix

The real solution is to only run Velite watch mode in development. Vercel handles Velite builds separately in production (I verified this by checking my Vercel project settings - there's a "Velite" section that shows it runs builds during deployment), so Next.js shouldn't trigger them.

Here's what I ended up with:

const isDev = process.env.NODE_ENV === 'development'
if (!process.env.VELITE_STARTED && isDev) {
  process.env.VELITE_STARTED = '1'
  import('velite').then(m => m.build({ watch: true, clean: false }))
}

This is different from the original code:

  • Removed isBuild - only run in development
  • I ended up using watch: true in dev mode
  • I ended up using clean: false in dev mode (I tried both settings. With clean: true, Velite would rebuild everything from scratch each time a file changed. With clean: false, it only rebuilt changed files, which was much faster)
  • Vercel handles Velite builds separately in production (no watch mode needed)

The VELITE_STARTED environment variable prevents Velite from starting multiple times during Next.js hot reloads. Without it, every time Next.js reloaded the config (when files change), Velite would start again, which causes issues.

To verify this works, I changed the content in one of my blog posts without restarting the dev server. Within a second, the change appeared in the browser. I also checked my Vercel deploy logs and only saw one Velite build.

Happy days. Back in business.

luke@terminal:/blog$ ls previous_post.sh
luke@terminal:/blog$ ls next_post.sh