Pages that don't exist until you click them.
No server. Routes live in the browser. A Service Worker intercepts navigation and generates HTML on the fly — styled, dynamic, instant.
No files. No static HTML to maintain. Define routes in JavaScript, and pages materialize at the moment of navigation.
No build step. Works with any framework or vanilla JS. Drop it in and links start working.
import { init } from '@lorb/phantom-page';
init({
routes: {
'/hello': () => `<html><body><h1>Hi.</h1></body></html>`,
},
});
// Click a link to /hello → the page appears. It never existed before.npm install @lorb/phantom-pageHandlers return full HTML — include any CSS you want.
init({
routes: {
'/about': () => `
<html>
<head>
<style>
body { font-family: system-ui; background: #0a0a0a; color: #fafafa; }
h1 { font-size: 4rem; font-weight: 200; letter-spacing: -0.02em; }
.container { max-width: 640px; margin: 0 auto; padding: 4rem 2rem; }
</style>
</head>
<body>
<div class="container">
<h1>About</h1>
<p>This page was generated at ${new Date().toLocaleTimeString()}.</p>
</div>
</body>
</html>
`,
},
});:param captures URL segments. Use them to generate content.
init({
routes: {
'/user/:id': ({ id }) => `
<html><body>
<h1>Profile #${id}</h1>
<p>Generated on the fly — no database, no server.</p>
</body></html>
`,
'/post/:slug': ({ slug }) => `
<html><body>
<h1>${slug.replace(/-/g, ' ')}</h1>
</body></html>
`,
},
});init({
routes: {
'/docs/*': () => `
<html><body>
<h1>Documentation</h1>
<p>Every /docs/* URL resolves to this page.</p>
</body></html>
`,
},
});Return a Response object for full control over headers and status codes.
init({
routes: {
'/api/time': () => new Response(
JSON.stringify({ time: Date.now() }),
{ headers: { 'Content-Type': 'application/json' } },
),
'/api/redirect': () => new Response(null, {
status: 302,
headers: { Location: '/hello' },
}),
},
});The Service Worker needs a moment to activate. Use waitUntilReady() if you navigate programmatically.
import { init, waitUntilReady } from '@lorb/phantom-page';
init({ routes: { /* ... */ } });
await waitUntilReady();
window.location.href = '/hello'; // Safe — route is activeBy default, phantom-page creates a Service Worker from a blob URL. This works in Chrome and Edge but throws SecurityError in Firefox and Safari.
For cross-browser support, use the swUrl option to register a real Service Worker file:
import { init, generateSWScript } from '@lorb/phantom-page';
// 1. Generate the SW script at build time and write it to a file:
const script = generateSWScript(
{ '/hello': () => `<html><body><h1>Hi.</h1></body></html>` },
);
// Write `script` to your public directory as `phantom-sw.js`
// 2. Point init() to the file:
init({
routes: { '/hello': () => `<html><body><h1>Hi.</h1></body></html>` },
swUrl: '/phantom-sw.js',
});You can also check support at runtime:
import { init, isSupported } from '@lorb/phantom-page';
if (isSupported()) {
// Blob URL approach — Chrome/Edge only
init({ routes: { /* ... */ } });
} else {
// Fall back to swUrl approach
init({ routes: { /* ... */ }, swUrl: '/phantom-sw.js' });
}| Browser | Default (blob URL) | With swUrl |
|---|---|---|
| Chrome / Edge | Full support | Full support |
| Firefox | Not supported | Full support |
| Safari | Not supported | Full support |
- Handlers must be self-contained. They run inside a Service Worker via
toString()serialization — closures and external imports won't work. phantom-page validates this at init and throws if a handler references outside scope. - Same-origin only (Service Worker limitation).
- SSR-safe — no-op without
navigator.serviceWorker.
| Export | Description |
|---|---|
init(config) |
Register routes. Returns async destroy() function |
waitUntilReady() |
Resolves when the Service Worker is active and controlling the page |
destroy() |
Unregister the Service Worker |
isSupported() |
Check if blob URL SW registration works in this browser |
generateSWScript(routes, fallback?) |
Generate the Service Worker script string (for the swUrl approach) |
Config options:
| Option | Type | Description |
|---|---|---|
routes |
Record<string, RouteHandler> |
Route pattern to handler map |
fallback |
string |
URL to redirect to on handler errors |
swUrl |
string |
Path to a pre-built SW file (required for Firefox/Safari) |
Route patterns: /path, /path/:param, /path/*
𖦹 MIT — Lorb.studio