Examples
Three reference plugins, each progressively more ambitious. All three are
single-file shells you can copy verbatim into a project scaffolded by
ssg plugins init.
1. Hello, plugin
The smallest possible plugin. Just a static HTML page that confirms it's mounted.
plugin.toml:
id = "hello-plugin"
version = "0.1.0"
name = "Hello Plugin"
description = "Smallest possible SSG plugin — proves your install works."
category = "tools"
entry = "index.html"
min_ssg_version = "0.29.0"
[[nav]]
label = "Hello"
path = "/plugins/hello-plugin/"
src/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Plugin</title>
</head>
<body>
<h1>Hello from your plugin!</h1>
<p id="loc"></p>
<script>
document.getElementById('loc').textContent =
'Mounted at ' + location.pathname;
</script>
</body>
</html>
That's it. ssg plugins build && ssg plugins publish ships it.
2. Block Heatmap
A real-world observability plugin. Pulls the last 7 days of blocked tool calls
from /api/json/audit and renders a heatmap.
plugin.toml:
id = "block-heatmap"
version = "0.1.0"
name = "Block Heatmap"
description = "Visualize blocked tool calls as a daily heatmap."
category = "observability"
entry = "index.html"
min_ssg_version = "0.29.0"
[[nav]]
label = "Block Heatmap"
path = "/plugins/block-heatmap/"
section = "activity"
order = 50
src/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Block Heatmap</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Block Heatmap</h1>
<p class="muted">Tool calls blocked by governance rules, last 7 days.</p>
</header>
<main>
<div id="grid" class="grid"></div>
<p id="empty" class="muted" hidden>No blocks in the last 7 days. Nice.</p>
</main>
<script src="app.js"></script>
</body>
</html>
src/app.js:
const cfg = window.__SSG_PLUGIN_CONFIG__ || {origin: '', token: ''};
async function load() {
const res = await fetch(`${cfg.origin}/api/json/audit?since=7d&decision=block`, {
headers: cfg.token ? {Authorization: `Bearer ${cfg.token}`} : {},
});
const body = await res.json();
render(body.events ?? []);
}
function render(events) {
const grid = document.getElementById('grid');
const empty = document.getElementById('empty');
if (events.length === 0) {
empty.hidden = false;
return;
}
const buckets = new Map();
for (const ev of events) {
const day = new Date(ev.ts).toISOString().slice(0, 10);
buckets.set(day, (buckets.get(day) ?? 0) + 1);
}
const max = Math.max(...buckets.values());
for (const [day, count] of buckets) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.style.opacity = (0.2 + 0.8 * (count / max)).toFixed(2);
cell.title = `${day}: ${count} blocks`;
cell.textContent = count;
grid.appendChild(cell);
}
}
load();
src/style.css:
body { font-family: -apple-system, sans-serif; padding: 32px; }
.grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; }
.cell {
background: #7c5cff;
color: #fff;
border-radius: 6px;
padding: 12px;
text-align: center;
font-weight: 600;
}
.muted { color: #888; font-size: 13px; }
Total: ~40 lines of code. The dashboard's existing /api/json/audit endpoint
does all the heavy lifting.
3. React + Vite plugin
For plugins that justify a build step, here's the pattern with Vite + React.
plugin.toml:
id = "react-example"
version = "0.1.0"
name = "React Example"
description = "Demonstrates a Vite + React plugin build pipeline."
category = "developer"
entry = "index.html"
min_ssg_version = "0.29.0"
[[nav]]
label = "React Example"
path = "/plugins/react-example/"
[build]
command = "pnpm install --silent && pnpm build"
package.json:
{
"name": "react-example",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^5.0.0"
}
}
vite.config.js:
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
// Important: base must match the plugin mount path so asset URLs work.
base: './',
build: {
outDir: 'dist',
emptyOutDir: true,
},
});
src/main.jsx:
import React, {useEffect, useState} from 'react';
import {createRoot} from 'react-dom/client';
const cfg = window.__SSG_PLUGIN_CONFIG__ || {origin: '', token: ''};
function App() {
const [status, setStatus] = useState(null);
useEffect(() => {
fetch(`${cfg.origin}/api/json/status`, {
headers: cfg.token ? {Authorization: `Bearer ${cfg.token}`} : {},
})
.then(r => r.json())
.then(setStatus);
}, []);
return (
<main style={{fontFamily: 'sans-serif', padding: 32}}>
<h1>React Example</h1>
<pre>{status ? JSON.stringify(status, null, 2) : 'Loading…'}</pre>
</main>
);
}
createRoot(document.getElementById('root')).render(<App />);
index.html (at project root, not src/):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Build and ship:
ssg plugins build # runs `pnpm install && pnpm build` then packages dist/
ssg plugins publish