@dispat/crier
v1.1.0
Published
Render HTML and changelogs to images, carousels, or video; publish to Instagram, Facebook, TikTok, Discord, and 10 more platforms. Native Crier CLI.
Maintainers
Keywords
Readme
Turn HTML templates and release notes into images, carousels, and videos. Publish them to fourteen social platforms from one command.
Questions, ideas, or release automation help? Join the Dispat Discord community.
@dispat/crier installs the native Crier Go CLI through npm and exposes the crier command. Use it for Instagram and Facebook posts, release announcements, LinkedIn carousels, Discord updates, Telegram albums, story cards, and music-backed videos. Images render in a pure-Go HTML/CSS engine: no Go installation, Chromium, or browser service is required.
npm install --global @dispat/crier --allow-scripts=@dispat/crier
crier render # Save the images locally.
crier --dry-run # Preview what each configured platform would receive.
crier publish # Render and publish to the enabled destinations.Run these commands in a directory with a Crier configuration, template, and data. The quickstart below creates all three. Running crier without a subcommand also publishes.
Install · Examples · Changelog images · Dispat setup · Platforms · Pagination · Text overflow · Video and music · Tunnels
Rendered examples
These are actual Crier renders, copied from the main repository. The image URLs below use the pinned upstream release so the npm README can display them before this wrapper repository is published. The same image files are included in docs/images, with their original paths.
| Preview | Example and source | What it demonstrates | | --- | --- | --- | | | Release changelog | Release version, features, fixes, breaking changes, and multi-line ellipsis. | | | Business promotion | Bundled Poppins font, gradients, caption templates, and a clamped description. | | | Game release | A landscape announcement with a separate story overlay. | | | Story overlay | The same data and base layout adapted to a vertical story. | | | Social quote | A template pool, seeded layout selection, serif typography, and alt text. | | | Event invitation | A 1080 × 1920 layout for event and story announcements. | | | Custom platform | A shell command as a publisher, with the same rendering and caption flow. |
To reproduce an upstream example, clone the main repository and run its configuration. Keep the example's adjacent data, overlays, and examples/fonts directory together; the bundled fonts carry their own licenses.
git clone https://github.com/yohimik/crier.git
cd crier
git checkout v1.1.1
crier render --config examples/release-changelog/crier.yamlInstall
npm
# Global command
npm install --global @dispat/crier --allow-scripts=@dispat/crier
crier --version
# Project development dependency
npm install --save-dev @dispat/crier
npm exec -- crier --helppnpm and Yarn
pnpm add --save-dev @dispat/crier
pnpm exec crier --help
# Yarn with its node_modules linker
yarn add --dev @dispat/crier
yarn crier --helpApprove the package's install script according to your package manager's policy. If scripts are disabled, use the explicit repair command. Yarn Plug'n'Play is not verified for this distribution.
Runtime and operating systems
| Requirement | Support |
| --- | --- |
| Node.js | ^20.17.0 || >=22.9.0 |
| macOS | x64 and ARM64 |
| Linux | x64 and ARM64 |
| Windows | x64 and ARM64 |
| Still images | Native renderer included; no browser or Go installation needed |
| MP4, animated GIF, or music-backed cover story | FFmpeg installed separately |
| A public tunnel | The selected tunnel program installed and configured separately |
Installation downloads the exact native release from GitHub, verifies its byte size and SHA-256 digest, runs its version check, and only then installs it. This package uses the six standard Go release assets.
npm package 1.1.0 wraps native Crier 1.1.1. Their patches can differ within the same major/minor line. npm ls @dispat/crier reports the npm version; crier --version reports the native version. The features and platform behavior documented here describe that native release.
Quickstart: your first image
Create a directory containing these three files. Rendering this example needs no credentials and sends no posts.
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: 88px;
background: linear-gradient(150deg, #11243b, #4b326c); color: #fff;
}
h1 { font-size: 84px; margin: 0; overflow-wrap: break-word }
p { font-size: 38px; line-height: 1.5; opacity: .85 }
</style></head><body><div class="card">
<h1>{{ .title }}</h1>
<p>{{ .subtitle }}</p>
</div></body></html>data.yaml
title: We just shipped 1.1
subtitle: A release announcement rendered from HTML, ready for your feed.crier.yaml
render:
template: template.html
data: data.yaml
width: 1080
height: 1080
format: png
output: card.png
hermetic-fonts: true
publish:
caption: "{{ .title }} — {{ .subtitle }}"crier render
# Writes card.png.For a generated starting configuration, use crier init; crier init --full writes every option. You still supply the template and data files.
Add a publishing destination
Add a Telegram destination to the existing publish section:
publish:
caption: "{{ .title }} — {{ .subtitle }}"
telegram:
enabled: true
chat-id: "@my_channel"Then supply the bot token through the environment:
export CRIER_PUBLISH_TELEGRAM_TOKEN='your-bot-token'
crier ping # Read-only credential checks; nothing posted.
crier --dry-run # Render and inspect the planned publication.
crier publish # Send the real post.Crier finds its configuration by walking up from the working directory. Paths in configuration files are relative to those files. Settings apply in order: file, environment, flags. For example, CRIER_RENDER_WIDTH=1200 or --render-width 1200 overrides render.width. Use crier config to inspect the resolved configuration with secrets redacted.
Supported publishing platforms
All fourteen publishers below are built into Crier 1.1.1. The table describes this implementation's media support; an enabled destination also needs its account credentials and any required API permissions.
| Platform | Images | MP4 | Animated GIF | Setup and behavior |
| --- | --- | --- | --- | --- |
| Instagram | Yes | Yes | No | Feed posts, photo carousels, reels, stories, and optional music-backed cover stories. Requires public media URLs. Still images are encoded as JPEG automatically. |
| Facebook | Yes | Yes | No | Facebook Page posts and stories; Page ID and Page access token. Direct uploads or optional URL staging. |
| TikTok | Yes | Yes | No | Photos use public URLs; videos can upload directly. Photo posts can request recommended music. |
| Telegram | Yes | Yes | Yes | Bot token and chat ID; albums, video, animation, and a separate audio message. |
| X / Twitter | Yes | Yes | Yes | Media upload followed by a post; configured X API credentials. |
| Mastodon | Yes | Yes | Yes | Instance URL and access token. |
| Discord | Yes | Yes | Yes | Incoming webhook; multiple attachments and optional music file. @everyone requires explicit mention permission in the config. |
| LinkedIn | Yes | Yes | No | Access token and author URN; multi-image posts or video. |
| Reddit | Yes | Yes | Yes | Configured Reddit credentials and User-Agent. Crier sends one page per post rather than an undocumented gallery flow. |
| Slack | Yes | Yes | Yes | Bot token and channel; file uploads and optional audio attachment. |
| VK | Yes | Yes | Yes | Access token and wall owner ID. |
| Threads | Yes | Yes | No | Threads token and public media URLs; supports carousels. |
| YouTube | No | Yes | No | Video uploads; still images must first become a video. |
| Boosty | Yes | No | No | Image posts through the implementation's unofficial API integration. |
A custom publisher can run your shell command or webhook script. Use crier platforms to see which destinations are enabled, configured, and need a public URL.
Cross-post the same card
Merge the following destinations into the quickstart's publish block. Telegram, Discord, and Facebook in this example accept uploads directly, so no tunnel is needed.
publish:
caption: "{{ .title }} — {{ .subtitle }}"
telegram:
enabled: true
chat-id: "@my_channel"
discord:
enabled: true
caption: "New release: {{ .title }}"
facebook:
enabled: true
page-id: "your-facebook-page-id"export CRIER_PUBLISH_TELEGRAM_TOKEN='your-bot-token'
export CRIER_PUBLISH_DISCORD_WEBHOOK_URL='your-incoming-webhook-url'
export CRIER_PUBLISH_FACEBOOK_TOKEN='your-page-access-token'
crier ping
crier publish --dry-run --json
# After reviewing the output:
crier publishPer-platform captions override the shared caption. Captions also receive .Platform, .Post, .Posts, .Page, and .Pages. Platforms can publish concurrently; multiple posts for one platform are sent in sequence. A failure at one destination does not cancel the other destinations. Inspect the report before replaying a partially successful announcement.
Turn a changelog into images
Crier draws the notes you provide. Your release tool generates the changelog and chooses the version. For a complete local example, see examples/changelog: it includes a configuration, a paginated template, sample release data, and a small Git-to-JSON adapter.
From this wrapper repository:
crier render --config examples/changelog/crier.yamlThe supplied example produces a cover and notes pages as examples/changelog/release-1.png, release-2.png, and so on.
Structured release data
The example template accepts this shape:
{
"package": "my-app",
"version": "1.1.0",
"summary": "A faster release, with fewer manual steps.",
"sections": [
{ "label": "Features", "items": ["Generate announcement cards", "Publish to multiple destinations"] },
{ "label": "Fixes", "items": ["Wrap long release-note entries"] }
]
}It renders the entries with ordinary template loops:
{{ range .sections }}
<section>
<h2>{{ .label }}</h2>
<ul>{{ range .items }}<li>{{ . }}</li>{{ end }}</ul>
</section>
{{ end }}Use a YAML or JSON file for arrays and nested objects. To pipe data directly from your release tooling, pass --render-data -; JSON is also accepted on stdin.
node make-release-data.mjs | crier render --config announce/crier.yaml --render-data -Generate a simple changelog from Git
Copy examples/changelog to announce/ inside the repository you want to announce, then run from that repository's root:
node announce/from-git.mjs v1.0.0 HEAD 1.1.0 my-app |
crier render --config announce/crier.yaml --render-data -The adapter groups feat: subjects as Features, fix: subjects as Fixes, and other subjects as Other. A conventional-commit ! or a BREAKING CHANGE: / BREAKING-CHANGE: body marker puts the entry under Breaking. It uses JSON serialization, so quotes and multi-line commit bodies cannot break YAML interpolation. It does not calculate semantic versions, resolve monorepo dependencies, or replace a release manager. Supply the intended Git range and release version yourself, or use the Dispat integration below.
Fit the notes to the job
Use a short, fixed-size summary card with line clamps when intentional truncation is acceptable. Use flowing pages when every release-note entry must appear. Crier's own announcement previews show the cover-plus-changelog pattern:
The local copies are included alongside the wrapper. For the complete upstream design and release scripts, see Crier's announce directory.
Automate announcements with Dispat
This follows the setup used by services/dispat/announce: one command in the package's announce stage, separate stable and release-candidate configurations, shared publishing destinations, and release data read directly from the stage environment. No intermediate changelog script is needed for this route.
Copy examples/dispat/announce into the package you release. The example dispat.yaml contains the entries to merge into that package's existing release configuration.
services/my-app/
dispat.yaml
announce/
publish.yaml
stable/
crier.yaml
template.html
rc/
crier.yaml
template.html
notes.yaml1. Connect the announce stage
Ensure dispat and crier are on the release job's PATH, for example by installing their npm wrappers:
npm install --global @dispat/bin @dispat/crier \
--allow-scripts=@dispat/bin --allow-scripts=@dispat/crierMerge these entries into the package-level dispat.yaml, preserving your existing build, publish, and postPublish stages:
scripts:
announce: >-
dispat if ANNOUNCE
--then 'crier publish --config "announce/$DISPAT_CHANNEL/crier.yaml"'
--else 'echo "announce: announcements are off for this run"'
flow:
announce: announceDispat runs this command from the package directory. DISPAT_CHANNEL=stable selects the stable configuration; rc selects the candidate. Add a folder for any other prerelease channel you use. The ANNOUNCE switch keeps ordinary release runs from posting unless announcement delivery is explicitly enabled in the release environment.
2. Read the release environment directly
The stable Crier config contains:
render:
template: template.html
data: "env:DISPAT_"
width: 1080
height: 1080
format: jpeg
jpeg-quality: 92
output: preview.jpg
hermetic-fonts: true
pages-max: 10
publish:
$ref: ../publish.yaml
caption: |-
{{ .package }} {{ .new_version }} is out.
Read the attached pictures for the full changelog.The prefix is removed and the remaining environment-variable names become lowercase template fields:
| Dispat environment | Crier template value | Use |
| --- | --- | --- |
| DISPAT_PACKAGE | {{ .package }} | Package name |
| DISPAT_NEW_VERSION | {{ .new_version }} | Exact release version |
| DISPAT_BREAKING_CHANGES | {{ .breaking_changes }} | Breaking-change notes |
| DISPAT_FEATURES | {{ .features }} | Feature notes |
| DISPAT_FIXES | {{ .fixes }} | Fix notes |
| DISPAT_DEPENDENCIES | {{ .dependencies }} | Dependency updates |
| DISPAT_CHANNEL | {{ .channel }} | Stable or prerelease channel |
Values read through env: are strings. The generated notes have one entry per line, so the stable template preserves them with white-space: pre-wrap and lets them paginate:
<style>
.items { white-space: pre-wrap; overflow-wrap: break-word }
</style>
{{ if .features }}
<h2>FEATURES</h2><div class="items">{{ .features }}</div>
{{ end }}The supplied stable template includes all four note groups. The caption stays short and points at the images, so long notes do not also become an oversized platform caption. The original Dispat setup additionally varies the wording by .Platform and includes short groups only while they fit its caption budget.
3. Share destinations between stable and rc
announce/publish.yaml is a publish-section fragment, with no outer publish: key:
instagram:
enabled: true
poll-timeout: 5m
max-attachments: 10
linkedin:
enabled: true
max-attachments: 10
discord:
enabled: true
max-attachments: 10
mention-everyone: falseBoth channel configs use publish.$ref: ../publish.yaml; fields beside the reference override the shared values. The sample uses no music file and leaves Discord mass mentions disabled. To reproduce Dispat's music-backed Instagram story, add cover-story: true under instagram and an audio source under each channel's render.video, as shown in Video and music. Install FFmpeg for that addition.
Dispat's actual release-candidate announcement is written by a person: the caption lives in rc/notes.yaml, and the corresponding card text lives in rc/template.html. The sample keeps the same structure:
# Inside announce/rc/crier.yaml
publish:
$ref: ../publish.yaml
caption:
$ref: notes.yamlEdit the candidate's caption and card together before releasing. Both can interpolate {{ .new_version }}. Stable announcements use the generated release-note groups instead.
4. Preview without posting
From the wrapper repository, the supplied example can be rendered with sample environment values:
DISPAT_PACKAGE=my-app \
DISPAT_NEW_VERSION=1.1.0 \
DISPAT_BREAKING_CHANGES='' \
DISPAT_FEATURES='Generate changelog images automatically' \
DISPAT_FIXES='Preserve long release-note entries' \
DISPAT_DEPENDENCIES='' \
crier render --config examples/dispat/announce/stable/crier.yaml
DISPAT_PACKAGE=my-app DISPAT_NEW_VERSION=1.2.0-rc.1 \
crier render --config examples/dispat/announce/rc/crier.yamlSet all note variables when hand-running a stable publish preview, even when empty; Dispat supplies them during a real release. render writes local images. publish --dry-run --json additionally resolves platform captions and publication plans without posting; it still needs the enabled publishers' required configuration.
5. Supply release-job credentials and staging
Set these in the release job's environment from your CI secrets:
| Environment variable | Value |
| --- | --- |
| CRIER_PUBLISH_INSTAGRAM_TOKEN | Instagram publishing token |
| CRIER_PUBLISH_INSTAGRAM_USER_ID | Instagram professional account ID |
| CRIER_PUBLISH_LINKEDIN_TOKEN | LinkedIn access token |
| CRIER_PUBLISH_LINKEDIN_AUTHOR_URN | LinkedIn author URN |
| CRIER_PUBLISH_DISCORD_WEBHOOK_URL | Incoming Discord webhook URL |
Instagram additionally needs staging. For a runner using an installed, configured ngrok client:
export CRIER_STAGE_MODE=server
export CRIER_STAGE_SERVER_TUNNEL_MODE=ngrok
# Supply NGROK_AUTHTOKEN through the release job's secrets when required.The shared sample retains Crier's default Instagram API host. If your token was issued through Instagram business login, set CRIER_PUBLISH_INSTAGRAM_API_BASE_URL=https://graph.instagram.com/v25.0, as Dispat's actual setup does. Choose the host for the token's login flow; the token and host must agree.
Before starting the real release, run crier ping against both channel configs with those credentials. When ready, set ANNOUNCE=1 in the existing release job and run your normal Dispat release. Keep package-release outputs in the package's postPublish hook, as Dispat does, so reporting package publication does not depend on a social network.
For example, these steps belong in your existing GitHub Actions release job, after installing the tools. Declare ANNOUNCE, the publishing credentials, and staging settings at job level so the preflight and release receive the same environment:
# Fragment of an existing release job; retain its checkout, tool installation,
# repository permissions, and package-publication credentials.
env:
ANNOUNCE: ${{ vars.ANNOUNCE }}
CRIER_PUBLISH_INSTAGRAM_TOKEN: ${{ secrets.CRIER_PUBLISH_INSTAGRAM_TOKEN }}
CRIER_PUBLISH_INSTAGRAM_USER_ID: ${{ secrets.CRIER_PUBLISH_INSTAGRAM_USER_ID }}
CRIER_PUBLISH_LINKEDIN_TOKEN: ${{ secrets.CRIER_PUBLISH_LINKEDIN_TOKEN }}
CRIER_PUBLISH_LINKEDIN_AUTHOR_URN: ${{ secrets.CRIER_PUBLISH_LINKEDIN_AUTHOR_URN }}
CRIER_PUBLISH_DISCORD_WEBHOOK_URL: ${{ secrets.CRIER_PUBLISH_DISCORD_WEBHOOK_URL }}
CRIER_STAGE_MODE: server
CRIER_STAGE_SERVER_TUNNEL_MODE: ngrok
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
steps:
- name: Check announcement inputs and credentials
if: env.ANNOUNCE != ''
working-directory: services/my-app
run: |
crier ping --config announce/stable/crier.yaml
crier ping --config announce/rc/crier.yaml
- name: Release packages and announce
run: dispat releaseSet the repository variable ANNOUNCE to 1 to enable this fragment's announcement path; leave it unset to skip it. Install and configure ngrok before these steps, or replace the staging environment with your S3 configuration. Set the Instagram API host override here too when your token requires it. This is documentation for your release workflow; the wrapper repository's own CI does not publish packages or social posts.
For a missing destination, inspect the completed posts first, then disable already successful destinations with variables such as CRIER_PUBLISH_INSTAGRAM_ENABLED=false before a deliberate replay. Dispat treats an announce-stage failure as a warning; read the Crier report rather than assuming the whole announcement succeeded because the package release did.
Copied stable/rc previews from Dispat and the original configurations, templates, fonts, and music provenance provide the complete reference design.
Pagination and carousels
Content that exceeds one page can flow onto the next. Do not set a fixed height on the document body when you want pagination. The quickstart's height: 100% is appropriate for a single card; a changelog uses normal document flow.
@page {
margin: 64px 64px 80px;
@top-left { content: "Release notes" }
@bottom-right { content: counter(page) " / " counter(pages) }
}
body { margin: 0 }
.cover { height: 936px; break-after: page } /* 1080 - 64 - 80 */
h2 { break-after: avoid }
.entry { break-inside: avoid }Margin boxes need nonzero page margins to be visible. break-after: page starts a new page; break-after: avoid keeps a heading with following content. A box larger than an entire page must still split.
render:
width: 1080
height: 1080
output: release.png
pages-max: 10
publish:
caption: "Release {{ .version }} — part {{ .Post }} of {{ .Posts }}"A one-page render writes release.png; a multi-page render writes release-1.png, release-2.png, and so on. pages-max defaults to 10, can be raised to at most 20, and fails the render if the document exceeds the configured ceiling.
Crier preserves the page order and splits longer sequences into the post sizes supported by its publishers:
| Destination | Photo pages per post in Crier 1.1.1 | | --- | --- | | Instagram feed, Facebook Page, Telegram, Discord, Slack, VK, Boosty | Up to 10 | | X and Mastodon | Up to 4, subject to instance/platform restrictions | | LinkedIn and Threads | Up to 20 | | Instagram stories, Facebook stories, Reddit | One page per story or post | | YouTube | Video only |
publish.<platform>.max-attachments can lower a publisher's cap. More pages become sequential posts rather than dropped pages. An Instagram or Telegram lead-video occupies one attachment slot in every post. Photo carousels do not automatically become a slideshow video; encoding a video is a separate flow.
Full behavior: pagination and carousels.
Text overflow and ellipsis
Choose whether a region should wrap, clip, truncate, or flow to another page. Crier supports these explicit CSS patterns.
A single line ending in an ellipsis:
.title {
width: 880px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}A description limited to three lines:
.description {
width: 880px;
overflow: hidden;
overflow-wrap: break-word;
max-lines: 3;
continue: discard;
block-ellipsis: auto;
}Long URLs, hashes, and unbroken words:
.notes { overflow-wrap: break-word }Use word-break: break-all for more aggressive wrapping. overflow: hidden alone clips; it does not add an ellipsis. Avoid clamping the full changelog when every entry must be preserved—let it paginate instead.
Crier's tested multi-line recipe uses max-lines, continue, and block-ellipsis. -webkit-line-clamp, display: -webkit-box, and the line-clamp shorthand are not supported substitutes. See text overflow and the CSS support reference.
One layout, different platform sizes
Render at a different size with a per-platform width and height. Use template overlays when the contents should rearrange, or fit when the existing image should be scaled into another frame.
publish:
instagram:
enabled: true
story: true
width: 1080
height: 1920
fit: contain
fit-background: "#11243b"contain keeps the full card with a background around it. cover fills the frame and crops the edges. stretch changes the aspect ratio. For text-heavy release cards, contain preserves the complete lines.
crier render --render-variant instagram --render-output story.pngTemplate {{ block }} sections can be replaced by {{ define }} blocks in a platform's overlay files. Platforms with the same size and overlays share a render. See overlays and fitting.
Video, GIFs, and music
Render an MP4
Install FFmpeg separately and put it on PATH, or configure render.video.ffmpeg-bin. Add this to a single-page template's existing render configuration:
render:
template: template.html
data: data.yaml
width: 1080
height: 1080
output: announcement.mp4
video:
enabled: true
format: mp4
fps: 24
duration: 4s
codec-preset: h264
audio: launch-theme.mp3
audio-loop: trueProvide launch-theme.mp3, or remove the audio keys to render silently. crier render executes and lays out the template once per frame and streams frames into FFmpeg. It is a frame-rendering pipeline, not browser JavaScript or CSS animation playback. Four seconds at 24 FPS means 96 layout/render passes.
Templates can animate using the frame values:
<div style="opacity: {{ .Video.Progress }}">The release is here.</div>.Video.Frame starts at zero; .Video.Frames is the total count, .Video.Time is elapsed seconds, and .Video.Progress runs from 0 to 1. render.video.frames sets an exact frame count instead of deriving it from duration and FPS.
Make an animated GIF
render:
output: announcement.gif
video:
enabled: true
format: gif
fps: 12
duration: 2sGIFs have no soundtrack; audio settings are ignored for that format. Telegram, Discord, Mastodon, X, Reddit, Slack, and VK accept GIFs through Crier. Use MP4 for the other video-capable publishers. Keep GIFs short to control output size.
Three ways to add music
| Goal | Configuration | Result |
| --- | --- | --- |
| A soundtrack inside the video | render.video.audio | FFmpeg mixes audio into the MP4; the uploaded video carries it. |
| A separate audio file with a photo post | publish.music-file | Discord and Slack attach the file; Telegram sends it after the image/album as an audio message. |
| A platform-recommended track for TikTok photos | publish.tiktok.auto-add-music: true | TikTok chooses a recommended track; this does not select a song by ID. |
For separate audio attachments:
publish:
music-file: launch-theme.mp3Use audio you are entitled to distribute. Crier 1.1.1 has no setting for choosing a licensed Instagram, Facebook, or TikTok library track by song ID. A soundtrack embedded in your own MP4 and an audio-file attachment are different features.
A pool chooses one audio file per run:
render:
video:
audio-pool:
- launch-theme.mp3
- alternate-theme.mp3Instagram feed carousel plus a music-backed cover story
Keep the main render as still images, then add:
render:
video:
enabled: false
audio: launch-theme.mp3
publish:
instagram:
enabled: true
cover-story: trueCrier publishes the normal photo feed post or carousel and generates a separate 16-second MP4 Story from page one, with the selected audio. This requires FFmpeg, a real audio file or audio pool, Instagram credentials, and public staging for the clip. cover-story cannot be combined with story: true, which changes the primary output to stories.
This is the music pattern used in Dispat's announcement setup. Alternatively, publish.instagram.lead-video: intro.mp4 or publish.telegram.lead-video: intro.mp4 prepends an existing MP4 to each photo carousel/album and consumes one attachment slot.
More: video rendering · music · existing media and frames.
Public media URLs: staging and tunneling
Instagram, Threads, and TikTok photo publishing need a publicly reachable media URL. The platform fetches the file from its own servers, so localhost is not sufficient. Crier stages the media using one of these modes:
| Mode | Use |
| --- | --- |
| none | Publishers that accept direct file uploads |
| s3 | Upload to an S3-compatible object store and provide public or presigned URLs |
| server | Serve the staged files locally, through an existing public address or a tunnel |
| url | Supply an already hosted media URL yourself |
ngrok or zrok
Install and configure your tunnel client first. Crier starts it when needed and closes the managed server and tunnel when the publication finishes.
stage:
mode: server
server:
listen: 127.0.0.1:0
tunnel:
mode: ngrok
startup-timeout: 30sUse mode: zrok for an installed and enabled zrok client. Tunnel binaries are not included in the npm package. The public URL must return the media directly: login pages, tunnel warning/interstitial pages, and inaccessible private addresses will prevent the platform from fetching it. Upstream documents this as a common problem with ngrok interstitials.
A custom tunnel, such as cloudflared
stage:
mode: server
server:
listen: 127.0.0.1:0
tunnel:
mode: custom
bin: cloudflared
args: ["tunnel", "--url", "http://{addr}"]
url-pattern: '(https://[a-z0-9-]+\.trycloudflare\.com)'{addr} and {port} are substituted into arguments. The URL pattern must have exactly one capture group, as the parentheses above provide. Do not set stage.server.public-url alongside an automatic tunnel; either configure an existing public endpoint or let the tunnel discover one.
S3-compatible staging
stage:
mode: s3
s3:
endpoint: s3.eu-west-1.amazonaws.com
region: eu-west-1
bucket: my-release-media
prefix: announcements
presign: true
presign-expiry: 1h
delete-after: trueexport CRIER_STAGE_S3_ACCESS_KEY='your-access-key'
export CRIER_STAGE_S3_SECRET_KEY='your-secret-key'Configure the endpoint and bucket for your provider. Presigned URLs allow the fetch without making the whole bucket public. The URL must remain valid throughout platform processing. delete-after: true removes staged objects after the run; retain them when your publishing flow requires later fetching.
For a file already hosted elsewhere:
stage:
mode: url
url: https://cdn.example.com/release.jpgCrier does not upload or verify that this URL represents your render. Use it only when your own process has already hosted the intended media. See staging and tunnel configuration.
Commands at a glance
| Command | Purpose |
| --- | --- |
| crier init / crier init --full | Generate a starter or complete configuration |
| crier render | Render locally without publishing |
| crier render --render-variant instagram | Preview one platform's layout |
| crier ping | Check enabled destinations and configured media inputs without posting |
| crier platforms | List destination readiness and media requirements |
| crier config | Inspect resolved configuration with secrets redacted |
| crier publish --dry-run --json | Render and inspect the publishing plan without network publication |
| crier publish / crier | Render, stage as needed, and publish |
| crier --version | Show the native binary version |
Results use stdout and diagnostics use stderr. The npm launcher preserves arguments, input/output streams, working directory, environment, exit codes, and termination signals. Native command flags follow the subcommand, for example crier render --config announce/crier.yaml.
Installation scripts and repair
If installation scripts were blocked or the download failed:
# Local npm installation
npm explore @dispat/crier -- node build/bin/postinstall.js
# Global npm installation, from any directory
npm explore --global @dispat/crier -- node build/bin/postinstall.js
# pnpm or Yarn with the node_modules linker
node node_modules/@dispat/crier/build/bin/postinstall.jsA missing binary also produces a repair command for that exact installation. Ordinary commands never trigger an automatic download. Installation needs HTTPS access to GitHub and its release-asset hosts; downloads honor npm_config_https_proxy, npm_config_proxy, npm_config_noproxy, and npm_config_cafile. Set CRIER_NPM_DEBUG=1 for installer diagnostics on stderr.
Update and remove
npm update @dispat/crier
# Global update:
npm install --global @dispat/crier@latest --allow-scripts=@dispat/crier
# Install a specific wrapper version:
npm install --global @dispat/[email protected] --allow-scripts=@dispat/crier
# Global removal:
npm uninstall --global @dispat/crierNative self-update mutations are blocked because npm owns this installation. crier self-update --check and crier self-update --help remain available. Use your package manager to upgrade or roll back.
This repository's Dispat configuration
This is one pnpm project with one root manifest. @dispat/[email protected] is a pinned
development dependency, and package.json pins pnpm 10.34.1.
That pnpm line supports the package's Node 20.17 minimum; the Docker test suite also
runs Node 22.9 and 24.18. pnpm-workspace.yaml approves only
Dispat's dependency build script and does not declare additional packages.
dispat.yaml declares one standalone package named crier at
src/. Its npm manifest stays at the root. changelog.file: ../CHANGELOG.md
writes CHANGELOG.md at the root, and the release commit includes
that file, package.json, and pnpm-lock.yaml.
pnpm install --frozen-lockfile
pnpm release:plan # Read-only release plan.
pnpm release:notes # Preview both the changelog and GitHub release body.Planning needs at least one Git commit; an empty, uncommitted clone has no HEAD
for Dispat to inspect. pnpm test and the build commands below work before that
first commit. Use the crier commit scope for release-worthy root-only changes
such as fix(crier): update installer documentation; unscoped changes in src/
also belong to the package.
The prepared first npm version remains 1.1.0. Dispat derives the release plan
from Git history and tags, not the parent manifest. The initial feature commit
includes the footer Release-As: 1.1.0 to pin that first version. Before the first
authorized release, confirm that pnpm release:plan selects 1.1.0.
The version stage updates the root manifest and refreshes pnpm-lock.yaml in Docker,
with install hooks disabled. The build packages native Crier 1.1.1, unless
CRIER_BINARY_VERSION selects another published version, and exports the compiled
wrapper, metadata, and tarball into .release/.
Dispat then runs flow.postBuild: tests: every unit, integration, coverage, and
artifact installation test runs against the releasing package, inside Docker on
Node 20.17, 22.9, and 24.18. Tests use the compiled files and tarball exported by the
completed build. All three runs must pass before the artifact gets a test receipt.
The publish stage verifies the tarball's identity, checksum, and receipt, then runs
npm inside Docker. Stable versions use latest; prereleases use their channel.
After publication, Dispat records and pushes the release commit and v{version}
tag, writes the root changelog, and creates a GitHub release in
yohimik/crier-npm. The GitHub release includes the generated release notes and
a link to that exact version on npm, such as
https://www.npmjs.com/package/@dispat/crier/v/1.1.0.
CI/CD
The release workflow follows Dispat's sequence, using the pinned npm development dependency as the driver:
- Is there anything to release?
pnpm exec dispat status --require-releasecomputes the plan. Exit 3 skips publication successfully; a broken plan fails. - Run dispat, publish binaries.
pnpm exec dispat release --log-format jsonowns versioning → Docker build → all tests inpostBuild→ Docker publication → changelog, commit, tags, and GitHub release. - Check published npm installation. Only when npm was published, wait for that exact version and install it on Linux, macOS, and Windows, on x64 and ARM64. Verify the native version and CLI help. This also runs if publication succeeded but a later release-recording step failed.
There is no separate pre-release test job. The complete suite gates publication
inside the releasing package's own flow, after its version and build are ready.
Docker Buildx uses the same GitHub Actions cache backend as Dispat. Each operation
is a separate named script in dispat.yaml; there are no shell script files or
shell dispatch functions. Node on the runner only
installs and invokes @dispat/bin; the package build, tests, and npm upload run in
containers. npm 12 runs on the Node 24 image, including script-approval tests.
CI runs on pull requests and pushes to main. Its
full-suite workflow invokes the same Docker build,
test, and artifact-verification scripts without publishing. It explicitly runs the
scripts in order because dispat run build does not execute release hooks.
Release is a manual dispatch from main and queues
concurrent releases. Both workflows retain coverage and the tested artifact.
The wrapper supports six native targets: Linux, macOS, and Windows on x64 and ARM64. The pre-publication container tests exercise Linux on the Docker host's native architecture. After publication, native runners check all six targets. There are no announcement jobs. The Dispat announcement example remains available for projects that want social posts.
Before dispatching Release, add the repository secret NPM_TOKEN:
- Create an npm granular access token
with Read and write permission for the
@dispatscope and Bypass 2FA enabled for unattended publishing. Choose direct publishing permission, not Read and write (stage only). The account must be able to publish under@dispat; a token cannot grant permissions its account does not have. - Open New repository secret,
enter
NPM_TOKENas the name, paste the token into GitHub's secret field, and save. - Dispatch Release from main after review. Dispat's
run.beforeAll: npm-authchecks for the secret and runsnpm whoamiinside Docker before versioning, building, or testing. Authentication failures stop the release immediately.
The workflow exposes the secret as NODE_AUTH_TOKEN. .github/npmrc
contains only that environment-variable reference; the actual token is never
committed. Authentication and publication mount this file read-only and receive
the token at runtime. Docker builds and tests receive no npm credentials.
The release job retains id-token: write for npm provenance and contents: write
for Dispat's lock, release commit, tags, and GitHub release. GitHub's OIDC environment
is forwarded to the publication container. The current configuration explicitly
requires NPM_TOKEN; repository rules must also permit the release writes.
Dispatching Release performs the real publication. Building or testing locally does not dispatch it. Pushing to main runs CI checks; publication requires a separate manual Release dispatch.
Development and release preparation
This repository adapts Dispat's packages/cli. Native source stays in yohimik/crier.
pnpm install --frozen-lockfile
pnpm test
CRIER_BINARY_VERSION=1.1.1 pnpm build
pnpm run pack
pnpm run test:artifactOn PowerShell, set $env:CRIER_BINARY_VERSION = '1.1.1' before the build.
test:artifact runs the detailed temporary-prefix npm/pnpm checks on macOS/Linux;
the Docker suite also runs the portable install check on its Linux architecture.
The project uses pnpm for dependency installation, scripts, and lockfile updates.
The artifact helpers deliberately call the npm CLI for npm pack and npm publish,
and the consumer tests exercise both npm and pnpm.
The build fetches metadata for the exact native tag and rejects missing assets,
invalid sizes/digests, and mismatched tags. Packing produces
dist/dispat-crier-1.1.0.tgz and an integrity record in dist/artifact.json.
The Docker release path is separate from these local development outputs:
pnpm exec dispat run build --since all
pnpm exec dispat run tests --since all
pnpm exec dispat run verify-artifact --since allThese commands require Docker with Buildx. The tested release tarball is under
.release/dist/, and coverage is under coverage/node-<version>/. Container paths
inside .release/dist/artifact.json are verified inside Docker. Do not replace
that tarball with a later local pack; publication requires the matching test receipt.
The README, changelog, copied previews, and runnable examples are included; generated example preview outputs are excluded. See TESTING.md for the full validation and release procedure.
Documentation and source
- Crier documentation: complete native feature documentation for the pinned binary.
- Full configuration sample: every configuration key and default.
- Template syntax, fonts, and template pools.
- Dispat release announcements: the working setup behind the integration example.
- Wrapper issues for npm installation; Crier issues for rendering and publishing.
MIT licensed. Copied preview provenance is listed in docs/images/README.md.
