vt-ssg

A lightweight static site generator for Val Town. Takes a markdown string and a JSX layout template, returns an HTML string. Includes a Hono-compatible FileSystemModule for writing output files directly into a val via the API.

Stack: marked · gray-matter · Hono JSX

Quick Start

/** @jsxImportSource npm:hono@4/jsx */ import { render } from "https://esm.town/v/nbbaier/vt-ssg/main.tsx"; const html = await render({ markdown: `--- title: Hello World date: 2026-04-04 --- # Hello Some **markdown** content.`, }); // html is a complete HTML string with <!DOCTYPE html>

Custom Layout

Pass a Hono JSX component as the layout option. It receives title, content (rendered HTML), and the full frontmatter object as props.

/** @jsxImportSource npm:hono@4/jsx */ import { render } from "https://esm.town/v/nbbaier/vt-ssg/main.tsx"; import type { LayoutComponent } from "https://esm.town/v/nbbaier/vt-ssg/types.ts"; const BlogLayout: LayoutComponent = ({ title, content, frontmatter }) => ( <html lang="en"> <head> <title>{title} | My Blog</title> <link rel="stylesheet" href="/styles.css" /> </head> <body> <header> <nav>Home | About</nav> </header> <article> <h1>{title}</h1> <time>{String(frontmatter.date)}</time> <div dangerouslySetInnerHTML={{ __html: content }} /> </article> <footer>© 2026</footer> </body> </html> ); const html = await render({ markdown: source, layout: BlogLayout, layoutProps: { theme: "dark" }, });

Batch Render + Write Files

Use renderToFiles to render multiple pages and write them as static HTML files into a val via the API. Requires a VT_WRITE_TOKEN env var with write permissions.

/** @jsxImportSource npm:hono@4/jsx */ import { renderToFiles } from "https://esm.town/v/nbbaier/vt-ssg/main.tsx"; const results = await renderToFiles({ pages: [ { path: "index.html", markdown: indexMd }, { path: "about.html", markdown: aboutMd, layout: BlogLayout }, { path: "blog/post-1.html", markdown: post1Md }, ], layout: DefaultBlogLayout, // shared layout (pages can override) dir: "static", // output directory (default: "static") fs: { callerUrl: import.meta.url }, }); // Output is relative to the calling file's directory by default. // Use fs: { callerUrl: import.meta.url, root: "." } to write from the val root.

Drop-in with Hono's toSSG

The createValTownFS function returns a Hono-compatible FileSystemModule, so you can use it directly with Hono's SSG helper:

import { Hono } from "npm:hono@4"; import { toSSG } from "npm:hono@4/ssg"; import { createValTownFS } from "https://esm.town/v/nbbaier/vt-ssg/fs.ts"; const app = new Hono(); app.get("/", (c) => c.html("<h1>Home</h1>")); app.get("/about", (c) => c.html("<h1>About</h1>")); const fs = createValTownFS({ callerUrl: import.meta.url }); const result = await toSSG(app, fs, { dir: "./static" }); // Output is relative to the calling file by default. // Writes static/index.html and static/about.html next to this file.

API Reference

render(options: RenderOptions): Promise<string>

OptionTypeRequiredDescription
markdownstringRaw markdown string, optionally with YAML frontmatter
layoutLayoutComponentHono JSX component. Defaults to a minimal HTML shell.
layoutPropsRecord<string, unknown>Extra props merged into the layout props
markedOptionsMarkedOptionsOptions passed to marked. GFM enabled by default.

renderToFiles(options: RenderToFilesOptions): Promise<PageResult[]>

OptionTypeRequiredDescription
pagesPageInput[]Array of pages to render (each has path, markdown, optional layout/layoutProps)
fsValTownFSOptionsFS config: { callerUrl: import.meta.url } (or valId/token)
layoutLayoutComponentShared layout for all pages (individual pages can override)
layoutPropsRecord<string, unknown>Shared extra layout props
markedOptionsMarkedOptionsOptions passed to marked
dirstringOutput directory prefix. Defaults to "static".

Each item in the returned PageResult[] has:

FieldTypeDescription
pathstringResolved output path that was (or was attempted to be) written
successbooleanWhether the page was rendered and written successfully
errorstring | undefinedError message if success is false

createValTownFS(options: ValTownFSOptions): FileSystemModule

OptionTypeRequiredDescription
callerUrlstringPass import.meta.url to target the calling val
valIdstringTarget val ID. Takes precedence over callerUrl.
tokenstringAPI token. Defaults to VT_WRITE_TOKEN env var.
rootstringBase directory for resolving output paths. Omitted: relative to the calling file's directory. ".": relative to the val root. Any other string: used as-is from the val root.

Path Resolution Examples

Given a build script at examples/my-site/build.tsx:

rootwriteFile("static/index.html")Resolves to
(omitted)relative to callerexamples/my-site/static/index.html
"."relative to val rootstatic/index.html
"dist"relative to dist/dist/static/index.html

getMarkdown(contentDir, callerUrl): Promise<{ path, normed, markdown }[]>

Reads all .md files from a directory in the current val and returns their paths and contents. Useful for file-based sites.

ParamTypeDescription
contentDirstringDirectory path to search for markdown files
callerUrlstringPass import.meta.url to resolve paths relative to the calling val

Each result object has:

FieldTypeDescription
pathstringFull file path within the val
normedstringPath relative to contentDir, without leading slash or extension (useful as a page slug)
markdownstringRaw markdown content

parseFrontmatter(raw: string): FrontmatterResult

Parse YAML frontmatter from a raw markdown string. Returns { data, content } where data is the parsed frontmatter object and content is the markdown body with frontmatter stripped.

LayoutProps

PropTypeDescription
titlestringExtracted from frontmatter.title, or ""
contentstringRendered HTML from the markdown body
frontmatterRecord<string, unknown>Full parsed frontmatter object
...layoutPropsunknownAny extra props from layoutProps

Architecture

Rendering mermaid diagram...

File Structure

main.tsx — Public API: render() + renderToFiles() types.ts — TypeScript type definitions fs.ts — Hono-compatible FileSystemModule for Val Town frontmatter.ts — gray-matter wrapper (parseFrontmatter) markdown.ts — marked wrapper (parseMarkdown, getMarkdown) defaults.tsx — Default HTML layout component test.tsx — Test script examples/ — Usage examples