Keep Pages static when the same output can be reused for every visitor.
Most Rocket Pages should stay static. Use Markdown Pages for durable content, and use concrete
JavaScript Pages when a route can be rendered once during rocket build.
Switch a JavaScript Page to request-time rendering only when the response depends on the incoming
Request: route parameters, query strings, headers, cookies, live data, per-request status codes,
generated non-HTML output, or platform data from a deployment adapter.
Keep Pages static when the same output can be reused for every visitor.
Add render: 'server' for params, headers, cookies, live data, or adapter context.
Return a Response when status, headers, cache policy, or content type are part of the API.
The examples below use one tiny component catalog and produce five route shapes from it: a static
JSON file, request-time JSON, parameterized JSON, parameterized HTML, and generated SVG. Direct
source-plus-response examples use Request Demo frames so the live GET response stays paired with
the source that creates it.
Rocket uses the same Page Runtime model in local development, static builds, and deployment adapters:
includeGlobs and records each Page by
config.path.Request and matches its pathname against configured Page
paths, including :param route segments.pageData for this render and calls the JavaScript Page with
(request, { params, pageData, adapterContext }).Response.Responsestringtext/html; charset=utf-8.Response.json().null or undefinedResponse.Return a Response when you need custom status codes or headers. Otherwise, return the simplest
value that expresses the result.
Start with the default static render mode for concrete JavaScript Pages:
/** @type {import('@rocket/js/types.js').JsPage} */
export default async request => ({
path: new URL(request.url).pathname,
generatedAt: new Date().toISOString(),
});
export const config = {
path: '/build-info.json',
metadata: { title: 'Build info' },
menu: false,
};
During rocket build, Rocket creates one build-time Request for /build-info.json and writes the
JSON response to static output. The Request Demo uses this guide's namespaced demo route so the
static JSON response can be inspected in place.
Add render: 'server' when the Page needs request-time behavior or when the path contains route
parameters:
export const config = {
path: '/api/components/:componentName.json',
render: 'server',
menu: false,
};
Parameterized JavaScript Pages need render: 'server' today because Rocket does not yet have a
static params enumeration API.
Keep reusable facts in ordinary modules. Pages should import those modules instead of duplicating lookup tables across API, HTML, and image routes:
export const componentCatalog = [
{
slug: 'button',
name: 'Button',
status: 'stable',
summary: 'A link-style action for navigation and setup tasks.',
variants: ['primary', 'secondary'],
},
{
slug: 'callout',
name: 'Callout',
status: 'preview',
summary: 'A short notice block for guidance, warnings, and success messages.',
variants: ['info', 'warning', 'success'],
},
];
export function findComponent(slug) {
return componentCatalog.find(component => component.slug === slug);
}
This keeps the request-time Page focused on request handling while the domain data remains easy to test and reuse from static Pages.
For a simple JSON endpoint, return a plain object and let Rocket normalize it:
import { componentCatalog } from '../../componentCatalog.js';
/** @type {import('@rocket/js/types.js').JsPage} */
export default async request => {
const url = new URL(request.url);
return {
path: url.pathname,
generatedAt: new Date().toISOString(),
components: componentCatalog,
};
};
export const config = {
path: '/api/components.json',
metadata: { title: 'Components API' },
render: 'server',
menu: false,
};
This Page uses render: 'server' because generatedAt should be evaluated for each request. If the
data should be captured once during the build, remove render: 'server'.
Parameterized routes expose their dynamic segments through context.params:
import { findComponent } from '../../componentCatalog.js';
/** @type {import('@rocket/js/types.js').JsPage} */
export default async (_request, { params }) => {
const componentName = params.componentName || '';
const component = findComponent(componentName);
if (!component) {
return Response.json(
{
error: 'Component not found',
componentName,
},
{ status: 404 },
);
}
return {
slug: componentName,
...component,
};
};
export const config = {
path: '/api/components/:componentName.json',
metadata: { title: 'Component API' },
render: 'server',
menu: false,
};
Use menu: false for API routes and parameterized routes that do not have a single concrete link
target. The same Page file handles both matching and missing component names. The 404 branch returns
a Response so the status code is part of the route contract:
if (!component) {
return Response.json(
{
error: 'Component not found',
componentName,
},
{ status: 404 },
);
}
When a JavaScript Page returns HTML, use the pageData Rocket creates for the current request.
Set pageData.content, render a layout, and return the rendered string:
import { html } from 'lit';
import { ssrRender } from '@rocket/js/ssr.js';
import { layout } from '@rocket/js/layout.js';
import { findComponent } from '../componentCatalog.js';
/** @type {import('@rocket/js/types.js').JsPage} */
export default async (request, context) => {
const component = findComponent(context.params.componentName);
if (!component) {
return new Response('Component not found', { status: 404 });
}
const url = new URL(request.url);
const apiPath = `/api/components/${component.slug}.json`;
context.pageData.title = `${component.name} Playground`;
context.pageData.content = html`
<h1>${component.name} Playground</h1>
<p>${component.summary}</p>
<h2>Variants</h2>
<ul>
${component.variants.map(variant => html`<li>${variant}</li>`)}
</ul>
<p>Metadata for this component is available from <a href=${apiPath}>${apiPath}</a>.</p>
<p>Rendered for <code>${url.pathname}</code>.</p>
`;
return ssrRender(layout(context.pageData));
};
export const config = {
path: '/playground/:componentName',
metadata: { title: 'Component playground' },
render: 'server',
menu: false,
};
The :componentName segment becomes context.params.componentName, so /playground/button and
/playground/callout render different HTML from one Page file.
Rocket finalizes HTML responses after your function returns. That means Rocket-owned icon output and other Page Runtime finishing work still apply to HTML returned from JavaScript Pages.
Use a Response when the content type, cache policy, or status is part of the route contract:
import { findComponent } from '../componentCatalog.js';
/** @type {import('@rocket/js/types.js').JsPage} */
export default async (request, context) => {
const component = findComponent(context.params.componentName);
if (!component) {
return new Response('Component not found', { status: 404 });
}
const name = escapeSvg(component.name);
const summary = escapeSvg(component.summary);
const status = escapeSvg(component.status);
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" role="img">
<rect width="1200" height="630" fill="#0f172a" />
<rect x="72" y="72" width="1056" height="486" rx="28" fill="#f8fafc" />
<text x="120" y="180" fill="#0f766e" font-family="Arial, sans-serif" font-size="36">
Acme UI Docs
</text>
<text x="120" y="300" fill="#111827" font-family="Arial, sans-serif" font-size="96" font-weight="700">
${name}
</text>
<text x="120" y="380" fill="#334155" font-family="Arial, sans-serif" font-size="34">
${summary}
</text>
<text x="120" y="472" fill="#0f766e" font-family="Arial, sans-serif" font-size="30">
Status: ${status}
</text>
</svg>`;
return new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml; charset=utf-8',
'Cache-Control': 'public, max-age=300',
},
});
};
function escapeSvg(value) {
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
}
export const config = {
path: '/open-graph/:componentName.svg',
metadata: { title: 'Component Open Graph Image' },
render: 'server',
menu: false,
};
This Page returns SVG instead of HTML, so Rocket sends the Response exactly as returned.
Request-time Pages work in local development through Rocket's dev server. A production build needs
an adapter when any Page uses render: 'server':
import { netlify } from '@rocket/js/adapters/netlify.js';
/** @type {import('@rocket/js/types.js').RocketConfig} */
export default {
includeGlobs: ['src/pages/**/*.rocket.{md,js}'],
adapter: netlify(),
};
Without an adapter, rocket build fails early when it finds server-rendered Pages. With the
Netlify adapter configured, Rocket still writes static Pages to dist/ and also bundles
server-rendered Pages into a Netlify Function. The generated function creates a Page Runtime and
passes Netlify's request context through as context.adapterContext.
For production settings, generated output, and Netlify-specific verification, see Netlify Adapter.
Run the site:
npm run start
Then visit the routes your own project defines:
/build-info.jsonStatic JSON rendered during the build./api/components.jsonRequest-time JSON rendered for each request./api/components/button.jsonParameterized JSON using context.params./api/components/unknown.jsonParameterized 404 JSON returned as a Response./playground/buttonParameterized HTML rendered through a layout./open-graph/button.svgGenerated non-HTML output returned as a Response.This docs page uses namespaced demo routes:
/request-time-javascript-pages/demo/build-info.json/request-time-javascript-pages/demo/api/components.json/request-time-javascript-pages/demo/api/components/button.json/request-time-javascript-pages/demo/api/components/unknown.json/request-time-javascript-pages/demo/playground/button/request-time-javascript-pages/demo/open-graph/button.svgThen run the production build:
npm run build
The site now mixes static Pages for durable content and request-time JavaScript Pages for request-shaped responses without changing how Pages are discovered.
Continue with Deploy to build and publish the static output, plus any adapter output your request-time JavaScript Pages require.