No description
  • Python 79.4%
  • CSS 13%
  • HTML 7.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Alic Szecsei 59b35440f2
All checks were successful
/ build-and-deploy (push) Successful in 9s
add rss and atom feeds
2026-08-25 02:09:27 -07:00
.forgejo/workflows test: cover frontmatter parsing and description derivation 2026-08-23 16:42:02 -07:00
assets add rss and atom feeds 2026-08-25 02:09:27 -07:00
content fix: edit meaning 2026-08-23 17:20:47 -07:00
templates add rss and atom feeds 2026-08-25 02:09:27 -07:00
tools feat: add Open Graph metadata to rendered pages 2026-08-23 16:41:52 -07:00
.gitignore initial commit 2026-08-22 19:02:14 -07:00
build_site.py add rss and atom feeds 2026-08-25 02:09:27 -07:00
README.md add rss and atom feeds 2026-08-25 02:09:27 -07:00
requirements-dev.txt feat: add Open Graph metadata to rendered pages 2026-08-23 16:41:52 -07:00
requirements.txt initial commit 2026-08-22 19:02:14 -07:00
test_build_site.py add rss and atom feeds 2026-08-25 02:09:27 -07:00

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
  • title or date is missing, or date isn't an ISO date
  • description or image is present but empty or not a string
  • image is neither root-relative nor a full URL
  • an essay has no description and no opening paragraph to derive one from
  • two files produce the same slug
  • assets/og-card.png is missing
  • content/ has no .md files 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.