Publish Markdown files to Ghost CMS. Two ways to do it: a browser-based uploader for quick manual posts, and a GitHub Action that syncs your repo's .md files to Ghost on every push.
action.yml # GitHub Action definition
src/ # Action source code
dist/index.js # Compiled action bundle
docs/index.html # Static web UI — drag-and-drop uploader
A single HTML file. No build step, no dependencies to install. Open it in a browser or host it on GitHub Pages.
- In your Ghost Admin, go to Settings > Integrations > Add custom integration
- Copy the Admin API Key (looks like
64hexchars:64hexchars) - Open
docs/index.htmlin your browser - Paste your Ghost URL and API key, hit Save
Your credentials stay in localStorage. Nothing leaves your browser except the API calls to your Ghost instance.
Drop .md files onto the upload area (or click to browse). Each file becomes a draft post in Ghost. You'll get a direct link to edit each one in Ghost Admin.
Séance reads YAML frontmatter from your Markdown files. All fields are optional; if you skip title, the filename gets used instead.
---
title: How we migrated to Postgres
tags: [engineering, databases]
excerpt: What went wrong and what we'd do differently.
slug: postgres-migration
feature_image: https://cdn.example.com/og-image.png
featured: true
status: draft
---
Your post content starts here.| Field | Type | What it does |
|---|---|---|
title |
string | Post title. Falls back to filename. |
tags |
string or list | Assigns Ghost tags. Creates them if they don't exist. |
excerpt |
string | Custom excerpt shown in post previews. |
slug |
string | URL slug. Ghost auto-generates one if omitted. |
feature_image |
URL string | Featured image URL for the post. |
featured |
boolean | Pin post as featured. |
status |
string | draft (default) or published. |
Keep blog posts in a Git repo as .md files. On push, Séance diffs against the previous commit and syncs changes to Ghost: new files become posts, modified files update existing posts, deleted files get unpublished (set to draft).
Add this workflow to your blog repo at .github/workflows/publish.yml:
name: Publish to Ghost
on:
push:
branches: [main]
paths: ['posts/**']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Required — Séance needs the parent commit for diffing
- uses: your-username/seance@v1
with:
ghost-url: ${{ secrets.GHOST_URL }}
ghost-admin-api-key: ${{ secrets.GHOST_ADMIN_API_KEY }}| Input | Required | Default | Description |
|---|---|---|---|
ghost-url |
yes | — | Your Ghost instance URL |
ghost-admin-api-key |
yes | — | Admin API key (id:secret format) |
posts-dir |
no | posts/ |
Directory containing your .md files |
default-status |
no | draft |
Status for new posts without status in frontmatter |
| Output | Description |
|---|---|
created |
Number of posts created |
updated |
Number of posts updated |
unpublished |
Number of posts set to draft (from deleted files) |
errors |
Number of files that failed |
- Runs
git diffbetween the current and previous commit - For each added/modified
.mdfile: parses frontmatter, converts to HTML, uploads relative images to Ghost, creates or updates the post (matched by slug) - For deleted
.mdfiles: reads the old file from git history to get the slug, sets the post to draft
Two options, both work:
- External URLs — reference images hosted on S3, a CDN, wherever. They pass through as-is.
- Relative paths — put images next to your
.mdfiles (e.g../images/diagram.png). Séance uploads them to Ghost's media library and rewrites the URLs automatically.
The action needs the parent commit to diff against. Without fetch-depth: 2 in your checkout step, GitHub's shallow clone won't have it and the action will fail.
Open the file directly:
open docs/index.htmlIf your Ghost instance blocks file:// origins (CORS), serve it locally instead:
cd docs && python3 -m http.server 8080Then visit http://localhost:8080.
MIT