This is an Eleventy (11ty) static site. Nothing runs at request time — every page, the sitemap, and the RSS feeds are generated at build time and served as plain files. To add content, add a file and rebuild.
Every prayer is one markdown file in content/prayers/. Filename doesn't matter
for routing — the permalink in the frontmatter decides the URL.
Create content/prayers/my-new-prayer.md:
---
layout: layouts/prayer.njk
permalink: "///"
title: "My New Prayer"
slug: my-new-prayer
category: night-prayer
keyword: "the target search keyword this page is for"
excerpt: "One sentence describing the prayer — this becomes the meta description."
dateAdded: 2026-08-06
dateModified: 2026-08-06
---
The prayer text goes here, as plain paragraphs.
Amen.
Required fields:
| Field | Purpose |
|---|---|
title |
Page <h1> and <title> tag |
slug |
Used in the URL via permalink |
category |
Must match a slug in _data/categories.json — decides which hub page it appears on and which nav link it's under |
excerpt |
Meta description + the line shown in listing pages |
dateAdded |
Not currently displayed, kept for reference |
dateModified |
Drives <lastmod> in the sitemap and pubDate in the RSS feeds — update this whenever you edit a prayer's text |
Bump dateModified on every edit, even small wording fixes — that's the freshness
signal the sitemap and feeds are built around.
Categories live in _data/categories.json. Each one auto-generates a hub page at
/{slug}/ listing every prayer whose category field matches, and a link in the
site nav. To add one:
{
"slug": "saints-prayer",
"title": "Prayers to the Saints",
"rubric": "Ad Sanctos",
"icon": "church",
"description": "One paragraph describing the category, used as its page meta description."
}
icon is a Material Symbols Outlined ligature
name (e.g. wb_sunny, bedtime, shield, church). rubric is the short Latin
subtitle shown under the category name on the homepage.
No template changes needed — the hub page, nav link, and homepage card are all generated automatically for anything in this file.
scripts/prayer-image.js renders any prayer as a share-ready PNG in the same
illuminated-missal design the prayer pages use — cream ground, double gold frame,
fleuron divider, uppercase title, drop cap, rubric and domain in the footer.
npm run social -- night-prayer # 1600×900, for X
npm run social -- night-prayer --size=portrait # 1080×1350, for Instagram
npm run social -- --all --size=square # every prayer, 1080×1080
Output lands in social/ (gitignored). The argument is a prayer slug, or a
path to any markdown file with the standard frontmatter — so you can render a
draft before it's committed.
Both the input and the output are fully customizable:
# write anywhere — the folder is created if it doesn't exist
npm run social -- night-prayer --out="C:/Users/you/Desktop/posts"
# render a draft sitting outside the repo
npm run social -- "C:/drafts/prayer-for-healing.md" --out="C:/drafts/img"
# point --all (and slug lookup) at a different source folder
npm run social -- --all --prayers="C:/drafts" --out="C:/drafts/img"
--out — absolute, or relative to where you ran the command. Nested paths
are created recursively. Defaults to social/ in the repo.--prayers — the folder --all scans and bare slugs resolve against.
Defaults to content/prayers/.Paths with spaces need quoting. Windows backslashes work as-is.
| Size | Dimensions | Use |
|---|---|---|
x (default) |
1600×900 | X / Twitter in-stream, LinkedIn |
og |
1200×630 | Open Graph link previews |
square |
1080×1080 | Instagram feed |
portrait |
1080×1350 | Instagram portrait |
story |
1080×1920 | Stories, Reels covers |
The body font auto-shrinks to fit on one image. A prayer too long even at the
smallest size is split across numbered slides (-1.png, -2.png) with a
CONTINUED mark and an n / total counter — ready to post as a carousel.
Flags: --out=dir, --prayers=dir, --pad=N, --font-scale=N, --keep-svg,
--svg-only, --no-excerpt, --no-drop-cap, --max-slides=N, --fonts=dir.
The proportions are tuned for phone viewing — these get read on a five-inch
screen in a scrolling feed, so the margins are tight and the body text sets
larger than a print layout would. Everything scales off the image's short edge,
so it holds at every preset size. The knobs live in the LAYOUT object at the
top of the script:
| Key | Default | Effect |
|---|---|---|
pad |
0.05 |
Content inset from the edge |
frameInset |
0.022 |
Where the gold frame sits |
bodyMax |
0.043 |
Body font ceiling |
bodyMinRatio |
0.62 |
How far body text may shrink before paginating |
measure |
38 |
Max line length in ems |
bodyFill |
0.96 |
Body width as a fraction of the content width |
lineHeight |
1.55 |
For a one-off tweak without touching the file, use --pad=0.03 (tighter) or
--font-scale=1.15 (bigger text). Note that shrinking is what keeps a prayer on
a single image — pushing --font-scale too high forces a split.
Text is laid out with a serif advance-width table rather than real font metrics,
so line breaks are approximate — check the output before posting a title with
unusual characters. PNG rasterization uses @resvg/resvg-js; if it isn't
installed the script writes SVG instead. Rendering uses whatever serif the system
provides (Georgia locally). To match the site's EB Garamond and Source Serif 4
exactly, drop the .ttf files in a folder and pass --fonts=that-folder.
These content shapes don't have templates yet — they'd need their own layout, not just a new category:
Everything else — a prayer, to any saint, for any occasion, in any of the existing categories — is buildable right now with just a markdown file.
npm install # first time only
npm run build # outputs static site to _site/
npm run serve # build + serve locally with live reload
Push to main. Coolify watches the repo and rebuilds the Docker image
(Dockerfile → static build → Caddy serving _site/ on port 80) automatically.
There's no manual deploy step beyond git push.
SITE_URL (set in Coolify's environment variables, currently https://prayerhub.space)
controls every canonical URL, the sitemap, and the RSS feeds — don't hardcode
the domain anywhere in content.