Every post is a markdown file with a YAML block on top. The parser reads scalar
key: value pairs and ignores everything nested, which is all a post needs. Fields the
tool does not know about are left alone, so your own metadata is safe.
---
title: How I Publish From The Terminal
slug: how-i-publish-from-the-terminal
---
Your article body.That is the whole requirement: a title and a slug.
---
title: How I Publish From The Terminal
subtitle: One command, no web editor, no copy and paste
slug: how-i-publish-from-the-terminal
cover: images/cover.png
audience: everyone
search_engine_title: Publish to Substack from the command line
search_engine_description: A CLI that turns markdown into a live Substack post.
id: 209491778
---| Field | Aliases | What it does |
|---|---|---|
title |
seo_title |
The post headline. Falls back to the filename. |
subtitle |
description, seo_description |
The dek under the headline. |
slug |
url_slug |
Required. The public URL. |
cover |
cover_image, image |
Hero image, relative to the markdown file. |
id |
The live post id. Written for you on first push. | |
audience |
everyone, only_paid, or only_founding. Set at creation. |
|
search_engine_title |
Overrides the <title> tag Google shows. |
|
search_engine_description |
Overrides the meta description. |
Left alone, Substack derives a slug from your title and truncates it. "You Are Giving
Your Agent Feedback Wrong" became you-are-giving-your-agent-feedback, and the URL its
author expected returned 404 with no redirect.
Three behaviors combine to cause that, and all three are verified.
POST /draftssilently drops aslugfield, so a new draft always comes back withslug: null.- Substack fills that null with its own truncated guess at publish time.
PUT /drafts/{id}does accept a slug, before or after publishing.
So push, update, and publish all read your slug, refuse without one, read the live
value back, and write it again whenever the two differ. A line reading
Substack set the slug to ... in the output means the guard did its job.
A valid slug is lowercase words joined by single hyphens. Keep it under 60 characters.
You never write id by hand. The first push creates the draft and writes the id into
your file, which is what makes the second push an update rather than a duplicate.
If you delete a draft in the Substack UI, delete the id line too. Otherwise the next
push writes to a dead id.
cover: is a path relative to the markdown file, so a post folder stays portable.
cover: images/hero.pngThe image uploads once and gets used twice: as the post's cover_image, which is the
thumbnail in your feed, archive, and social embeds, and as the image filling the «COVER»
slot in a saved post template. A URL works too, and is passed through without uploading.
A missing cover file is a hard error, on the grounds that silently publishing without one is worse than stopping.
title is what readers see. search_engine_title is what Google shows in results. They
can differ, which is useful when a good headline runs past the 60 characters a search
result will display.
An absent SEO field is omitted from the payload rather than sent blank, so a value you typed into the Substack UI survives a push from a file that has not caught up.
Anything else. Keep your own tags, dates, categories, reading time, or build metadata in the same block. The parser reads what it recognises and passes over the rest, and nothing you add gets published.