Skip to content
Content
Skill

/ghost-blog

Manage Ghost CMS blogs through the Content and Admin APIs. Use for reading, creating, editing, publishing, or scheduling posts and pages, uploading images, and managing tags, members, and newsletters.

From plugin
ghost-agent-tools
131 skill

Claude Code

$ npx -y skills add georgeguimaraes/ghost-agent-tools --skill ghost-blog --agent claude-code

SKILL.md

ghost-blog.SKILL.md
name: ghost-blog
description: "Manage Ghost CMS blogs through the Content and Admin APIs. Use for reading, creating, editing, publishing, or scheduling posts and pages, uploading images, and managing tags, members, and newsletters."

Ghost Blog Management

Interact with the user's Ghost blog using the Content API (read-only, public data) and Admin API (full read/write access).

Environment Variables

| Variable | Purpose | |----------|---------| | `GHOST_API_URL` | Base URL of the Ghost instance (e.g. `https://myblog.ghost.io`) | | `GHOST_CONTENT_API_KEY` | Content API key for read-only public access | | `GHOST_ADMIN_API_KEY` | Admin API key in `{id}:{secret}` format for full access |

These are created in Ghost Admin under Settings > Integrations > Custom Integration.

The `ghost_api.py` Module

All operations use the `Ghost` class from `ghost_api.py` (stdlib only, no dependencies). It handles JWT auth, JSON serialization, and HTTP requests. The module lives in the same directory as this skill file.

Requires [uv](https://docs.astral.sh/uv/). Resolve `GHOST_SKILL_DIR` to the absolute directory containing this `SKILL.md`, wherever the skill is installed. Both Python helpers live beside it. Use that path rather than the working directory:

GHOST_SKILL_DIR="/absolute/path/to/ghost-blog"
PYTHONPATH="$GHOST_SKILL_DIR" uv run --no-project python - << 'PY'
from ghost_api import Ghost

g = Ghost()
# ... operations here ...
PY

API Methods

| Method | Signature | Description | |--------|-----------|-------------| | `g.get(path, **params)` | Returns dict | GET request, params become query string | | `g.post(path, data, **params)` | Returns dict | POST with JSON body | | `g.put(path, data, **params)` | Returns dict | PUT with JSON body | | `g.delete(path)` | Returns None | DELETE request | | `g.upload(file_path, ref=None)` | Returns URL string | Multipart image upload | | `g.unsplash_search(query, orientation="landscape", per_page=10)` | Returns list of dicts | Search Unsplash photos | | `g.unsplash_caption(photo_id=None, user_name=None, user_username=None)` | Returns HTML string | Build Unsplash attribution caption | | `g.set_unsplash_feature_image(post_id, photo_id)` | Returns post dict | Set feature image from Unsplash photo ID |

**Path convention**: paths start with `content/` or `admin/` (e.g. `content/posts`, `admin/posts/abc123`). Auth is handled automatically based on prefix.

Common Operations

List Posts

# Public posts
posts = g.get("content/posts", include="tags,authors", limit=15)

# All posts including drafts
posts = g.get("admin/posts", include="tags,authors", formats="html", limit=15)

for p in posts["posts"]:
    date = (p.get("published_at") or "(draft)")[:10]
    tags = ", ".join(t["name"] for t in p.get("tags", []))
    print(f"  {date}  {p['title']}  {tags}")
print(f"Total: {posts['meta']['pagination']['total']}")

Filter and Search Posts

Pass `filter` as a query param using NQL syntax:

# By tag
posts = g.get("content/posts", filter="tag:my-tag", include="tags")

# Drafts only (Admin API)
drafts = g.get("admin/posts", filter="status:draft", formats="html")

# Last 7 days
recent = g.get("admin/posts", filter="published_at:>now-7d", formats="html")

# Combined: published + specific tag (+ is AND, comma is OR)
posts = g.get("admin/posts", filter="status:published+tag:news", formats="html")

**NQL operators**: `:` (equals), `-` (not), `>` `>=` `<` `<=` (comparison), `~` (contains), `[a,b]` (in), `+` (AND), `,` (OR), `()` (grouping). Wrap dates/special chars in single quotes.

Read a Single Post

# By ID
post = g.get("admin/posts/POST_ID", formats="html", include="tags,authors")["posts"][0]

# By slug (Content API)
post = g.get("content/posts/slug/my-post-slug", include="tags,authors")["posts"][0]

Create a Post

Use `g.create_post()` which automatically adds `source=html` when HTML content is present (Ghost v5+ requires this to convert HTML to its internal Lexical format; without it, content will be empty):

post = g.create_post({
    "title": "My New Post",
    "html": "<p>Post content in HTML.</p>",
    "status": "draft",
    "tags": [{"name": "Tag Name"}],
    "custom_excerpt": "A short excerpt.",
})
print(f"Created: {post['title']} (ID: {post['id']})")

**Status options**: `draft` (default), `published`, `scheduled` (requires `published_at`).

Tags that don't exist are created automatically. To preserve raw HTML blocks, wrap in `<!--kg-card-begin: html-->` and `<!--kg-card-end: html-->`.

Update a Post

Use `g.update_post()` which fetches `updated_at` automatically and adds `source=html` when HTML content is present. Tags and authors are **replaced entirely** on update, so send the complete desired list.

post = g.update_post("POST_ID", {
    "title": "Updated Title",
    "html": "<p>Updated content.</p>",
})

If you already have `updated_at` from a previous GET, pass it to skip the extra request:

post = g.update_post("POST_ID", {
    "html": "<p>Updated content.</p>",
}, updated_at=existing_post["updated_at"])

Publish a Draft

post = g.get("admin/posts/POST_ID")["posts"][0]
g.put("admin/posts/POST_ID",
    {"posts": [{"status": "published", "updated_at": post["updated_at"]}]})

Schedule a Post

post = g.get("admin/posts/POST_ID")["posts"][0]
g.put("admin/posts/POST_ID",
    {"posts": [{
        "status": "scheduled",
        "published_at": "2026-03-15T11:00:00.000Z",
        "updated_at": post["updated_at"],
    }]})

Delete a Post

**Always confirm with the user before deleting.**

g.delete("admin/posts/POST_ID")

Upload an Image

url = g.upload("/path/to/image.jpg")
print(url)  # https://myblog.ghost.io/content/images/2026/02/image.jpg

Supported formats: JPEG, PNG, GIF, WEBP, SVG.

Insert an Image into Post Content

After uploading, use this HTML to embed

Read more
@georgeguimaraes
Ships withghost-agent-tools

A skill for managing Ghost blogs from Codex, Claude Code, and other agents that support Agent Skills.

Stars on GitHub

Momentum

stars in total
13
Star history of georgeguimaraes/claude-code-ghost on GitHub, a chart by star-history.com

More skills from other plugins.

prompt-lookup

prompt-lookup

Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use…

@gh-f@gh-fInstall Skill
ppt-master

ppt-master

AI-driven presentation workflow for generating editable PPTX decks and slides, reconstructing page visuals, creating reusable…

@hugohe3@hugohe3Install Skill
morph-ppt

morph-ppt

Use this skill when the user wants a .pptx with smooth cross-slide animation — PowerPoint Morph transitions, Keynote-style…

officecli13d33k
@iofficeai@iofficeaiInstall Skill

Questions about ghost-blog.

Which coding agents can use ghost-blog?
ghost-blog installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.