I Fixed Velite Watch Mode Problem (And Immediately Broke It Again)
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 (Setting Up Velite with Next.js 16). 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 (my build script?) 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. My build script was already running Velite before Next.js even started, so both were 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. The production Velite build comes from my build script (it runs velite && next build during deploys), so Next.js shouldn't trigger another one.
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: truein dev mode - I ended up using
clean: falsein dev mode (I tried both settings. Withclean: true, Velite would rebuild everything from scratch each time a file changed. Withclean: false, it only rebuilt changed files, which was much faster) - The build script handles the Velite build in production (no watch mode needed)
The production build only works because the build script runs velite && next build. Without that step, removing isBuild from next.config.ts would leave Velite with no production build at all.
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.