Your First Page
This walkthrough shows the complete path from a sitemap entry to a generated HTML file.
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"
}
}
] The renderId is globally unique and becomes the output folder name: out/homeRenderId/index.html.
Step 2 — Create the Data Handler
src/handlers/HomeDataHandler.ts
const getHomeData = async () => {
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
idin the sitemap - The value is passed to the matching widget as
props.data
Step 3 — Create the Layout
src/layouts/MainLayout.tsx
import { WidgetPlaceholder } from "streak/components";
const MainLayout = () => {
console.info("Rendering Main Layout"); // runs at build time on the server
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. The console.info call runs at build time, not in the browser.
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
typein 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, nouseEffect— widgets are stateless
Step 5 — Build
bun run build This runs streak-forge pre-build, which processes the sitemap through 7 stages and writes:
out/homeRenderId/index.html
What the Output Contains
The generated HTML file contains:
- The fully rendered layout with all widget HTML injected in place of
WidgetPlaceholderelements - A hidden
<script id="w-m">element with JSON metadata listing every widget's id and asset path - The
app.jsIIFE — the client-side coordinator - Inline
<script>tags for anyScriptcomponents used in widgets
The page is complete static HTML. No framework runtime is included.
What Happens in the Browser
When the page loads, app.js:
- Records the page load time
- Reads widget metadata from
<script id="w-m"> - Checks the
__visession cookie to determinewindow.ftr(first-time render) - Loads widget assets sequentially, chaining callbacks
- Makes
gDomhelpers available onwindowfor Script functions to use