Esc

Your First Page

This walkthrough shows the complete path from a sitemap entry to Streak rendering a page — both live in the dev server and in a full build.


Step 1 — Add an Entry to streak.sitemap.json

[
  {
    "url": "/",
    "renderConfig": {
      "renderId": "homeRenderId",
      "metadata": {},
      "dataHandler": "HomeDataHandler",
      "rootLayout": "MainLayout",
      "widgets": [
        { "id": "PageHead",    "type": "PageHead" },
        { "id": "HelloBanner", "type": "HelloBanner" }
      ],
      "version": "1.0.0"
    }
  }
]

renderId must be globally unique across the sitemap. The url and version fields determine where a full build writes this page's output: out/1.0.0/raw-content.json.


Step 2 — Create the Data Handler

src/handlers/HomeDataHandler.ts

const getHomeData = async (
  metadata?: Record<string, unknown>,
  { common }: { common?: Record<string, unknown> } = {},
) => {
  return {
    status: 200,
    PageHead: {
      title: "Hello",
    },
    HelloBanner: {
      heading: "Hello World",
    },
  };
};

export default getHomeData;
  • Must be the default export
  • Must return { status: 200, ...widgetData }
  • Each key must match a widget id in the sitemap
  • The value is passed to the matching widget as props.data
  • Streak calls the handler as (metadata, { common }) — the sitemap's metadata field, and (if CommonHandler.ts exists) its return value. Both are optional in practice; a handler with no parameters still works

Step 3 — Create the Layout

src/layouts/MainLayout.tsx

import { WidgetPlaceholder } from "streak-forge/components";

const MainLayout = () => {
  console.info("Rendering Main Layout"); // runs on the server, never in the browser
  return (
    <html dir="ltr" lang="en">
      <head>
        <WidgetPlaceholder id="PageHead" type="PageHead" />
      </head>
      <body>
        <WidgetPlaceholder id="HelloBanner" type="HelloBanner" />
      </body>
    </html>
  );
};

export default MainLayout;

Every widget in widgets[] needs a matching WidgetPlaceholder. Both id and type must match the sitemap entry exactly.


Step 4 — Create the Widget

src/widgets/HelloBanner.tsx

type HelloBannerProps = {
  data?: {
    heading?: string;
  };
};

const HelloBanner = (props: HelloBannerProps) => {
  const heading = props?.data?.heading ?? "Hello World";
  return (
    <section>
      <h1>{heading}</h1>
    </section>
  );
};

export default HelloBanner;
  • Filename must exactly match the type in the sitemap (case-sensitive)
  • Data arrives as props.data — always use ?. and ?? because Streak passes { data: undefined } if the handler returned nothing for this widget
  • No useState, no useEffect — widgets are stateless

Step 5 — Render It

While developing, just run the dev server — no separate build step is needed:

bun run dev

Open http://localhost:3690/ (or whatever url you used). streak-forge dev renders that page fresh for the request: Middleware.ts (if present) resolves which render config to use, then CommonHandler.ts (if present) supplies shared data, then your data handler runs, then the layout, then each widget — and the resulting HTML is returned.


Step 6 — Build

To render every page in the sitemap and write it to disk — for example, before publishing — run:

bunx streak-forge build

This writes:

out/1.0.0/raw-content.json

raw-content.json is a JSON snapshot of the render: the page's renderId, metadata, dataHandler, rootLayout, version, and each widget's rendered HTML and collected styles. It's an intermediate build artifact, not a finished HTML page.

Optionally, run bunx streak-forge pre-build first to bundle your handlers, widgets, and layouts into .prebuild/ — later builds then skip re-transpiling that source.

Publishing this output to a live site is handled by Nexus — see the Nexus documentation for publishing/hosting details.


What Happens on Each Render

For every page — whether serving a live dev request or writing it out during a build — Streak follows the same sequence:

  1. Middleware.ts (if present) runs first and may override which render config is used for the URL.
  2. CommonHandler.ts (if present) supplies data shared across every page.
  3. The page's own data handler runs as (metadata, { common }) => {...}.
  4. The layout renders, with a WidgetPlaceholder for each widget.
  5. Each widget renders with its slice of the handler's data as props.data.
  6. Styles are collected.