with one click
streaming-ssr
// Add streaming SSR to a merjs page. Use when the user wants shell-first rendering, skeleton placeholders, or parallel data fetching that resolves inline.
// Add streaming SSR to a merjs page. Use when the user wants shell-first rendering, skeleton placeholders, or parallel data fetching that resolves inline.
Full production build — codegen, compile, prerender, and prepare for deployment.
Build and start the merjs dev server with hot reload. Use when the user wants to run, start, or serve the project.
Scaffold a new merjs API route. Use when the user wants to create a new API endpoint.
Scaffold a new merjs page. Use when the user wants to create a new page, route, or view.
Work with the merjs Zig web framework. Use when creating pages, API routes, WASM modules, or modifying the merjs build system. Provides conventions for file-based routing, SSR, dynamic routes, type-safe APIs via dhi, sessions, and Cloudflare Workers deployment.
| name | streaming-ssr |
| description | Add streaming SSR to a merjs page. Use when the user wants shell-first rendering, skeleton placeholders, or parallel data fetching that resolves inline. |
| argument-hint | [page-name] |
| disable-model-invocation | true |
Scaffold or upgrade app/$ARGUMENTS.zig to use renderStream — shell-first streaming with skeleton placeholders that resolve as data arrives.
renderStream is called instead of render when the route is hitstream.write(html) flushes bytes to the browser immediately (chunked transfer encoding)stream.placeholder(id, skeleton_html) writes a shimmer skeleton + <div id="P:id"> into the live DOMmer.fetchAll() fetches multiple URLs in parallel (threads on dev server, two-phase WASM bridge on Cloudflare Workers)stream.resolve(id, real_html) injects a hidden div + inline <script> that swaps the skeleton with real contentstream.flush() ends the responseOn Cloudflare Workers, mer.fetchAll() uses a two-phase JS bridge:
collect_fetch_urls)fetch()app/$ARGUMENTS.zig:const std = @import("std");
const mer = @import("mer");
pub const meta: mer.Meta = .{
.title = "PAGE_TITLE",
.description = "PAGE_DESCRIPTION",
.extra_head = "<style>" ++ page_css ++ "</style>",
};
// Fallback for non-streaming clients (required)
pub fn render(req: mer.Request) mer.Response {
_ = req;
return mer.html("<p>Requires streaming.</p>");
}
pub fn renderStream(req: mer.Request, stream: *mer.StreamWriter) void {
const alloc = req.allocator;
// Shell hits browser immediately — before any fetch
stream.write(
\\<div class="page">
\\ <h1>PAGE_TITLE</h1>
);
// Skeleton placeholders — visible in DOM while fetching
stream.placeholder("section-a",
\\<div class="skeleton">Loading...</div>
);
// Fetch multiple URLs in parallel
const results = mer.fetchAll(alloc, &.{
.{ .url = "https://api.example.com/data-a" },
.{ .url = "https://api.example.com/data-b" },
});
defer for (results) |r| if (r) |ok| ok.deinit(alloc);
// Resolve skeleton → real content inline
if (results[0]) |res| {
stream.resolve("section-a", buildCard(alloc, res.body));
} else {
stream.resolve("section-a", "<p>Failed to load.</p>");
}
stream.write("</div>");
stream.flush();
}
fn buildCard(alloc: std.mem.Allocator, body: []u8) []const u8 {
_ = body;
return std.fmt.allocPrint(alloc, "<div class=\"card\">data</div>", .{}) catch "error";
}
const page_css =
\\.page { max-width: 640px; margin: 0 auto; }
\\.card { background: var(--bg2); border-radius: 10px; padding: 20px; }
\\.skeleton { background: var(--bg3); border-radius: 10px; padding: 20px; height: 80px;
\\ position: relative; overflow: hidden; }
\\.skeleton::after { content:''; position:absolute; inset:0;
\\ background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent);
\\ animation:shimmer 1.5s infinite; }
\\@keyframes shimmer { 0%{transform:translateX(-100%)} 100%{transform:translateX(100%)} }
;
zig build codegen to register the routezig build serve and visit the route — watch skeletons resolverender (fallback) and renderStreampub const meta: mer.Metastream.placeholder(id, skeleton) must come BEFORE mer.fetchAllstream.resolve(id, html) must come AFTER results are readystream.flush() must be called at the endmer.fetchAll returns []?mer.FetchResult — always handle the null casedefer deinit on results to free memoryid passed to placeholder and resolve must match exactlyapp/stream-demo.zigsrc/mer.zig — StreamWriter, fetchAll, placeholder, resolvesrc/ssr.zig — streaming enginesrc/router.zig — dispatchStream / dispatchBufferedsrc/worker.zig — two-phase fetch exports (collect_fetch_urls, provide_fetch_result)PRIMITIVES.md — full API reference