I needed both a website and a playground, and I needed the code blocks on the website to actually highlight Hew — not just dump monochrome text and hope people squint past it.
Astro and Tailwind
hew.sh runs on Astro with Tailwind CSS, deployed to Cloudflare Pages. The choice was boring on purpose. Astro renders static HTML, ships zero JavaScript by default, and lets me drop interactive islands where I need them (the playground page, the grammar explorer) without hydrating the entire site. Tailwind handles the styling.
The interesting part was syntax highlighting. The first version used Shiki with a TextMate grammar for static Markdown blocks and a separate Monaco tokenizer for the playground. That worked, but it meant the website had a second opinion about Hew syntax: every new keyword, operator, or comment form had to be taught to the highlighters after the compiler already knew it. The Tailwind base color collision from Part 10 was the worst bug I hit during the website build, and it was a five-character rename.
Monaco and semantic tokens
Static syntax highlighting covers blog posts and docs. But the playground needs an editor, and the editor needs live tokenization — not just coloring a static block, but re-tokenizing on every keystroke. Monaco is the obvious choice (it’s VS Code’s editor, running in the browser), but the important bit is where the tokens come from.
The current pipeline asks the Hew frontend compiled to WASM for semantic_tokens(source). The compiler returns absolute spans tagged as keyword, type, function, variable, number, string, operator, or comment; the site projects those spans onto Monaco lines and theme scopes. The editor now highlights exactly what the compiler sees, including comments emitted by the frontend, instead of maintaining a regex grammar beside it.
Compiling the frontend to WASM
The playground has two halves: diagnostics and execution. Execution hits a server — the compiled program runs inside a gVisor sandbox (Google’s container isolation runtime), isolated in a container that gets torn down after a timeout. But diagnostics run entirely in the browser.
The Hew compiler’s Rust frontend — lexer, parser, type checker — compiles to WebAssembly via wasm-bindgen (the Rust-to-JS bridge generator). The WASM module exposes three functions: analyze(source) returns diagnostics as JSON, get_keywords() returns the keyword list for editor completion, and hover(source, offset) returns type information at a cursor position. The diagnostics integration is straightforward — a debounced content-change listener calls analyze(), converts byte offsets to line/column positions, and pushes Monaco markers:
editor.onDidChangeModelContent(() => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => runDiagnostics(monaco, editor), DEBOUNCE_MS);
});300 milliseconds after you stop typing, the entire Rust frontend runs against your code — in the browser, no server round-trip — and red squiggles appear under errors. The same lexer, parser, and type checker that produce native binaries through the MLIR pipeline, running as WASM to check your code while you write it.
The WASM build itself was not smooth. The wasm-bindgen output includes a workaround for a Safari TextDecoder bug — Safari’s decoder accumulates internal state and starts returning garbage after processing roughly 2 GB of text. The generated JS tracks total bytes decoded and recreates the decoder when it approaches the limit. I didn’t write that fix. wasm-bindgen generates it. I spent an hour debugging garbled diagnostics output in Safari before discovering the workaround existed and I just needed to update wasm-bindgen. (The generated code was already smarter than me about the bug I was trying to fix.)
The .wasm binary sits at public/wasm/hew_wasm_bg.wasm and gets fetched on first load of the playground page. It’s about 2 MB — the entire Rust frontend, compiled. Every time the compiler’s frontend changes, I rebuild the WASM module and commit the new binary. The commit messages read like a changelog: “rebuild WASM with literal coercion and cast support,” “rebuild WASM diagnostics from Rust frontend v0.1.7.” Not elegant, but it works — the website always matches the compiler.
hew.run
The standalone playground at hew.run is a SvelteKit app. It’s a separate repo because it started as a prototype before the website existed, and by the time hew.sh had its own playground page, hew.run had its own URL and its own users. Both share the same Monaco configuration — semantic-token highlighting, the dark and light themes, the WASM diagnostics module. The SvelteKit version has a slightly different layout (full-screen editor, output panel below) but the same engine underneath.
The keyword and type arrays in hew.run are copied from hew.sh manually. There’s no generator script for it — just copy the arrays when they change. It’s on the list of things to automate. (It has been on the list for three weeks.)
The blog itself
The TextMate grammar that highlights the code blocks in this post is the same grammar I wrote about creating in Part 10. The Monaco editor in the playground is the same one I use to test the code examples. In practice, when I break the grammar, I break both the editor and the blog at the same time.