Skip to content

Frontend Guide

AnthonyChen05 edited this page Feb 23, 2026 · 1 revision

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.


The {{ASSETS}} placeholder

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.


Calling your API

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' })
})

Minimal HTML template

<!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>

Minimal JS template

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('')
})

Using a frontend framework

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 build

The 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.


Real-time updates via Socket.io

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.


Tips

  • 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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

See also

Clone this wiki locally