Minima is currently only a headless CMS, but I’ve been hard at work giving it a head.
It will launch with a small set of official themes, but after seeing how good Gemini 3 was at design, it was obvious the infrastructure also had to support AI-generated themes.
The original plan was a single, multi-tenant “customer” Nuxt application. It would pull a site’s theme from the database and compile the templates into Vue components at runtime, but otherwise be a relatively normal Nuxt app.
Nuxt excludes the Vue runtime compiler by default, but it can be enabled via vue.runtimeCompiler, and there are even tests covering what I was trying to do.
In local development, the entire pipeline worked flawlessly.
Then we deployed to Cloudflare.
On-the-fly template compilation is not supported in the ESM build of @vue/server-renderer. All templates must be pre-compiled into render functions.
Uh, what?
Here’s the relevant code in @vue/server-renderer, note the TODO:
// TODO: this branch should now work in ESM builds, enable it in a minor
if (!__CJS__) {
throw new Error(
`On-the-fly template compilation is not supported in the ESM build of ` +
`@vue/server-renderer. All templates must be pre-compiled into ` +
`render functions.`,
)
}Armed with false hope and Claude Code, I eventually found a workaround – using a Nitro hook and a Rollup plugin to force Nuxt to bundle the CJS build of @vue/server-renderer instead of the ESM one:
export default defineNuxtConfig({
vue: {
runtimeCompiler: true,
},
experimental: {
externalVue: false,
},
hooks: {
// Bundle the CJS build of @vue/server-renderer
'nitro:config': (config) => {
config.rollupConfig = config.rollupConfig || {}
config.rollupConfig.plugins = config.rollupConfig.plugins || []
config.rollupConfig.plugins.push({
name: 'vue-server-renderer-cjs',
resolveId(id) {
if (id === '@vue/server-renderer') {
const cjsPath = require.resolve('@vue/server-renderer/dist/server-renderer.cjs.js')
return { id: cjsPath, external: false }
}
return null
},
})
},
},
})This introduced us to the final boss:
Code generation from strings disallowed for this context.
Here’s the line that triggers it:
return (compileCache[cacheKey] =
Function('require', code)(fakeRequire))Turns out, that is a complete non-starter on Cloudflare at the time of writing.
I built a minimal repro, confirmed the approach works fine when targeting Node, and concluded that the absolute best-case scenario on Cloudflare would be that we could make it work – without SSR.
That wasn’t acceptable.
Back to the drawing board
SSR is non-negotiable, and I really didn’t want to host customer sites on a different platform just to make this work.
Eventually I remembered that Cloudflare had already solved a very similar problem in vibesdk: generating and running code on Workers.
So how did they do it?
I cloned the repo into .tmp and pointed Claude Code at it.
The answer involves a combination of:
The (somewhat simplified) flow looks like this:
Download a Nuxt app scaffold from R2
Retrieve the site’s theme from D1
Codegen the Vue components
Build the Nuxt application
Deploy the compiled app and assets to Workers for Platforms
The “customer app” no longer renders anything itself. It’s now a lightweight dispatch worker that routes requests to each customer’s isolated Nuxt application.
This supports both:
subdomains on onminima.com
custom domains via CNAME, using SSL for SaaS
A blessing in disguise
Runtime compilation meant instant theme updates. Build-time compilation meant scary new infrastructure.
But looking at it now, the trade-off is actually an improvement:
Customer sites are fully isolated
The full build + deploy pipeline takes ~90 seconds on average, without any real optimization yet
The runtime compilation work still powers live previews inside the CMS
Dropping the Vue runtime compiler means smaller bundles for customer sites
Onwards.