It's like a sourdough starter for websites. To use CakeWalk, make a copy of this project. Discard the git history. Develop your site according to the instructions below. Modify the included build system to suit your needs. Go wild, make a mess, this is your space to play. Replace all mentions of "CakeWalk" with the name of your site — including in this readme. In fact, delete this paragraph, and replace it with your own writing. But maybe keep the below, so people who want to help will know where to go.
Run npm install (or yarn, or pnpm… whatever you use, it'll probably work).
Henceforth, you'll use the site CLI to build the site. If site doesn't work, use ./site instead or — even better — add . to your $PATH.
- Run
siteto build the website and spin up a live-reloading dev server. - Run
site helpto see other available commands.
Here's the folder structure:
content— html and markdown files for every page, plus any related assets; see pagesfonts— uncompressed source files for fonts used on the site; see fontspublic— automatically generated when building the sitesystem— the build system source code; see systemtemplate— every type of page on the site, plus any related assets; see templates
The build system takes HTML and Markdown files in the content folder, parses and transforms them with rules defined in system, combines the result with various templates in template, and finally saves the fully-assembled page to public.
In short, the content folder should feel like an FTP server. You put stuff in there, and it shows up on the website. The file path becomes the URL. There's more, of course — macros and templates and other behaviour that we'll get to in a second. But all that fanciness is opt-in. If you stick to plain HTML and simple assets, /content acts just like FTP.
We're going to talk a lot about pages — so what's a page? It's an HTML or Markdown file that lives in the content folder, with some frontmatter explaining what the page is and how it should be processed.
If you make an HTML file /content/pens/index.html, the build script copies it over to /public/pens/index.html, and it becomes a web page with the URL /pens/index.html.
If you make a Markdown file /content/rams/index.md, the build system will convert it to HTML, copy it to public, and it'll end up with the URL /rams/index.html.
Many web servers have a nice feature: if a page is named index.html, you can omit that part of the URL. So the above pages can also be accessed at /pens and /rams. Those are clean URLs.
To make the most of these clean URLs, the build system has one trick: pages are renamed from name.html to /name/index.html, so the URL can be /name.
Thus, a page with the URL /eraser/head will be generated by exactly one of these files:
/content/eraser/head.md/content/eraser/head.html/content/eraser/head/index.md/content/eraser/head/index.html
Use whichever file naming you like. It's fine to mix and match. Though for pages with static assets, it's recommended to use one of the latter two forms so you have a natural place to put those assets. And if there's ever a conflict (where two files would have the same URL), you'll see a helpful error message.
At the top of every page is the frontmatter section — properties that control how the page is processed by the build system. The frontmatter section begins and ends with ---, and uses a key: value # comment syntax that looks like YAML but is much simpler.
Here's an example of what frontmatter could look like:
---
title: It's Okay To Forget
author: Ivy Boo
date: 2025-03-22
description: You can choose not to write it down. To treat each opportunity as an obligation. It's okay.
image: /blog/its-okay.png
template: blog
---Note that to make use of this example frontmatter, you'd need a template /template/blog.html, and you'd probably wanna find a place to plant {{author}}, natch.
All the fanciness of the build system is opt-in. Frontmatter is the primary way for a page to opt-in to special processing.
- An HTML file with no frontmatter, no processing is applied (including the aforementioned clean-URL renaming).
- All markdown files in
/contentget processing whether they have frontmatter or not.
The most crucial piece of frontmatter is template, which specifies what kind of page this is. The build system will inject the page's content into the matching HTML file from /template, and might even apply special processing for specific templates. See the Templates section for more.
Another important piece of frontmatter is publish. It's true by default, which means the page will be included when building the website. You can set it to draft, which means the page won't be included in production builds, but will be included in local dev builds. Or you can set it to false to completely exclude the page from all builds.
If a non-draft page links to a draft, you'll see a helpful warning.
For most CLI commands, you can specify --no-draft if you don't want drafts to be included. But, for site build, drafts are excluded by default, and you must use --draft to include them.
If a page is given a date, it'll be included in RSS feeds. The date must be in YYYY-MM-DD format.
titlewill be used for<title>and for RSS.subtitlewill often be combined withtitlefor stuff like RSS.descriptionwill be used for OG and RSS. Can include inline markdown.imagefor the rich preview whenever someon 8000 e shares a link to the page.stylesandscripts, which accept comma-separated lists of relative or absolute paths.clean: falseto skipname.html -> name/index.htmlrewriting.index: false— Page will not be included in RSS feeds or the sitemap.
The /template folder contains a handful of HTML files that give pages their structure and styling.
Pages use the template frontmatter to specify which of these template HTML files to use. If omitted, default is used.
All non-HTML files in the /template folder are treated as static assets and copied straight over to /public with no further processing.
Templates can have frontmatter, too, though this is a more advanced topic that depends on the inner workings of the build system.
Pages & templates can contain macros, which look like: {{ rain-dogs }}.
Macros mark locations that the build system should do something.
The most basic macro is {{ content }}, which marks where pages will be injected into templates.
If there's no special behaviour defined for a macro, the default behaviour is to copy a value from the frontmatter. For instance, if the page frontmatter includes band: Zs and the template has {{band}} are so damn good!, the final page will have the text Zs are so damn good!. Accurate.
If you see a macro and you aren't sure what it does, take a look at /system/macros.ts — that's where all the macro expansion logic lives.
Finally, there are a few special macros that use HTML syntax instead of the {{ }}-style syntax. They're designed to blend seamlessly into the surrounding HTML:
- Stuff inside an
<md>tag will be processed as Markdown and turned into a<p>. Handy if you want to build a custom page with HTML but use Markdown for bits of text, or use Markdown inside HTML inside Markdown inside…
There's a folder of frequently-used HTML snippets at /template/includes/. You can add files here, and then include them in your pages with the {{include:___}} macro.
In typical web development, you'd put a woff2 font file alongside your other assets, and then load it in your stylesheet. But fonts often contain a ton of wasted data — ligatures and alternates and glyphs for characters that are never used.
Instead, to add fonts to our website, you place ttf or otf files in the /fonts folder. The build system can then:
- Scan every page of the site, and collect every unique character of text.
- Create a subset of data in the
ttf/otffiles with just the characters we use. - Create optimized
woff2files of those subset fonts, and save them to/template/static/fonts/.
The build system will redo step 1 every time it runs. It will only redo steps 2 & 3 if it notices that the set of unique characters has changed.
So, when you add, remove, or change any of the fonts in the /fonts folder, you need to force the build system to redo steps 2 & 3. To do that, just delete the /template/static/fonts/chars.txt file.
This automatic font subsetting requires two non-NPM binary dependencies: harfbuzz and woff2.
- On Mac, you can
brew install harfbuzz woff2 - Not sure about Linux / Win — it is a goal to fully support these platforms, but that work hasn't been done yet.
- It's fine if these deps are missing. The build system will still run, but when the set of characters changes it will log a single warning and skip regenerating the fonts.
- Any characters that are missing will still be displayed on the site, they'll just appear in a fallback font.
- These deps can be added to a Github Actions flow, ensuring we always have the correct font subsets when the site is deployed.
In the Redirects.txt file, you'll find a list of redirect rules. One rule per line, space-separated.
/source /destination
/old/busted /new/hotness
Bare text is a comment 👋
The redirect source MUST start with a slash. The destination can be any URL.
/secret https://feelingisreality.com
You can redirect other kinds of files, too, not just HTML/Markdown pages.
We don't control the server, so we can't do a 301 or 302. Instead, the build system
will hardlink the file at the new path to the old path, so that both paths will work.
/old/path.pdf /new/path.pdf
- Add a redirect whenever you move or rename an existing page or asset (ie: PDF) that someone might have bookmarked.
- You can also use a redirect to create an alias so that multiple URLs will point to the same thing — but don't go overboard.
- These redirects are excluded from the Sitemap.
TODO: Need to think more about how these should work, and then document them properly.
One design detail that might be worth describing: there should always be a page in the content folder.
- We don't want the build system to auto-generate pages, because then you can't look at the URL of a page and just know where it comes from.
For now, we make a simplifying assumption: if there's a page that will appear in an index, like a lab note or newsletter, then the index page needs to be in either a sibling or ancestor folder of the page. (Ignoring the "/index.html" or "/index.md" part, if any.)
For example, this index is in a parent folder:
- Some Page: /content/path/to/one/page.md
- The Index: /content/path/to/index.md
Or this index, which is in a sibling folder:
- Some Page: /content/the/page.md
- The Index: /content/the/index.md
This would not work, because the index is not an ancestor or sibling of the page:
- Some Page: /content/here/page.md
- The Index: /content/there/index.md
This helps us automatically figure out which pages should belong in which indexes.
The build system will automatically check internal links for validity. If you try to link to a page that doesn't exist, you'll see a warning in the terminal.
The website includes the site CLI tool.
You can run site help to list all the available commands.
If
sitedoesn't work, use./siteinstead (or better yet, add.to your $PATH)
site is a little shell script at the root of the repo. It executes system/app.ts using the fastest available Node-compatible runtime (bun, deno, or node).
app.ts is the main entrypoint to the build system. If you want to learn how it works, start reading from there. It's all extensively commented, and a lot of care was taken to make it easy to understand.
If you need to build the website in a novel environment, it should be straightforward to get up and running. For instance, here's the build command on Netlify: bun system/app.ts build. For other options, see the source code of the site command.
The build system only uses basic Node APIs and a few super-simple dependencies:
- chokidar — used for local dev to watch folders for changes.
- glob — scans for files matching a pattern.
- markdown-it — a commonmark-compliant markdown renderer.
- typescript — just to use
npm exec tscto check for errors. - ws — used for local dev to more reliably trigger reloads in the browser.
So, really, all you need to run the build system is a markdown renderer. The rest can be omitted, or implemented from scratch in an afternoon.
Now, what about running the build system in the browser? That's totally do-able. All I/O goes through wrapper functions with simple expectations:
glob: take a pattern, return a bunch of "paths" (or some sort of identifier)read: take a "path", return text contentwrite: take some text and a path, no returnlink(semantically equivalent tocopy): take two paths, no return
You could shim or replace these functions so they don't have anything to do with node — for instance, you could read and write into an Automerge doc, and display content in a GUI. If you want to try this, start with system/io.ts, and just focus on the site build command. You should be up and running in no time.
Run tests with site test.
You can add tests to the system/test folder. We're assuming node:test is the runner, since that likely minimizes dependencies.