Rocket
On this page

Request-Time JavaScript Pages#

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.

Choose the smallest render mode that fits#

Static first

Keep Pages static when the same output can be reused for every visitor.

Server when the request matters

Add render: 'server' for params, headers, cookies, live data, or adapter context.

Response for route contracts

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.

How Rocket Runs One#

Rocket uses the same Page Runtime model in local development, static builds, and deployment adapters:

  1. Page discovery reads every file matched by includeGlobs and records each Page by config.path.
  2. The Page Runtime receives a web Request and matches its pathname against configured Page paths, including :param route segments.
  3. A Page Module Loader loads the environment-specific module for the matched Page.
  4. Rocket creates pageData for this render and calls the JavaScript Page with (request, { params, pageData, adapterContext }).
  5. Rocket normalizes the JavaScript Page Result to a Response.
Response
Rocket sends the response exactly as returned.
string
Rocket sends HTML with text/html; charset=utf-8.
plain object or array
Rocket sends JSON through Response.json().
null or undefined
Rocket sends an empty Response.

Return a Response when you need custom status codes or headers. Otherwise, return the simplest value that expresses the result.

Choose The Render Mode#

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.

Share Request Data#

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.

Return JSON#

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

Read Route Parameters#

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 },
  );
}

Render HTML With PageData#

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.

Return Non-HTML Responses#

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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}

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.

Configure The Production Adapter#

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.

Checkpoint#

Run the site:

npm run start

Then visit the routes your own project defines:

This docs page uses namespaced demo routes:

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

Next step#

Continue with Deploy to build and publish the static output, plus any adapter output your request-time JavaScript Pages require.