Brick Heart: Build Scope
Project
Brick Heart ("A passion for building") is an accessibility-focused site and, later, a web app.
Repo: ~/brickheart-web on Ubuntu with VS Code, deployed from GitHub to Cloudflare Workers.
site/→ www.brickheart.org (live, multilingual; explains how to use the app, gallery, etc.)dev/→ dev.brickheart.org (blog, changelog, roadmap; English only)app/→ app.brickheart.org (WebGL editor; not started)
Carried over from the existing site
- Full BCP 47 tags in
langand hreflang (en-GB, zh-Hans); folders use the first part (/en/,/zh/). - Language labels are written in their own language;
argetsdir="rtl". - CSS uses logical properties and has visible focus rings, 44px targets, dark mode, reduced motion, forced-colors support, a skip link and landmarks.
- Picker: first-time visitors see it, the choice is saved in localStorage, and "Change language" goes to
/?change. - Strict CSP: no inline
<style>,<script>,style=""oronclick="". - One 404 per language plus a neutral root one.
- hreflang lives in the generated sitemap.
languages.jsonis the single config of live languages.
Folder layout
shared/
assets/ copied wholesale into every dist (css, js, brand, favicons, manifest, _headers)
engine/ build code and templates; never copied to dist
site/ content/, languages.json, build.mjs, package.json, wrangler.jsonc
dev/ content/, build.mjs, package.json, wrangler.jsonc
dist/is generated and gitignored, and eachwrangler.jsoncpoints at./dist.- Nothing hand-written lives in
dist/. - Anything that differs per project is generated:
robots.txt(sitemap line),sitemap.xml, and the 404 pages. - Links are relative. There is no leading
/: use../en/...from subfolders and no prefix for root files. This keeps localfile://testing working. Only canonical URLs, the sitemap and hreflang are absolute. npm run buildis the only command needed.
Content rules (site and dev)
- Content is
.mdwith no front matter. - The first
# headingis the page title and the<h1>. The build fails if a file has no h1 or more than one. - Headings are h1 to h4 only (warn on h5/h6). CSS scopes them by context (
article h2,section h3, ...), so there are no heading classes. - Semantic HTML:
<header>,<nav>,<main>,<section aria-labelledby>(each h2 and its content),<article>,<footer>. - Code blocks and raw HTML in Markdown are never turned into live HTML. Raw HTML is disabled.
Site
content/about.md→dist/en/about.html. Subfolders carry over (content/legal/privacy.md→dist/en/legal/privacy.html).- Translations mirror it:
dist/zh/about.html. Page names stay English in every language. - Each language has exactly one
index.html, built fromcontent/index.md. - The site has no dates, tags, archive, tag pages or feeds. A page that needs a date has it written in the
.md. - Build fails on: missing h1, multiple h1s, or duplicate h1 between pages.
- File names are not date-based on the site.
Dev
Collections are folders under dev/content/:
blog/ announcements/ accessibility/ engineering/ design/ community/ data_analysis/ assistance/<tool>/
changelog/ app/ dev/ site/
roadmap/ now/ next/ later/
- File names:
yyyyMMdd[a-z].mdfor blog and changelog. The date comes from the name, and the trailing letter only breaks ties on the same day. The letter never appears in the HTML name, displayed date, or title. - HTML name = slug of the h1. For example,
# Hello from devbecomesblog/hello-from-dev.html. The h1 is also the display title. - Duplicate slug in a collection fails the build (two files would collide).
- Build fails if a file name isn't
yyyyMMdd[a-z]or the date is invalid (blog and changelog only). - Roadmap items are short
.mdfiles. Their folder (now,next,later) sets their status, androadmap.htmlshows them grouped. Moving an item means moving the file. There is no "Done" group: a shipped item is deleted from the roadmap and gets a changelog entry. - The changelog describes what changed for visitors in plain sentences, and is not generated from git.
- Assistance is
blog/assistance/{chatgpt,claude,copilot,deepseek}/. The assistant writes each entry at the end of a session, and a human edits and approves it. Entries are curated outcomes, with no back and forth or dead ends. The h1 is the purpose, followed by:- Asked for: …
- Produced: …
- Human changes: …
- Checked by: …
- There are no AI labels on posts. Assistance entries are ordinary entries with ordinary tags and get no special handling.
Tags
- Every folder in an entry's path is a tag. A multi-tag article lives in a subfolder path.
- The same folder name anywhere in the tree is the same tag.
- Tag pages are
tags/<name>.html, with later pages astags/<name>-2.html. - A tag page shows its intro text first, then all matching entries newest first, with each entry listed once.
- An intro file is
<name>.mdinside a folder named<name>. It is not itself an entry. Two intro files for one tag fail the build. - Every tag gets a page. The tag navigation (to the right of the page) lists only tags with an intro file or at least N entries (default 3, one config value).
- Orphan posts are accepted: an old post with no related posts is reachable via the archive only.
Lists (same rule everywhere)
- Index (
blog/index.html,changelog/index.html): latest 10 entries, with date, title, tags and the first paragraph as an excerpt. archive.html: every entry, showing date, title and tags only.- Tag pages: same format as the index, with accessible pagination (
<nav aria-label="Pagination">,aria-current="page", real previous and next links). - No feeds anywhere. There is no RSS on dev or the site.
Translation (site only; dev is English only)
Translation is a local step, and Cloudflare only runs templating.
- Collect:
npm run buildsplits each page into blocks (heading, paragraph, list item) and writescontent.en.json. Each block has a key (page and position), the English text and a hash. Code blocks and URLs are excluded. - UI category: a separate
uicategory in the same JSON, not from.md. It holds nav labels (About, Privacy, ...), "Skip to content", "Change language", and "This page was machine translated from English into {language}". - Todo: the build diffs against
translations.jsonand writestranslations/todo.jsonfor new or changed blocks only. It prints a notice when there is work to do.
{ "languages": ["zh", "ar"], "blocks": { "about/3": { "hash": "...", "en": "..." } } }
The language list is taken from languages.json (excluding en), so up to ~10 languages come from one config.
4. Translate: you translate each language on the same blocks and save the result as translations/inbox.json.
5. Merge: the next build merges the inbox into translations.json, which is a single file holding all languages and pages and storing the English hash per block. A block whose hash no longer matches the current English is rejected, not merged.
6. Template: pages with complete, current translations are built per language. Anything else falls back to English with the correct lang attribute and stays out of the hreflang sitemap. A reviewed flag per page controls the machine-translation notice. Locally the build warns about stale translations, and on Cloudflare it never fails because of them.
7. If an AI tool does the translating, it is logged under assistance like any other use.
Open items to settle
- Exact slug rules (lowercase, hyphens, punctuation and non-ASCII handling).
- Roadmap item file names (they aren't dated, so free-form is assumed).
- Whether
picker.jsand other site-only assets are split out ofshared/assets/for dev.
Suggested build order
- Reorganise into
shared/assetsandshared/engine, and remove duplicates. - Write the shared engine: Markdown to semantic HTML, relative links, failure checks.
- Build
dev/: collections, tags, index, archive, pagination, roadmap, changelog, sitemap, 404. - Build
site/step 1: pages fromcontent/in English. - Build
site/step 2: collect, todo, inbox merge, per-language templating, hreflang sitemap.