Install
openclaw skills install @nickludlam/mailchimp-to-ghost-skillSync Mailchimp newsletter posts to a Ghost blog. Fetches newsletter content from RSS, converts HTML to clean markdown with horizontal rules, uploads images to Ghost, and publishes as Lexical-format posts with proper formatting.
openclaw skills install @nickludlam/mailchimp-to-ghost-skillFetch newsletter posts from a Mailchimp RSS feed and sync them to a Ghost blog as properly formatted posts with uploaded images and horizontal rules.
This skill automates the complete workflow of:
beautifulsoup4, requests, feedparser, markdownify, lxmlGHOST_URL - Your Ghost instance URLGHOST_STAFF_ACCESS_TOKEN - Ghost Admin API tokencd skills/sync-newsletter
pip install beautifulsoup4 requests feedparser markdownify lxml
Install globally via npm:
npm install -g @tryghost/ghst
Set up your Ghost credentials in ~/.openclaw/.env:
GHOST_URL="https://myghostblog.com"
GHOST_STAFF_ACCESS_TOKEN="your-token-id:secret"
You MUST re-fetch the RSS feed every time before syncing to ensure you're working with the latest newsletter data. The cache may contain stale or previously-processed entries.
# 1. Clear any stale cache and fetch fresh from RSS (ALWAYS DO THIS FIRST)
rm -rf cache/ && python3 scripts/fetch_markdown.py
# 2. Sync to Ghost (downloads images, uploads them, creates draft post)
python3 scripts/sync_to_ghost.py
Why this matters: The RSS feed only shows the most recent newsletter(s). If you sync without re-fetching, you may:
-2, -3 suffixes to slugs)All cached data is stored in the cache/ folder:
cache/feed_cache.xml — RSS feed cachecache/newsletter_markdown_1.md — Generated markdownTo force a fresh fetch, delete the cache:
rm -rf cache/
The sync script checks Ghost for existing posts with similar titles before creating. If a match is found, it warns you and exits. Use --force to create anyway:
python3 scripts/sync_to_ghost.py --force
The fetch script extracts and converts:
mceDividerBlockContainer → ---The sync script creates Lexical JSON with:
--- → horizontalrule nodesbutton nodes (center-aligned by default)Mailchimp button blocks are converted to Ghost's native Lexical button nodes:
Markdown pattern: [button: text](url)
Lexical output:
{
"type": "button",
"version": 1,
"buttonText": "Read More",
"alignment": "center",
"buttonUrl": "https://example.com"
}
Configuration:
sync_to_ghost.py)scripts/fetch_markdown.pyFetches Mailchimp RSS and converts to clean markdown.
Features:
mceDividerBlockContainer elements and converts to --- horizontal rulesConfiguration:
RSS_URL — The Mailchimp feed URL (hardcoded in script)limit — Number of entries to process (default: 1)cache/newsletter_markdown_N.mdscripts/sync_to_ghost.pyComplete sync workflow: downloads images, uploads to Ghost, creates Lexical-format draft post.
Features:
ghst image upload<br>-style breaks)--- → horizontalrule nodes)[button: text](url) pattern → center-aligned Lexical button nodesnewsletter tag as draftCommand-line options:
--force — Bypass deduplication check# Navigate to skill directory
cd ~/.openclaw/workspace/skills/sync-newsletter
# Fetch latest newsletter
python3 scripts/fetch_markdown.py
# Review the markdown
cat cache/newsletter_markdown_1.md
# Sync to Ghost (creates draft)
python3 scripts/sync_to_ghost.py
# Publish via Ghost admin when ready
-2, -3 suffix)Cause: You synced without re-fetching the RSS feed first. The cache contained an already-published newsletter.
Fix: Always clear cache and re-fetch before syncing:
rm -rf cache/
python3 scripts/fetch_markdown.py
python3 scripts/sync_to_ghost.py
Delete cache and re-fetch:
rm -rf cache/
python3 scripts/fetch_markdown.py
Check Ghost API is accessible:
ghst site info --json
The deduplication should prevent this. If it fails, check the existing post title matches exactly.
Cause: The markdown may contain plain links instead of the button pattern.
Fix: Check the generated markdown has [button: text](url) pattern (not just [text](url)). If buttons aren't detected, the Mailchimp HTML structure may have changed.
--- in markdown and converted to Ghost's horizontalrule nodes[button: text](url) pattern