-
Notifications
You must be signed in to change notification settings - Fork 0
Frontend Guide
How to write the HTML, CSS, and JS for your module's UI.
Prism doesn't impose any frontend framework. Write vanilla JS, or drop in React/Vue/Svelte — it's just files in a public/ folder.
Your HTML can't hardcode the asset path because it depends on the module name. Use {{ASSETS}} as a placeholder — it's replaced at serve time:
<link rel="stylesheet" href="{{ASSETS}}/style.css" />
<script src="{{ASSETS}}/app.js"></script>If your module folder is kanban, this becomes /kanban-assets/style.css when the page is served.
Use window.location.pathname as the base for all API calls. This means your JS works correctly regardless of what the module folder is named:
const API = window.location.pathname.replace(/\/$/, '')
// e.g. "/kanban"
// Fetch from your module's routes
const data = await fetch(API + '/api/boards').then(r => r.json())
// POST with a body
await fetch(API + '/api/boards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'My Board' })
})<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Module</title>
<link rel="stylesheet" href="{{ASSETS}}/style.css" />
</head>
<body>
<div id="app"></div>
<script src="{{ASSETS}}/app.js"></script>
</body>
</html>const API = window.location.pathname.replace(/\/$/, '')
async function apiFetch(method, path, body) {
const res = await fetch(API + path, {
method,
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined,
})
if (!res.ok) {
const e = await res.json().catch(() => ({}))
throw new Error(e.error ?? `HTTP ${res.status}`)
}
return res.json()
}
document.addEventListener('DOMContentLoaded', async () => {
const data = await apiFetch('GET', '/api/items')
document.getElementById('app').innerHTML = data.items
.map(item => `<div>${item.title}</div>`)
.join('')
})You can use any framework — just point it at your public/ folder as the output directory.
Example with Vite (React/Vue/Svelte):
# Create a Vite project
npm create vite@latest my-module-ui -- --template react
# Build it into the module's public folder
# In vite.config.js:
export default {
build: {
outDir: '../backend/src/modules/my-module/public'
}
}
npm run buildThe built files land in public/ and get served automatically.
Important: When using a framework, your JS will handle routing client-side. Make sure the entry HTML file is still index.html and references your built JS/CSS. The {{ASSETS}} replacement only applies to files served by Prism's fs.readFileSync handler — if you're serving a pre-built app, hardcode the asset path or inject it at build time.
The server has Socket.io attached. You can connect from the browser and receive live pushes:
// Load Socket.io client (served by the socket.io server automatically)
const socket = io()
socket.on('connect', () => {
console.log('Connected:', socket.id)
// Join a room to receive targeted events
socket.emit('join', { userId: 'user-1' })
})
// Listen for events your backend emits
socket.on('item:updated', (data) => {
console.log('Item updated:', data)
reloadItems()
})See Real-Time-with-SocketIO for the backend side.
- No build step required. Vanilla JS with ES modules works great for small tools.
-
CSS variables. If you want to match the Prism dashboard aesthetic, the CSS variables are documented in
dashboard/public/style.css. - Escape user input. If you're rendering user data into innerHTML, always escape it to avoid XSS.
function esc(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}- Creating-a-Module — Full walkthrough
- Routing-and-Assets — How asset paths work
- Real-Time-with-SocketIO — Live updates