name: lovable-vps-deploy-fix
description: Fix Vite build not generating script tags when deploying Lovable/TanStack Start React projects to VPS with nginx
category: devops
tags: [vite, lovable, tanstack-start, vps, deploy, nginx]
Lovable → VPS Deploy (Vite Build Fix)
Context
When deploying a Lovable/TanStack Start React project to a standard VPS (nginx), the Vite build fails to generate index.html with tags. The build succeeds but produces an empty/corrupt dist/index.html (no JS entry point injected).
Root Cause
Lovable projects use a non-standard Vite setup:
- -
src/index.htmlis the React entry point (NOT rootindex.html) - - The Vite config often references
@lovable.dev/vite-tanangle-configwhich only works with Lovable's dev server - - Running
vite buildstandalone produces adist/index.htmlthat is essentially the same assrc/index.html— thetag is NOT injected by Vite because the entry point doesn't have one - - VPS path:
/var/www/comercialrs/ - - Build output:
dist/(nginx serves from here) - - Nginx root:
/var/www/comercialrs/dist - - Build tool:
node ./node_modules/vite/bin/vite.js build(notnpm run build— avoids Lovable hooks)
Solution
1. Create clean entry point at src/index.html
`html
`
2. Configure vite.config.ts (no Lovable references)
`js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
build: {
outDir: "dist",
assetsDir: "assets",
rollupOptions: {
input: "./index.html" // references src/index.html
}
},
});
`
3. Create index.html at project root (HTML template with script tag)
`html
`
Critical: This root index.html MUST have . Vite uses this as the HTML template — it reads the script tag to find the entry module, then during build replaces the src with the hashed bundle filename. Without this script tag, Vite generates a broken dist/index.html.
Build Command
`bash
cd /var/www/comercialrs
node ./node_modules/vite/bin/vite.js build
`
After build, dist/index.html will have injected.
Post-Build Patch Script (fallback if above doesn't work)
`js
// scripts/post-build-fix.mjs
import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
function getMainJs() {
const files = readdirSync(join(root, 'dist', 'assets'))
.filter(f => f.startsWith('main-') && f.endsWith('.js'));
if (!files.length) throw new Error('No main-*.js found');
return /assets/${files[0]};
}
function patch(filePath, mainJs) {
let c = readFileSync(filePath, 'utf8');
c = c.replace('
', \n );
writeFileSync(filePath, c);
}
const mainJs = getMainJs();
patch(join(root, 'dist', 'index.html'), mainJs);
patch(join(root, 'index.html'), mainJs);
`
Verification
`bash
Should show
cat dist/index.html
Should return the script tag
curl -sk https://yoursite.com/ | grep "script"
`
Key Insight
Vite's rollupOptions.input uses the given HTML file as a template. It reads the tag to discover the entry module, bundles everything, then replaces the script src with the hashed output filename. If the HTML has no script tag, nothing gets injected.