ghost-blog

SKILL.md

Ghost Blog Management

Manage Ghost CMS posts and tags via Admin API.

Setup

1. Install Dependencies

cd .claude/skills/ghost-blog/scripts
uv venv
uv pip install -r requirements.txt

2. Configure Environment

cd .claude/skills/ghost-blog/scripts
cp .env.example .env
# Edit .env with your Ghost credentials

.env file:

GHOST_URL=https://your-blog.ghost.io
GHOST_ADMIN_KEY=your-key-id:your-secret-key
GHOST_API_VERSION=v5.0

Get your Admin API Key:

  1. Go to Ghost Admin → Settings → Integrations
  2. Create a Custom Integration
  3. Copy the Admin API Key (format: id:secret)

3. Verify Setup

cd .claude/skills/ghost-blog/scripts && uv run python ghost_core.py

Quick Start

List posts:

python scripts/posts_browse.py --status draft
python scripts/posts_browse.py --tag news --featured

Manage single post:

python scripts/posts_crud.py get --id POST_ID
python scripts/posts_crud.py create --title "New Post" --html "<p>Content</p>"
python scripts/posts_crud.py publish --id POST_ID

Bulk operations:

python scripts/posts_bulk.py publish --filter "status:draft" --execute
python scripts/posts_bulk.py add-tag --filter "status:published" --tag "archive" --execute

Manage tags:

python scripts/tags_manage.py list
python scripts/tags_manage.py create --name "Tutorial"

Dispatch Rules

User Intent Script Example Command
test, run tests pytest cd scripts && uv run pytest -v
list posts, show drafts, filter posts posts_browse.py --status draft --tag news
get post, read post, show post posts_crud.py get --id xxx or --slug xxx
create post, new post, write post posts_crud.py create --title "..." --html "..."
update post, edit post, change post posts_crud.py update --id xxx --title "..."
delete post, remove post posts_crud.py delete --id xxx --confirm
publish post, publish draft posts_crud.py publish --id xxx
unpublish post posts_crud.py unpublish --id xxx
bulk publish, publish all drafts posts_bulk.py publish --filter "..." --execute
bulk unpublish posts_bulk.py unpublish --filter "..." --execute
add tag to posts, tag posts posts_bulk.py add-tag --filter "..." --tag xxx --execute
remove tag from posts posts_bulk.py remove-tag --filter "..." --tag xxx --execute
list tags, show tags tags_manage.py list (no options needed)
create tag, new tag tags_manage.py create --name "..."
delete tag, remove tag tags_manage.py delete --slug xxx --confirm

Scripts

Script Purpose
ghost_core.py Shared: JWT auth, HTTP client, error handling
posts_browse.py List, filter, search posts
posts_crud.py CRUD operations for single posts
posts_bulk.py Batch operations (publish, tags, featured)
tags_manage.py CRUD operations for tags

Filter Syntax (NQL)

Ghost uses NQL for filtering:

# Status
status:draft
status:published
status:scheduled

# Tags (use slug, not name)
tag:news
tags:[news,tutorial]

# Featured
featured:true

# Combine (AND)
status:published+featured:true

# Combine (OR)
status:draft,status:scheduled

Note: The --tag option in posts_browse.py accepts both tag names and slugs. It automatically resolves names to slugs via API lookup.

Creating Posts from Markdown Files

When creating posts from markdown files (e.g., a blog series):

import markdown
from ghost_core import api_request

# 1. Convert Markdown to HTML
md_content = open('article.md').read()
html_content = markdown.markdown(md_content, extensions=['extra', 'nl2br'])

# 2. Create post with source=html (CRITICAL for Ghost 5.0+)
post_data = {
    'title': 'My Post',
    'html': html_content,
    'status': 'draft',
    'tags': ['my-tag']
}
response = api_request('POST', 'posts/',
                       data={'posts': [post_data]},
                       params={'source': 'html'})  # Required!

Important: Ghost 5.0+ uses Lexical editor format. The source=html param tells Ghost to convert HTML to Lexical. Without it, post content will be empty!

Fixing Internal Links

If markdown files have internal links like [Title](other-file.md), replace them with Ghost slugs after creating posts:

LINK_MAP = {
    'old-file.md': '/new-ghost-slug/',
}

html = html.replace('href="old-file.md"', 'href="/new-ghost-slug/"')

Safety Features

  • Bulk operations: Preview mode by default

  • Delete operations: Require --confirm flag

  • API versioning: Uses Ghost v5.0 API

  • HTML conversion: Auto-adds source=html param for Ghost 5.0+ compatibility

Testing

Setup Test Environment

cd .claude/skills/ghost-blog/scripts
uv venv
uv pip install -r requirements.txt

Run Tests

cd .claude/skills/ghost-blog/scripts && uv run pytest -v

Run with Coverage

cd .claude/skills/ghost-blog/scripts && uv run pytest -v --cov=. --cov-report=term-missing

Troubleshooting

Error Cause Solution
"No virtual environment found" Missing venv Run uv venv in scripts directory
"Failed to spawn: pytest" Missing deps Run uv pip install -r requirements.txt
"GHOST_URL not set" Missing .env Copy .env.example to .env and configure
"GHOST_ADMIN_KEY invalid format" Wrong key format Key must be id:secret format
"UnauthorizedError" Invalid API key Check key is valid and has permissions
"UPDATE_COLLISION" Post modified Retry operation (auto-refetches updated_at)
Post content empty Missing source=html Ghost 5.0+ requires source=html param
Tag filter returns 0 Using name instead of slug Use tag slug or let script resolve it
HTTP 422 on tags list Invalid params Don't use include=count.posts or order
Links point to .md files Internal markdown links Replace with Ghost slugs after creating posts

References

Weekly Installs
10
First Seen
12 days ago
Installed on
cline10
github-copilot10
codex10
kimi-cli10
gemini-cli10
cursor10