How it works
queue/my-post.md --> worker polls every 60s --> sent/my-post.md (with permalinks)
--> failed/my-post.md (with errors)
- Write a markdown file with YAML frontmatter specifying target platforms
- Drop it into
queue/ - The worker picks it up, publishes to each platform in parallel
- The file moves to
sent/with platform permalinks written back into the frontmatter - If anything fails, the file moves to
failed/with error details and any successful URLs
You can also use the CLI or the HTTP API to create, manage, and publish posts programmatically.
Post format
Social post
---
platforms:
- bluesky
- mastodon
- linkedin
scheduledAt: 2026-03-24T09:00:00Z # optional
---
Your post content here. Supports **markdown**.
Blog post
Blog platforms (Medium, Dev.to, Substack) require a title field. You can target both social and blog platforms in the same post.
---
title: "Why Posterous Was Ahead of Its Time"
platforms:
- medium
- devto
- substack
- bluesky
---
Long-form **markdown** content here...
Post with images
Place the markdown file and images together in a subdirectory of queue/:
queue/
my-post/
post.md
photo.jpg
banner.png
---
platforms:
- bluesky
- linkedin
images:
- path: photo.jpg
alt: "A sunset over the mountains"
- path: banner.png
---
Post content here.
Frontmatter reference
| Field | Required | Description |
|---|---|---|
platforms |
Yes | One or more of bluesky, mastodon, linkedin, medium, devto, substack |
title |
For blog platforms | Post title for Medium, Dev.to, and Substack |
scheduledAt |
No | ISO 8601 timestamp. Post waits in queue until this time |
images |
No | Array of {path, alt}. Max 4 images. Formats: jpg, png, gif, webp |
Supported platforms
| Platform | Type | Format | Features |
|---|---|---|---|
| Bluesky | Social | Plaintext | Rich text facets (links, mentions, hashtags), link previews, images |
| Mastodon | Social | Plaintext | Bare domain auto-linking, image attachments with alt text |
| Social | Plaintext | Link previews with OG thumbnails, image attachments | |
| Medium | Blog | HTML | Requires title |
| Dev.to | Blog | Raw markdown | Requires title |
| Substack | Blog | HTML via SMTP | Arrives as draft, requires title |
System of record
After publishing, Blurt writes platform permalinks back into the frontmatter of each file. The sent/ directory is your system of record.
---
title: "My Post"
platforms:
- bluesky
- mastodon
publishedAt: "2026-03-28T14:30:45Z"
results:
bluesky:
url: "https://bsky.app/profile/you.bsky.social/post/abc123"
publishedAt: "2026-03-28T14:30:44Z"
mastodon:
url: "https://mastodon.social/@you/123456789"
publishedAt: "2026-03-28T14:30:45Z"
---
My post content.
Failed posts move to failed/ with error details and any URLs that did succeed, so you know exactly what happened.
Architecture
queue/ --> QueueScanner finds pending posts
--> PublishOrchestrator publishes to all platforms in parallel
--> PostMover moves to sent/ (with permalinks) or failed/ (with errors)
- Posts are POROs —
Postis a plain Ruby object that reads.mdfiles. No ActiveRecord. - Filesystem is authoritative — SQLite stores
PublishLogfor fast queries. The files insent/are the source of truth. - File locking — prevents double-processing via
.publishingsuffix rename. - Parallel publishing — all platforms publish simultaneously via
Concurrent::Future. - Link previews — Bluesky and LinkedIn automatically fetch OG metadata and attach link cards.