RamNode logo
Lovable + Vike SSR

Lovable + Vike - Server-Side Rendering

Add SSR to your Lovable-exported React application using Vike โ€” without restructuring your existing code. Your React Router setup stays intact while gaining full SSR benefits.

Node.js 18+
15 Minutes Setup
โšก Zero Restructuring

Introduction

This guide shows how to add server-side rendering to your Lovable-exported React application using Vike โ€” without restructuring your existing code. Your React Router setup stays intact while gaining full SSR benefits.

The key insight: Vike's wildcard route feature lets you delegate all routing to React Router while Vike handles the SSR. This means you can deploy in minutes, not hours.

SSR Benefits You'll Gain

  • Full HTML content on initial load (not an empty <div id="root">)
  • Search engines see your complete page content
  • Faster First Contentful Paint and better Core Web Vitals
  • Pages work with JavaScript disabled (initial view)
  • SPA navigation still works after hydration

Prerequisites

  • โ€ข Node.js 18.x or higher on your VPS
  • โ€ข A Lovable project exported to GitHub
  • โ€ข SSH access to your server
  • โ€ข PM2 process manager (npm install -g pm2)

Project Structure

You'll add just a few files to your existing Lovable project. Your src/ folder stays untouched:

Project Structure
your-lovable-app/
โ”œโ”€โ”€ src/ # YOUR EXISTING CODE (unchanged)
โ”‚ โ”œโ”€โ”€ pages/
โ”‚ โ”‚ โ”œโ”€โ”€ Index.tsx
โ”‚ โ”‚ โ”œโ”€โ”€ Admin.tsx
โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard.tsx
โ”‚ โ”‚ โ””โ”€โ”€ NotFound.tsx
โ”‚ โ”œโ”€โ”€ components/
โ”‚ โ”œโ”€โ”€ App.tsx # Your existing React Router setup
โ”‚ โ””โ”€โ”€ main.tsx
โ”œโ”€โ”€ pages/ # NEW: Vike wrapper (4 files)
โ”‚ โ”œโ”€โ”€ +config.ts
โ”‚ โ””โ”€โ”€ @path/
โ”‚ โ”œโ”€โ”€ +route.ts # Wildcard catch-all
โ”‚ โ””โ”€โ”€ +Page.tsx # Renders RouterApp
โ”œโ”€โ”€ renderer/
โ”‚ โ””โ”€โ”€ +Layout.tsx # SSR/Client router wrapper
โ”œโ”€โ”€ lib/
โ”‚ โ””โ”€โ”€ RouterApp.tsx # Copy routes from App.tsx
โ”œโ”€โ”€ server.js # Express server
โ”œโ”€โ”€ vite.config.ts
โ”œโ”€โ”€ ecosystem.config.js # PM2 config
โ””โ”€โ”€ package.json
1

Install Dependencies

Install packages
npm install vike vike-react express compression serve-static
npm install -D @types/express
2

Update vite.config.ts

vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import vike from 'vike/plugin';
import path from 'path';
export default defineConfig({
 plugins: [react(), vike()],
 resolve: {
 alias: {
 '@': path.resolve(__dirname, './src'),
 },
 },
 server: {
 host: true,
 },
});
3

Create Vike Wrapper Files

pages/+config.ts

pages/+config.ts
import vikeReact from 'vike-react/config';
import type { Config } from 'vike/types';
export default {
 extends: vikeReact,
} satisfies Config;

pages/@path/+route.ts (The Key File)

This wildcard route captures all URLs and delegates routing to React Router:

pages/@path/+route.ts
// Catch-all route - matches any URL path
// Delegates routing to React Router instead of Vike filesystem routing
export default '/*';

pages/@path/+Page.tsx

pages/@path/+Page.tsx
import RouterApp from '../../lib/RouterApp';
export default function Page() {
 return <RouterApp />;
}

renderer/+Layout.tsx

This layout handles the SSR/client router switching:

renderer/+Layout.tsx
import React from 'react';
import { BrowserRouter, StaticRouter } from 'react-router-dom';
import { usePageContext } from 'vike-react/usePageContext';
function RouterWrapper({ children }: { children: React.ReactNode }) {
 const pageContext = usePageContext();
 const isServer = typeof window === 'undefined';
 if (isServer) {
 // SSR: StaticRouter doesn't use browser APIs
 return (
 <StaticRouter location={pageContext.urlPathname}>
 {children}
 </StaticRouter>
 );
 }
 // Client: BrowserRouter for SPA navigation
 return <BrowserRouter>{children}</BrowserRouter>;
}
export default function Layout({ children }: { children: React.ReactNode }) {
 return (
 <React.StrictMode>
 <RouterWrapper>
 {children}
 </RouterWrapper>
 </React.StrictMode>
 );
}
4

Copy Your Routes to lib/RouterApp.tsx

Copy the route definitions from your existing App.tsx into this file:

lib/RouterApp.tsx
import { Routes, Route } from 'react-router-dom';
// Import your existing page components
import Index from '../src/pages/Index';
import Admin from '../src/pages/Admin';
import AdminLogin from '../src/pages/AdminLogin';
import Dashboard from '../src/pages/Dashboard';
import Settings from '../src/pages/Settings';
import NotFound from '../src/pages/NotFound';
export default function RouterApp() {
 return (
 <Routes>
 <Route path="/" element={<Index />} />
 <Route path="/admin" element={<Admin />} />
 <Route path="/admin/login" element={<AdminLogin />} />
 <Route path="/dashboard" element={<Dashboard />} />
 <Route path="/settings" element={<Settings />} />
 {/* Keep catch-all route last */}
 <Route path="*" element={<NotFound />} />
 </Routes>
 );
}

That's it! Your existing useNavigate(), useParams(), useLocation(), <Link>, and protected route logic all work unchanged.

5

Create server.js

server.js
import express from 'express';
import compression from 'compression';
import { renderPage } from 'vike/server';
import { createServer as createViteServer } from 'vite';
const isProduction = process.env.NODE_ENV === 'production';
const port = process.env.PORT || 3000;
async function startServer() {
 const app = express();
 app.use(compression());
 if (isProduction) {
 // Serve built assets
 app.use(express.static('dist/client'));
 } else {
 // Dev: use Vite middleware
 const vite = await createViteServer({
 server: { middlewareMode: true },
 });
 app.use(vite.middlewares);
 }
 // SSR handler
 app.get('*', async (req, res, next) => {
 const pageContextInit = { urlOriginal: req.originalUrl };
 const pageContext = await renderPage(pageContextInit);
 
 if (pageContext.httpResponse) {
 const { body, statusCode, headers } = pageContext.httpResponse;
 headers.forEach(([name, value]) => res.setHeader(name, value));
 res.status(statusCode).send(body);
 } else {
 next();
 }
 });
 app.listen(port, () => {
 console.log(`Server running at http://localhost:${port}`);
 });
}
startServer();
6

Update package.json Scripts

package.json
{
 "type": "module",
 "scripts": {
 "dev": "node server.js",
 "build": "vite build",
 "start": "NODE_ENV=production node server.js"
 }
}
7

Create ecosystem.config.js

ecosystem.config.js
module.exports = {
 apps: [{
 name: 'lovable-ssr',
 script: 'server.js',
 instances: 'max',
 exec_mode: 'cluster',
 env_production: {
 NODE_ENV: 'production',
 PORT: 3000,
 },
 max_memory_restart: '500M',
 error_file: './logs/err.log',
 out_file: './logs/out.log',
 }]
};

How the SSR Request Flow Works

When a browser requests /admin:

  1. 1Browser requests /admin
  2. 2Nginx proxies to Express:3000
  3. 3Express calls renderPage({ urlOriginal: '/admin' })
  4. 4Vike matches wildcard '/*' in pages/@path/+route.ts
  5. 5Vike renders pages/@path/+Page.tsx โ†’ <RouterApp />
  6. 6Layout wraps with <StaticRouter location="/admin">
  7. 7React Router matches /admin โ†’ renders <Admin />
  8. 8Server returns fully SSR'd HTML with admin content
  9. 9Client hydrates with <BrowserRouter>
  10. 10SPA navigation works normally

Server Deployment

Clone and setup
cd /var/www
git clone https://github.com/yourusername/your-lovable-app.git
cd your-lovable-app
npm install
Build for production
npm run build
Create logs directory and start
mkdir -p logs
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup

Nginx Configuration

/etc/nginx/sites-available/yourdomain.com
server {
 listen 80;
 server_name yourdomain.com www.yourdomain.com;
 gzip on;
 gzip_types text/plain text/css application/json application/javascript;
 location / {
 proxy_pass http://localhost:3000;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection 'upgrade';
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 proxy_cache_bypass $http_upgrade;
 }
 location /assets/ {
 proxy_pass http://localhost:3000;
 expires 1y;
 add_header Cache-Control "public, immutable";
 }
}

Enable and secure:

Enable site and SSL
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Verify SSR is Working

Check that the server returns rendered HTML, not an empty shell:

Test SSR output
curl -s http://localhost:3000/admin | head -30

You should see actual content like <h1>Admin Dashboard</h1>, not just an empty <div id="root"></div>.

PM2 Management Commands

PM2 Commands
pm2 list # View running processes
pm2 logs lovable-ssr # View logs
pm2 restart lovable-ssr # Restart
pm2 reload lovable-ssr # Zero-downtime reload
pm2 monit # Monitor resources

Advanced: Full Vike Migration (Optional)

For new projects or if you want Vike's native features (+data.ts, +guard.ts, automatic code splitting), you can use filesystem routing instead. This requires restructuring your pages into the pages/ directory with Vike conventions. See the Vike documentation for details.

Troubleshooting

Hydration Mismatch Warnings

Ensure browser-only code is in useEffect or guarded with typeof window !== 'undefined'.

Routes Not Matching

Verify your routes in lib/RouterApp.tsx match your original App.tsx exactly.

Static Assets 404

Ensure express.static points to 'dist/client' and assets use the /assets/ path prefix.

Ready to Deploy?

Get a high-performance Cloud VPS for your Lovable SSR app.

AltStyle ใซใ‚ˆใฃใฆๅค‰ๆ›ใ•ใ‚ŒใŸใƒšใƒผใ‚ธ (->ใ‚ชใƒชใ‚ธใƒŠใƒซ) /