This Blog: Generated by a Static Site Generator Written in My Own Language
This blog is generated by kovdocs -- a static site generator written in kov. kov compiles to RISC-V. The RISC-V binary runs in kov's built-in emulator. The emulator reads markdown files from disk via VFS MMIO, processes them through a markdown parser written in kov, and writes HTML files back to disk via VFS MMIO. 42 blog posts about building hypervisors, generated by a programming language I wrote, running on a CPU architecture I don't even own hardware for.
The whole stack is mine. The language, the compiler, the emulator, the markdown parser, the HTML template, the CSS -- all written from scratch. No dependencies beyond the Rust standard library that the compiler itself is built on.
How it works
The blog generator is two kov source files. main.kov handles the index page and drives the post conversion loop. template.kov handles the HTML shell, CSS generation, and navigation. A post conversion reads a markdown file, processes it line by line, and writes the corresponding HTML.
// main.kov — the blog generator entry point
import io;
import fs;
import template;
fn main() {
println("blog v0.2" as u32);
// build index page
let idx = fs_create("blog/output/index.html" as u32);
build_index(idx);
fs_flush(idx);
fs_close(idx);
// convert each post from markdown to HTML
convert_post(
"blog/posts/01-blue-pill.md" as u32,
"blog/output/01-blue-pill.html" as u32,
"Building an AMD SVM Blue-Pill Hypervisor" as u32,
"2026-01-15" as u32,
// ... tags, reading time ...
);
println(" 01-blue-pill.html" as u32);
println("done" as u32);
}
The markdown parser
The markdown parser is part of convert_post in main.kov. It reads the input file byte by byte into a 128-byte line buffer, processes each line when it hits a newline, and writes the HTML equivalent to the output file. It handles headings, bold text, inline code, code blocks, unordered lists, links, and horizontal rules.
// Line processing in the markdown converter:
loop {
let byte = fs_read_byte(in_fd);
if byte == 4294967295 { // EOF (u32 max)
if line_len > 0 {
process_line(out_fd, line_buf, line_len, in_code, in_list);
}
break;
}
if byte == 10 { // newline
let state = process_line(out_fd, line_buf, line_len, in_code, in_list);
in_code = state & 1;
in_list = (state >> 1) & 1;
line_len = 0;
} else {
write_word(line_buf + line_len, byte);
line_len = line_len + 1;
}
}
The process_line function checks the first few characters to determine the line type. Lines starting with ## become <h2> tags. Lines starting with - become list items. Lines with triple backticks toggle code block mode. Everything else is a paragraph. The state is packed into a single integer -- bit 0 for "inside code block", bit 1 for "inside list" -- because kov doesn't have tuples.
CSS as string literals
The CSS template is written directly in kov as a sequence of fs_write_str calls. Every line of CSS is a string literal in the source code. This is, admittedly, not the most elegant approach. But it works, and it means the entire blog output -- HTML structure, CSS styling, everything -- is generated from kov source with zero external files.
// template.kov — CSS generation via string writes
fn write_css(fd: u32) {
fs_write_str(fd, ":root{--l0:#080808;--l1:#0f0f0f;..." as u32);
fs_write_byte(fd, 10); // newline
fs_write_str(fd, "*{margin:0;padding:0;box-sizing:border-box}" as u32);
fs_write_byte(fd, 10);
fs_write_str(fd, "body{background:var(--l0);color:var(--l6);..." as u32);
fs_write_byte(fd, 10);
// ... ~60 more lines of CSS ...
fs_write_str(fd, "::selection{background:var(--l4)}" as u32);
fs_write_byte(fd, 10);
}
The design language is monochrome. Eight shades of grey, from #080808 (deepest background) to #e8e8e8 (bright emphasis). No colors. Depth comes from the difference between shades -- darker elements recede, lighter elements rise. The blog you're reading right now uses a more polished version of this same palette, with added depth effects like box shadows and border gradients.
The numbers
Total generation time for the blog: about 50 milliseconds. That's the kov compiler compiling the blog generator to RISC-V, plus the emulator executing the binary, plus all the VFS I/O. The emulator executes roughly 800,000 RISC-V instructions to process all the posts.
// Build + run pipeline:
//
// $ kov run blog/main.kov
// compile: ~20ms (kov → RISC-V ELF)
// emulate: ~30ms (~800K instructions)
// output: index.html + 42 post HTMLs
//
// The emulator runs at ~25 MIPS on the host.
// Real RISC-V hardware would be faster.
// But 50ms is already instant for a blog build.
800K instructions for 42 blog posts is about 19K instructions per post. Most of that is the byte-by-byte markdown parsing loop and the string-literal CSS writes. A smarter implementation would buffer writes and use block I/O. But at 50ms total, optimization isn't worth the complexity.
Why this exists
I could have used Hugo. I could have used Jekyll. I could have written a Python script. Instead, I wrote a programming language, wrote a static site generator in that language, compiled it to a CPU architecture I don't own, and ran it in an emulator I also wrote. That's objectively absurd.
But the blog generator is what proved kov was a real language. Not the test suite, not the examples, not the LED blink demo. The blog generator reads files, processes text, writes structured output, and produces something a browser can render. It exercises the full pipeline -- imports, string handling, file I/O, control flow, the VFS bridge. If the blog builds correctly, the language works.