Skip to content
Vincle

Loading…

    Static generation

    Build HTML files at compile time. renderToStatic gives you a context to render all your pages, then emit fragment files if any page uses <Defer> with a lazy factory. The browser fetches fragments after the shell page loads: no streaming connection required.

    When to use static generation#

    Static generation is the right choice when:

    • Your content is known at build time (docs, blogs, marketing sites)
    • You deploy to a CDN or static file server (S3, Nginx, Cloudflare Pages)
    • You want zero server infrastructure for serving pages
    • Fragments can be fetched lazily by the client

    If you need fragments to arrive in the same HTTP response as the shell, use Streaming instead.

    Pure static#

    No lazy <Defer> in your pages? Call renderToStatic without an adapter. Each ctx.renderPage call applies any configured shell transforms and returns a complete HTML document, ready to write to a file.

    import type { JSX } from "@vincle/core";
    import { renderToStatic } from "@vincle/flow";
    import { writeFile } from "node:fs/promises";
    declare const pages: { Component: () => JSX.Element; out: string }[];
    await renderToStatic(async (ctx) => {
    await Promise.all(
    pages.map(async (page) => {
    const html = await ctx.renderPage(() => <page.Component />);
    await writeFile(page.out, "<!DOCTYPE html>\n" + html);
    }),
    );
    });

    With deferred fragments#

    Pass an adapter and call ctx.emitFragments after rendering all pages. Each deferred fragment is rendered, already Frame-wrapped, and passed to your callback with its id, its URL and its HTML.

    import type { JSX } from "@vincle/core";
    import { renderToStatic } from "@vincle/flow";
    import { NativeAdapter } from "@vincle/flow/adapters";
    import { writeFile } from "node:fs/promises";
    declare const pages: { Component: () => JSX.Element; out: string }[];
    await renderToStatic(
    async (ctx) => {
    for (const page of pages) {
    const html = await ctx.renderPage(() => <page.Component />);
    await writeFile(page.out, "<!DOCTYPE html>\n" + html);
    }
    // One .html file per deferred fragment.
    await ctx.emitFragments((_id, url, html) => writeFile("./dist" + url, html));
    },
    { adapter: NativeAdapter },
    );

    The browser loads the shell page first; each <Defer> placeholder carries its fragment's URL, and the adapter fetches it. Fragment files are static HTML at predictable URLs, so they can be cached aggressively.

    The generatePath option sets each fragment's URL:

    Option Default Description
    generatePath (id) => `/fragments/${id}.html` URL path for each fragment

    On-demand regeneration#

    A full rebuild is wasteful when only one piece of data changed: a price, a stock count, a comment count. Because each fragment is a standalone file at a predictable URL, only that file needs to change; the shell page that references it is untouched.

    renderFragment produces exactly the bytes a full build would have written for one id, without rendering any page:

    import { renderFragment } from "@vincle/flow";
    import { NativeAdapter } from "@vincle/flow/adapters";
    declare function fetchPrice(symbol: string): Promise<{ value: number }>;
    // A Netlify/Vercel Edge Function: both run standard Request → Response
    // handlers, so this needs no platform SDK import.
    export default async function handler(req: Request): Promise<Response> {
    const symbol = new URL(req.url).searchParams.get("symbol");
    if (!symbol) return new Response("Missing symbol", { status: 400 });
    const price = await fetchPrice(symbol);
    const { url, html } = await renderFragment(
    `price-${symbol}`,
    <span>{price.value.toFixed(2)}</span>,
    { adapter: NativeAdapter },
    );
    // `url` matches the path the full build already wrote this fragment to,
    // upload `html` there (blob store, on-demand revalidation, CDN purge +
    // PUT…). The shell page that includes it never needs rebuilding.
    return Response.json({ url, html });
    }

    How you publish those bytes at url (a blob store, a revalidation call, a CDN purge and re-upload) depends on the host and stays in your handler. See renderFragment for the full API.