Serein powers A Poem Per Day, a static site that publishes one poem per day from Markdown source files.
If you would like to share a poem to be published on the website, feel free to get in touch.
poems/holds the source poems.templates/holds the HTML templates.assets/holds styles, browser scripts, fonts, and branding assets.scripts/holds the build and maintenance scripts..github/workflows/holds CI and deployment automation.
The site is generated by scripts/build.mjs.
That pipeline is responsible for:
- loading and validating poem files
- enforcing filename and directory conventions
- rendering the homepage, archive, about page, poets index, poet pages, and dated poem pages
- bundling client-side assets
- generating social cards and editorial reports
- generating RSS and sitemap files when
SITE_URLis present
The project targets Node.js 24.x LTS and uses npm scripts as the execution layer for build, watch, preview, normalization, and editorial checks. .nvmrc and .node-version are checked in so local Node version managers can select the expected runtime.
In an interactive terminal, serein editorial opens a keyboard-first report viewer. When output is redirected or piped, it falls back to the plain-text report. The editorial report also flags same-poet proximity issues, and the TUI can apply the suggested fix with f on an actionable proximity item.
Each poem is stored as a Markdown file with frontmatter.
---
title: The Title
poet: The Poet
translator:
publication:
source:
date: 2026-03-10
---
First line of the poem.
Second line of the poem.Required fields:
titlepoetdate- poem body
Optional fields:
translatorpublicationsource
Poem files follow the path pattern poems/YYYY/MM-Month/YYYY-MM-DD-slug.md. Dates are unique across the collection. Missing optional metadata is still surfaced by the editorial checks.
Before running npm run poems:sync (alias: npm run normalize:poems), the date frontmatter may also use symbolic values:
nextpicks the closest unused date on or after the current publication date while respecting the poet cooldown rule described below. Multiplenextvalues in one normalize run are resolved sequentially in alphabetical path order.random-<month>such asrandom-maypicks an unused day in that month of the current publication year.
The normalizer rewrites those symbolic values to concrete YYYY-MM-DD dates, removes empty optional frontmatter fields, rewrites frontmatter in the canonical order (title, poet, translator, publication, source, date), and then renames the poem file/path to match.
Serein also enforces a poet cooldown of 30 days for newly assigned next dates and reports any existing same-poet pairs that are scheduled too close together. If serein poems finds an actionable issue, it prints a suggested command such as serein poems fix-proximity 2026/04-April/2026-04-12-example.md, which moves the selected poem and any later poems by the same poet far enough forward to clear the cooldown.
The poem renderer also supports one small piece of custom markup:
-
::linebuilds one visual line from aligned segments.|<...|places text on the left,|^...|centers it,|>...|places it on the right, and|~...|inserts space.::line |<left phrase| |~3ch| |>right phrase|
Each poem is published at /YYYY/MM/DD/.
Once a poem is published, its raw Markdown source is also available at /YYYY/MM/DD.md.
The homepage, archive, poets index, and poet pages resolve against the viewer's local date, capped to the site's one-day public share horizon. Dated poem pages at /YYYY/MM/DD/ become shareable as soon as they enter that horizon and only remain blocked for poems beyond it.
The build system also supports date-controlled previews through --as-of and SEREIN_AS_OF. Runtime date overrides are gated behind SEREIN_ENABLE_RUNTIME_AS_OF=1.
GitHub Actions validates the site on pushes and pull requests. A separate scheduled workflow triggers the Cloudflare Pages deploy hook at the publication rollover.
Production environments provide SITE_URL so canonical URLs, RSS, and the sitemap can be emitted with the correct domain.
Released under the MIT License.