- Python 79.4%
- CSS 13%
- HTML 7.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| assets | ||
| content | ||
| templates | ||
| tools | ||
| .gitignore | ||
| build_site.py | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
| test_build_site.py | ||
crowprose
Source for writing.crowmaki.me. build_site.py renders
content/*.md through the Jinja templates in templates/ into public/, and the Forgejo
workflow force-pushes that output to the site branch.
Adding an essay
Drop a Markdown file in content/. The filename becomes the URL, lowercased with runs of
non-alphanumeric characters collapsed to hyphens, so content/The Long Winter.md publishes at
/the-long-winter/. Frontmatter never sets the slug. Rename the file to change the URL.
---
title: Meaning
date: 2026-08-22
description: A man wakes in a white room and is told what it was all for.
image: /assets/covers/meaning.png
---
The ice was black, which is to say I didn't see it.
Frontmatter reference
| Key | Required | Type | Used for |
|---|---|---|---|
title |
yes | string | Page heading, <title>, and og:title |
date |
yes | ISO date | Index ordering (newest first) and article:published_time |
description |
no | string | meta description and og:description |
image |
no | path or URL | og:image |
Anything else you put in the frontmatter is ignored, and the build says so on stderr without stopping:
warning: content/meaning.md: ignoring unknown frontmatter key 'descriptoin', did you mean 'description'?
Watch for those. A misspelled key does nothing otherwise, and the page publishes looking fine.
Write date unquoted as 2026-08-22. A quoted "2026-08-22" works too. Anything the build
can't read as an ISO date fails.
description
Leave it out and the build takes the opening paragraph, strips the markup, and cuts it at the last word boundary within 200 characters. That works when the first line is a hook. It reads badly when the essay opens on a fragment or a line of dialogue, so write your own in those cases. Aim for under 200 characters; most clients truncate somewhere near there anyway.
An empty description: "" fails the build rather than falling back. Delete the key instead.
image
Every page defaults to the site card at /assets/og-card.png. Set image to override it for
one essay.
The value must start with / or be a full https:// URL. A bare assets/cover.png fails the
build, because scrapers can't resolve a relative path. Everything under assets/ is copied
verbatim to public/assets/, so a file at assets/covers/meaning.png is reachable at
/assets/covers/meaning.png.
The build doesn't open the file. When you override the image it stops emitting
og:image:type, og:image:width, and og:image:height, since it has nothing true to say
about them, and og:image:alt becomes the essay title. Use 1200x630 to match what the default
card advertises.
Writing
--- on its own line after the frontmatter is a scene break. The parser deliberately consumes
only the first pair of fences so the rest reach the renderer, where they become an <hr> that
the stylesheet paints as a centred asterism. Don't add a second --- immediately below the
frontmatter to close it off.
Markdown is CommonMark with the typographer on. Straight quotes become curly, ... becomes an
ellipsis, -- becomes an en dash, and --- inside a line becomes an em dash. Only a ---
alone on its own line is a scene break. Typing the real characters works as well.
Building
python -m venv .venv
.venv\Scripts\activate # Windows
. .venv/bin/activate # macOS, Linux
pip install -r requirements-dev.txt
python build_site.py
python -m http.server 8000 --directory public
pytest
requirements.txt holds the three packages the build itself needs. requirements-dev.txt adds
pytest and the three the card generator needs. CI installs requirements.txt plus pytest.
public/ is gitignored and wiped on every build. Never edit anything in it.
When the build fails
It exits 1 and prints one line naming the file. Only these stop it, and an unknown frontmatter key isn't one of them. It refuses to publish when:
- the frontmatter block is missing, unterminated, or isn't a YAML mapping
titleordateis missing, ordateisn't an ISO datedescriptionorimageis present but empty or not a stringimageis neither root-relative nor a full URL- an essay has no
descriptionand no opening paragraph to derive one from - two files produce the same slug
assets/og-card.pngis missingcontent/has no.mdfiles at all
The feeds
Every build writes two feeds of all essays, newest first, and every page links both for autodiscovery so a reader given the site URL finds them on its own:
| Path | Format |
|---|---|
/feed.xml |
RSS 2.0, full text in content:encoded |
/atom.xml |
Atom 1.0, full text in <content type="html"> |
They carry the same essays and the same full text; each one advertises the other as an
alternate, so a reader that prefers one format finds it from either. Items also carry the same
description the social card uses, as the RSS description and the Atom summary.
These are the paths readers guess at and the ones existing subscriptions point at, so changing
RSS_PATH or ATOM_PATH orphans them. The Atom feed's <id> is the site URL, and a reader
that sees an <id> change treats the feed as a brand new one and re-notifies every entry.
Links inside an essay are rewritten to absolute URLs on the way into both feeds, since a reader
renders an entry on its own origin where /assets/plate.png would 404. The site's own pages
keep their root-relative links.
Each feed carries the newest FEED_MAX_ITEMS essays (10). Older work stays on the index but
drops out of the feeds, so a new subscriber never sees it. The build says which builds are
capped:
feeds carry the newest 10 of 25 essays (FEED_MAX_ITEMS)
Because every item carries the essay's full text, the feeds grow with the writing. Over 150 KB the build warns:
warning: feed.xml is 164 KB, over the 150 KB mark; consider lowering FEED_MAX_ITEMS (now 10)
That's a nudge, not an error — the build still publishes. Readers throttle or truncate large
feeds at thresholds none of them document, so the fix is to lower FEED_MAX_ITEMS.
Every page also carries a visible "Subscribe: RSS · Atom" line in its footer, for readers who don't have autodiscovery.
Atom requires a feed-level author. SITE_AUTHOR is the byline it publishes, verbatim.
lastBuildDate (RSS) and updated (Atom) are the newest essay's date rather than the build
clock. That keeps an unchanged content/ rebuilding to byte-identical feeds, so the deploy
force-push doesn't churn and readers don't see a feed as freshly changed on every push.
Dates have no time of day, so items publish at midnight UTC. Two essays sharing a date appear
in the same order the index gives them. Nothing records a revision date, so an Atom entry's
updated tracks its publication date and editing an essay never re-surfaces it in a reader.
The social card
assets/og-card.png is the 1200x630 image that shows up when someone pastes a link into
Discord, Slack, Bluesky, or iMessage. It's committed to the repo. Regenerate it with:
python tools/make_og_card.py
That script reads the site's own woff2 fonts and palette, so the card tracks the stylesheet.
Its tagline lives in TAGLINE there, separate from SITE_DESCRIPTION in build_site.py.
Change one and you probably want to change the other.
Scrapers cache aggressively. Facebook and X will hold a bad first scrape for hours, so check the card before sharing a new link widely.
Site settings
All of them are constants at the top of build_site.py:
| Constant | Controls |
|---|---|
SITE_TITLE |
Masthead, <title> suffix, og:site_name |
SITE_URL |
Origin every absolute URL is built from. No trailing slash |
SITE_DESCRIPTION |
The home page's description only |
OG_IMAGE_PATH |
Default card, root-relative |
OG_IMAGE_ALT |
Alt text for the default card |
OG_IMAGE_SIZE |
Advertised dimensions. Keep it matching tools/make_og_card.py |
SUMMARY_MAX_CHARS |
Cutoff for derived descriptions |
RSS_PATH |
Where the RSS feed is written and linked from. Don't change it |
ATOM_PATH |
Where the Atom feed is written and linked from. Don't change it |
SITE_AUTHOR |
The byline in the Atom feed |
FEED_MAX_ITEMS |
How many essays each feed carries |
FEED_SIZE_WARN_BYTES |
Feed size that earns a warning |
Deploying
Push to main. .forgejo/workflows/build.yml runs the tests, builds the site, and force-pushes
public/ to the site branch. There is no staging step and no way to preview the deployed
result short of pushing, so run the build locally first.