This is how codesthings.com is built, from data files to deployed website. It is a small Node program that reads JSON and Markdown, fills in EJS templates, and writes plain HTML into a folder.
The shape of it
Link to section: The shape of itThe pattern is data in, pipeline, files out, borrowed from my campsnap-filters project. Nothing runs on a server and no page content is rendered in the browser. The pages are finished HTML before anyone asks for them.
All content lives in data/:
site.json,now.json,uses.jsonandlinks.jsonhold the homepage copy, the Now page, the Uses page and the link list.projects/has one JSON file per project, with alaneofworkormake.photos/has one JSON file per photo.blog/has one Markdown file per post. The file name gives the slug and the date.
To change the site, I edit one of those files. A project or photo file with a missing key or a bad lane fails the build and names the file, rather than rendering a broken card.
The build
Link to section: The buildsrc/index.js is orchestration only. It loads the data, builds the assets, and renders every page through a single renderPage function. The work happens in small modules under src/lib/:
data.jsreads and validates everything underdata/.validate.jschecks project and photo files.frontmatter.jssplits a post into its front matter and body.markdown.jsrenders posts with markdown-it, adds heading anchors, callouts and copy buttons, and highlights code with Shiki at build time.reading.jsworks out the reading time.images.jsis the image pipeline.css.jsbundles the stylesheets with Lightning CSS.buildstamp.jsreads the git commit for the version in the footer.
Templates are EJS files in src/templates/, and every page is built from the same header, footer and head partials.
Images
Link to section: ImagesEvery image a post, project or photo refers to goes through sharp. It writes AVIF and WebP at 480, 960 and 1600 pixels wide, wraps them in a <picture> with the real width and height, and lazy-loads everything except the first image. Results are cached in .cache/images/, keyed by a hash of the source, so a rebuild only does new work. Only images that something refers to are shipped. If a post points at an image that doesn't exist, the build fails and names the post and the path.
Styling and scripts
Link to section: Styling and scriptsThe stylesheet is plain CSS in ordered files: tokens, base, layout, components, pages and print. The build joins and minifies them into one file. Dark is the default and light is a switch on <html>, set by a tiny inline script before first paint. The JavaScript is plain ES modules with no framework and no bundler, and the post-only scripts (the contents list, the copy buttons and the video facade) only load on posts.
Tests
Link to section: TestsThe pure parts have unit tests that run with node --test: front matter, validation, the build stamp, the image pipeline, the Markdown renderer, reading time and the theme and floating-button scripts. npm test runs them all.
The CV is a separate site
Link to section: The CV is a separate siteMy CV lives in its own repository, built the same way: JSON in data/ (experience, skills, education, projects and the cover letter), EJS templates, and a small Node build. It has two runtime dependencies, ejs and debug. It builds an HTML page and also a set of PDFs, which it makes by sending the page to a Gotenberg container that turns HTML into PDF. This site links to it at /cv/, and both repositories publish to the same domain.
I split them because the CV changes for different reasons. It gets edited when I change jobs or apply for one, and this site gets edited when I write or make something.
Deploying
Link to section: DeployingA push to main runs .github/workflows/deploy.yml. It runs make build, which runs the same Node build inside a node:24-alpine container, and publishes the public/ folder to the pages branch. GitHub Pages serves that branch at codesthings.com. There is no server to look after.
Why no framework?
Link to section: Why no framework?The site is a handful of pages that change when I write something. A framework would add a build step, a dependency tree and a runtime for no visible gain, and this version is easier to read in an afternoon than to configure.