| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
# Install dependencies
npm install ssr-electron// main.js
import { app, BrowserWindow } from 'electron';
import { createSSR } from 'ssr-electron';
// Create the SSR bridge instance - it automatically registers schemes and handlers
const ssr = createSSR({ debug: true });
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
// Load the app from the virtual URL
win.loadURL('http://localhost/');
return win;
}
app.whenReady().then(() => {
// Register route for the main page
ssr.registerRoute('/', (request, url) => {
// HTML is generated and served directly from the main process
return new Response(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron SSR Example</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
button {
background-color: #4a86e8;
color: white;
border: none;
border-radius: 4px;
padding: 10px 15px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Electron SSR Example</h1>
<button
hx-get="/system-info"
hx-target="#content">
Load System Info
</button>
<div id="content">
<p>Click the button to load data from the main process</p>
</div>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
});
// Register route for system info
ssr.registerRoute('/system-info', (request, url) => {
// This data is only available in the main process
return new Response(`
<div>
<h2>System Information</h2>
<ul>
<li>Platform: ${process.platform}</li>
<li>Architecture: ${process.arch}</li>
<li>Node.js Version: ${process.version}</li>
<li>Electron Version: ${process.versions.electron}</li>
</ul>
<p><em>This data comes directly from the Electron main process!</em></p>
</div>
`, {
headers: { 'Content-Type': 'text/html' }
});
});
createWindow();
});If you've built an Electron app, you're familiar with the pain of IPC (Inter-Process Communication). The main and renderer processes are completely isolated, forcing developers to set up complex messaging systems just to share data and trigger actions across the boundary. This results in:
Several projects make working with IPC slightly more fun:
I wanted something simpler.
I stumbled upon this article in my rabbit hole: The ultimate Electron app with Next.js and React Server Components. I ended up not going this direction as I deeply dislike NextJS, but later I stumbled upon Datastar and HTMX and remembered this article and I wondered if I could adapt the idea to work them instead. Potentially I could also get this working with just bare React 🤔
The result is Electron SSR - a library that lets you use server-side rendering patterns directly in your Electron app without dealing with IPC.
The biggest advantage of Electron SSR is that it allows you to return views from the main process that have direct access to Node.js modules that are normally unavailable in the renderer.
With traditional Electron development, if you want to use Node.js modules in your renderer or access data from the main process, you need to:
This requires careful coordination between multiple files and introduces complexity:
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
readNote: () => ipcRenderer.invoke('read-note'),
saveNote: (content) => ipcRenderer.invoke('save-note', content),
// Expose a function to listen for notifications from main
onNoteUpdated: (callback) => ipcRenderer.on('note-updated', (_event, value) => callback(value))
})
// main.js
const { app, BrowserWindow, ipcMain, safeStorage } = require('electron')
const fs = require('fs/promises')
// Creating the window
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
});
mainWindow.loadFile('index.html');
}
// IPC handlers
ipcMain.handle('read-note', async () => {
try {
const encryptedContent = await fs.readFile('user-notes.enc')
return await safeStorage.decryptString(encryptedContent)
} catch (error) {
return { error: error.message }
}
})
ipcMain.handle('save-note', async (event, content) => {
try {
const encrypted = await safeStorage.encryptString(content)
await fs.writeFile('user-notes.enc', encrypted)
// After saving, notify the renderer about the update
mainWindow.webContents.send('note-updated', 'Note saved successfully!')
return { success: true }
} catch (error) {
return { error: error.message }
}
})
// renderer.js
document.getElementById('load-button').addEventListener('click', async () => {
const content = await window.electronAPI.readNote()
if (content.error) {
document.getElementById('note-container').innerText = `Error: ${content.error}`
} else {
document.getElementById('note-container').innerText = content
}
})
document.getElementById('save-button').addEventListener('click', async () => {
const content = document.getElementById('note-input').value
const result = await window.electronAPI.saveNote(content)
if (result.error) {
alert(`Failed to save: ${result.error}`)
} else {
alert('Saved successfully!')
}
})
// Listen for updates from the main process
window.electronAPI.onNoteUpdated((message) => {
document.getElementById('status-container').innerText = message
// You could also refresh the note content here
})With Electron SSR, you can simply define a route handler that:
// In your main process setup
import { createSSR } from 'ssr-electron';
import fs from 'fs/promises';
import { safeStorage } from 'electron';
const ssr = createSSR({ debug: true });
// Register a route that reads a file and returns its content
ssr.registerRoute('/notes', async (request, url) => {
try {
// Access Node.js modules directly
const encryptedContent = await fs.readFile('user-notes.enc');
const content = await safeStorage.decryptString(encryptedContent);
// Return HTML with the content already integrated
return new Response(`
<div id="notes-container">
<h1>Your Notes</h1>
<div class="note-content">${content}</div>
<button hx-post="/save-note" hx-target="#notes-container">Save</button>
</div>
`, {
headers: { 'Content-Type': 'text/html' }
});
} catch (error) {
return new Response(`<div>Error: ${error.message}</div>`, { status: 500 });
}
});
// Handle saving notes
ssr.registerRoute('/save-note', async (request, url) => {
// Get form data from the request
const formData = await request.formData();
const noteContent = formData.get('content');
// Encrypt and save to filesystem
const encrypted = await safeStorage.encryptString(noteContent);
await fs.writeFile('user-notes.enc', encrypted);
// Return updated UI
return new Response(`
<div id="notes-container">
<h1>Your Notes</h1>
<div class="note-content">${noteContent}</div>
<button hx-post="/save-note" hx-target="#notes-container">Save</button>
<div class="success-message">Saved successfully!</div>
</div>
`, {
headers: { 'Content-Type': 'text/html' }
});
}, 'POST');In your renderer, the code is pure HTML and HTMX - no IPC needed:
<body>
<div id="app">
<div hx-get="/notes" hx-trigger="load">Loading...</div>
</div>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</body>Electron SSR also supports Server-Sent Events, making it easy to push updates from the main process to the renderer:
// In main process
import { watch } from 'fs';
// Set up file watcher
watch('user-notes.enc', () => {
// When file changes, broadcast to all clients
ssr.broadcastContent('note-updated', `
<div class="note-content">${decryptedContent}</div>
`);
});
// In renderer
<div hx-sse="connect:sse://events">
<div hx-sse="swap:note-updated" id="note-content">
Initial content
</div>
</div>This approach completely eliminates the need to manually handle IPC communication, making your Electron apps feel more like traditional web development while still leveraging the full power of Node.js and native modules.
Electron SSR works by
Essentially, it turns your main process into a server that your renderer can communicate with using standard web protocols, without actually running a server or opening any ports.
See the examples directory for complete examples:
I just started playing around with HTMX and Datastar, so feel free to submit updates or more examples to show off the power!
MIT
| Back | FazBrowse Home | New Git URL |