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
121 skill
Install
$ npx -y skills add georgeguimaraes/claude-code-ghost --skill ghost-blog --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/ghost-blog

Context preview

The summary Claude sees to decide when to auto-load this skill.

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.

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
Ships withghost-agent-tools

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

Get the whole plugin
Stats
12
Stars
1
Forks
Active
Maintenance
Python
Language
MIT
License
1d ago
Last commit
7mo ago
Created

Repo: georgeguimaraes/claude-code-ghost