A content calendar that lives in git

Plan, render and publish branded social posts from a git repo.

Each post is a Markdown file. postline checks it against your house rules, renders an on-brand image card for it, and publishes it to LinkedIn, Bluesky, Mastodon or any webhook when its time comes.

npm install --global postline
Get started

One Markdown file in. A branded card and a scheduled post out.

posts/2026-11-03-untrusted-output.md
---
at: 2026-11-03 08:30
topic: llm-output
card:
  layout: rows
  eyebrow: AI security
  headline:
    - LLM output is untrusted input,
    - not your application's output.
  items:
    - title: Rendered as HTML
      note: Cross-site scripting through the model
      chip: xss
    - title: Executed as a query
      chip: injection
    - title: Fetched as a URL
      chip: ssrf
  footer: Validate what comes out, not just what goes in.
---

What comes out of the model is untrusted input
to the rest of your system.
…
#AISecurity #AppSec
The rendered card: dark background, the eyebrow AI security, the headline LLM output is untrusted input, not your application's output, and three numbered rows tagged xss, injection and ssrf.
The card, rendered by headless Chromium from the brand in postline.config.yaml.

What you get

  1. Posts as files

    Review copy in pull requests. Grep your history. Nothing is locked in a vendor.

  2. Cards from your design system

    Colours, font and logo live in one file. Point it at your design tokens, add your own CSS, or write your own layouts in HTML.

  3. Rules that block bad posts

    Hashtag limits, banned phrases, allowed link domains, emoji policy, per-network character limits and a check for broken text encoding.

  4. Publishing that never double posts

    Official APIs only. If a run dies mid-send or a network gives no clear answer, postline stops and asks instead of guessing.

  5. Every graphic kept

    A lossless, full-size copy of each card is saved on your computer with a gallery to browse them later.

Your week, at a glance

Drafts for every empty slot with postline plan. Lint them in CI. Then publish from GitHub Actions, your operating system's scheduler, or the built-in daemon on any always-on machine. A ledger records what went where.

How scheduling works

Terminal
$ postline lint
3 posts, 0 errors, 0 warnings

$ postline status --week 2026-11-03
week of 2026-11-03 (America/New_York)

Tue, Nov 3, 8:30 AM        2026-11-03-untrusted-output  linkedin:queued blog:queued
Thu, Nov 5, 8:30 AM        2026-11-05-least-privilege   linkedin:queued blog:queued

$ postline daemon --every 15
postline daemon started; checking every 15 minutes

Does every post look the same? Only if you want it to.

  1. Built-in layouts Rows, grid and statement, in your colours, font and logo. Zero setup.
  2. Your tokens and CSS Read the tokens file your design system already exports and restyle the built-in layouts.
  3. Your own layouts Drop HTML templates into a folder; quote.html becomes layout: quote.
A landscape Nsisong Labs card in the grid layout with four industry panels.
A custom quote layout in Nsisong Labs blue: Reputation compounds faster than revenue.
A custom layout: one HTML template and the studio's own tokens.

Bring your own design system

Where it publishes

LinkedIn
Company pages or members, via the Posts API. Hashtags stay clickable.
Bluesky
App password login. Links and hashtags become rich text, and retries can't post twice.
Mastodon
Any instance, with an idempotency key on every status.
Webhook
A signed JSON POST for Zapier, Make, n8n, Slack approvals or your own service.
Files
Markdown and the image into a folder, such as your static site's blog.

Set up a channel

Start with one post.

postline init scaffolds a config, a sample post, a logo and a GitHub workflow.

Read the getting started guide