Ryan Workman's personal blog at https://workman.tech, served by GitHub Pages
from the repo root. Posts are authored in markdown under _posts/<theme>/
and a small Node script (build.js) renders them to HTML alongside per-theme
landing pages and regenerates the homepage post list.
-
Create
_posts/<theme>/YYYY-MM-DD-slug.md. The folder name is the theme (e.g.engineering), and it becomes part of the URL. -
Add frontmatter at the top of the file:
--- title: rails perf wins from a 3-month audit date: 2026-06-01 description: short blurb used for the meta description and OG tags tags: [ruby, rails, performance] ---
titleanddateare required.descriptionis optional but recommended.tagsis optional and defaults to[]. -
Run
npm installonce, thennpm run buildwhenever you change a post. -
Review the diff (it will include both the source
.mdand the generated HTML) and commit everything. CI fails the PR if the committed HTML isn't a fresh build.
The published URL will be
https://workman.tech/<theme>/YYYY-MM-DD-slug.html.
_posts/<theme>/*.md— markdown sources. Theme = parent folder._posts/drafts/— local, gitignored staging area for work-in-progress drafts.build.jsskips it (seeEXCLUDED_THEME_DIRS), so a draft sitting here never renders, never lands on the homepage, and never generates a publicdrafts/directory. Move a draft into a real_posts/<theme>/folder to publish it._template.html— per-post template. Carries the full<head>(viewport, favicons, OG/Twitter meta, canonical link) plus the article body wrapper._theme_template.html— per-theme landing page template.build.js— single-file Node script. Walks_posts/, renders posts and theme landing pages, regenerates the post-list region insideindex.htmlbetween<!-- posts:start -->and<!-- posts:end -->markers. Every file it writes is run through Prettier first, so the committed HTML is deterministically formatted.<theme>/— generated output directories (committed; GH Pages needs them). Each generated HTML file starts with a<!-- generated by build.js -->marker sonpm run cleancan safely identify and remove them.index.html— hand-edited, but the region between the two marker comments is rewritten on every build.
Dependencies: markdown-it and gray-matter at runtime, plus dev tools
prettier (which build.js runs over every rendered file so the generated HTML
is consistently formatted) and html-validate (HTML linting, config in
.htmlvalidate.json).
npm run dev— local preview at http://localhost:3000 (wrapspython3 -m http.server 3000; needs Python 3 onPATH).npm run build— render everything (each file formatted with Prettier).npm run lint:html— runhtml-validateoverindex.htmland the generated theme HTML.npm run lint:fix— format source files with Prettier (prettier --write .; scope controlled by.prettierignore). html-validate has no autofix, so it is not part of this.npm run clean— remove generated theme directories (only directories whose HTML files all start with the generated marker are touched) and reset the homepage post-list region to the empty-state placeholder.
.github/workflows/build-check.yml runs on every pull request and on pushes to
main. It runs npm ci && npm run build and fails if the working tree changed
afterward (git status non-empty). That guards the one invariant that matters
here: the committed HTML must be a fresh render of the source. It catches stale
generated HTML, generated files that were never committed, and build.js errors
(bad frontmatter or filenames make the build exit non-zero). If it fails, run
npm run build locally and commit the result. The workflow then runs
npm run lint:html (html-validate) over the output.
- Code-block syntax highlighting (markdown-it emits
<pre><code class="language-foo">already; needs a stylesheet or a highlighter pass). - Per-tag landing pages.
- RSS feed.
sitemap.xml.- Per-post Open Graph images.
- Formal tests for
build.js(currently validated manually).