Render an HTML template to an image or a video, and publish it to fourteen social platforms with one command.
cd my-project && crierProvide one layout, one data file, and one config. Instagram gets a story, Discord gets a card, and everyone gets the caption written for them.
crier finds the configuration by walking up from where you are. It works the way git finds a repository. Publishing is what it does with no arguments. So the everyday flow is: change directory, run crier.
- Fourteen platforms. Instagram, Facebook, TikTok, Telegram, X, Mastodon, Discord, LinkedIn, Reddit, Slack, VK, Threads, YouTube, and Boosty. Publish images, video, and animated GIFs. You can also use any shell script you like as a platform of your own.
- HTML and CSS you already know. Use gradients, web fonts, SVG, and blend modes. They are laid out by a pure-Go engine and painted by a rasterizer written for it. There is no headless browser.
- One layout, many shapes. Use template overlays and per-platform sizes. One card becomes a story and a banner without a second template.
- Configuration that composes. Set every value in a file, an environment variable, or a flag. The file is found by walking up from where you are, the way git finds a repository.
- A track with the post. Attach an audio file you have the rights to at Discord, Slack and Telegram, or let TikTok pick one. Open an Instagram carousel or a Telegram album with a video, which is the only way a soundtrack reaches Instagram. No API anywhere names a licensed track, and the page says so rather than leaving you to search.
- A single static binary. Built with
CGO_ENABLED=0for six platforms. There is nothing to install alongside it.
macOS and Linux:
curl -fsSL https://raw.githubusercontent.com/yohimik/crier/main/install.sh | shWindows:
irm https://raw.githubusercontent.com/yohimik/crier/main/install.ps1 | iexThe script resolves the latest stable release. It verifies the download against the sha256 digest GitHub publishes for it. It installs to /usr/local/bin when that is writable, or ~/.local/bin otherwise. Pin a version with CRIER_VERSION. Choose the directory with CRIER_BIN_DIR.
dispat install yohimik/crierThe crier releases name their binaries the way the dispat installer expects. They use the repository's name and the platform. This means the bare command works on its own. You need dispat 1.7 or newer. An older dispat requires --asset 'crier-{os}-{arch}'.
The bare command resolves the newest stable release. Release candidates are prereleases, and dispat install skips those unless you add --prerelease.
- uses: yohimik/crier@v1
- run: crierThe action installs crier and puts it on PATH. It takes version (default: the latest stable), bin-dir and github-token. It reports version and path.
@v1 is a moving tag scoped to the stable line. It follows the newest 1.x stable release, and a release candidate never drags it forward: @v1 asked for stable and gets stable. Pin the full tag, @v1.0.0, to stay put.
More: installing.
go install github.com/yohimik/crier/cmd/crier@latestThis builds from source and needs Go 1.26 or newer. The binary reports the module version rather than the one stamped at release time. Released binaries carry the version, the commit and the build date in their ldflags. A go install build reads what it can from the module's own build info instead. Everything else is identical.
@latest follows Go's prerelease rule and resolves the newest stable release, skipping release candidates.
crier self-update # verified against GitHub's digest, and reversible
crier self-update --rollback # within a week of an updateMore ways, and what each does: installing.
mkdir promo && cd promo
crier init # writes a commented crier.yaml to editThe crier init command names a template and a data file. You need to write them.
template.html:
<!doctype html>
<html><head><style>
html, body { height: 100%; margin: 0 }
body { font-family: "Go", sans-serif }
.card {
width: 100%; height: 100%; box-sizing: border-box; padding: 96px;
background: linear-gradient(160deg, #12203a, #4a2f6f); color: #fff;
}
h1 { font-size: 96px; margin: 0 }
p { font-size: 40px; opacity: .8 }
</style></head>
<body><div class="card">
<h1>{{ .title }}</h1>
<p>{{ .subtitle }}</p>
</div></body></html>data.yaml:
title: crier ships v1
subtitle: One template, fourteen platforms, one command.Next, fill in the crier.yaml file. The init command already created it.
render:
template: template.html
data: data.yaml
width: 1080
height: 1080
output: card.png
hermetic-fonts: true
publish:
caption: "{{ .title }} — {{ .subtitle }}"
telegram:
enabled: true
chat-id: "@my_channel"Then run this command:
export CRIER_PUBLISH_TELEGRAM_TOKEN=…
crier render # 1. see the picture: card.png, no network
crier ping # 2. are the credentials right? nothing is posted
crier --dry-run # 3. what would be sent, still no network
crier # 4. post itYou can use the same template without a data file. Point render.data at a prefix. Every variable with that prefix becomes a value.
# crier.yaml
render:
template: template.html
data: env:CARD_
width: 1080
height: 1080
output: card.pngCARD_TITLE="crier ships v1" \
CARD_SUBTITLE="One template, fourteen platforms, one command." \
crier renderFor example, CARD_TITLE becomes {{ .title }} and CARD_MAIN_TITLE becomes {{ .main_title }}. The tool strips the prefix, converts the rest to lower-case, and keeps the underscores. These values are plain strings exactly as you write them. If your data has structure, you still need a file or the --render-data - flag. Read the data document for more details.
You can find every option and its default value in crier.example.yaml. This file shows all options at a glance. You can also run crier init --full to write this exact configuration into your own directory.
Each example is a complete project. Run the command to get the image. All previews below were rendered by crier itself.
| Example | Demonstrates | |
|---|---|---|
![]() |
business-promo · 1080×1080template.html · crier.yamlcrier render --config examples/business-promo/crier.yaml |
Bundled font (Poppins), linear gradient, line-clamp overflow, caption templating |
![]() |
video-game-release · 1920×1080template.html · story-overlay.html · crier.yamlcrier render --config examples/video-game-release/crier.yaml |
Per-platform overlays: an Instagram story from the same layout. Pixel display font, repeating gradient |
![]() |
social-quote · 1200×675template-serif.html · template-panel.html · crier.yamlcrier render --config examples/social-quote/crier.yaml |
Template pool and seeded randomisation, serif face, radial gradient, alt text |
![]() |
release-changelog · 1080×1080template.html · announce.sh · crier.yamlcrier render --config examples/release-changelog/crier.yaml |
A release posting about itself: dispat release variables piped in on stdin. Monospace face, scanline pattern |
![]() |
event-invite · 1080×1920template.html · crier.yamlcrier render --config examples/event-invite/crier.yaml |
Story format, rounded face, duotone gradient |
![]() |
custom-platform · 1080×1080template.html · publish.sh · crier.yamlcrier render --config examples/custom-platform/crier.yaml |
A shell script as a platform: curl to a webhook. Geometric sans over mono, layered radial background |
![]() |
square-1080 · 1080×1080template.html · crier.yamlcrier render --config examples/square-1080/crier.yaml |
The smallest useful project: no bundled fonts, no overlays |
![]() |
story-1080x1920 · 1080×1920template.html · crier.yamlcrier render --config examples/story-1080x1920/crier.yaml |
A story starter with {{ block }} sections ready for overlays |
The examples bundle OFL-licensed fonts. These live in examples/fonts/. Each font includes its licence.
crier |
render and post to every enabled platform; publishing is the default |
crier render |
render the template and write the file |
crier ping |
check every enabled platform's credentials, without posting |
crier platforms |
which platforms are enabled, and which are configured |
crier config |
the resolved configuration, secrets redacted |
crier init |
write a configuration file to start from (--full for every option) |
crier self-update |
replace this binary with the newest release (--rollback undoes it) |
crier --version |
the version, the commit and the build date |
The crier publish command is the default. You can leave it out. For example, crier --dry-run and every other flag work without it. You will find the full list and the dispatch rule in the command line.
Results go to standard output. Logs go to standard error. The exit code tells you what happened.
The index links everything. These are the pages people reach for first:
- Configuration: how the three layers compose, how the file is found, and every key.
- Writing templates, overlays, captions, text overflow, and fonts.
- CSS support: what the engine implements.
- Pagination and carousels: long content across several pages, and the posts they become.
- Publishing: includes a page per platform.
- Staging: the public URL Instagram insists on.
You do not need anything for images. You need ffmpeg for video, and ngrok or zrok for a tunnel. You only need them when you use those features.
The changelog is in cmd/crier/CHANGELOG.md. dispat writes it from the conventional commits. See releasing.
Read the Development guide. It covers building the project. It also explains the two test suites, the golden images, and the commit convention.
This project uses the MIT licence. See the LICENSE file for details.







