This skill should be used when the user wants to interact with a Ghost blog via its Content and Admin APIs. Relevant when the user says things like 'list my blog posts', 'create a new draft', 'publish my draft', 'schedule a post for tomorrow', 'upload an image to my blog', 'manage blog tags', 'show my Ghost site info', 'filter posts by tag', 'delete a post', 'list members', 'send a newsletter', or any Ghost blog management task involving posts, pages, tags, members, newsletters, tiers, or images.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
This skill should be used when the user wants to interact with a Ghost blog via its Content and Admin APIs. Relevant when the user says things like 'list my blog posts', 'create a new draft', 'publish my draft', 'schedule a post for tomorrow', 'upload an image to my blog', 'manage blog tags', 'show my Ghost site info', 'filter posts by tag', 'delete a post', 'list members', 'send a newsletter', or any Ghost blog management task involving posts, pages, tags, members, newsletters, tiers, or images.
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.
Every script follows this pattern (use the base directory shown when this skill is loaded as the PYTHONPATH):
PYTHONPATH=<base_directory_of_this_skill> python3 << 'PY'
from ghost_api import Ghost
g = Ghost()
# ... operations here ...
PY
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")
# 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.
Unsplash feature images: Use the built-in helpers to search Unsplash and set feature images with proper attribution in one call:
results = g.unsplash_search("abstract flowing lines", per_page=5)
for r in results:
print(f"{r['id']}{r['width']}x{r['height']} by {r['user_name']} plus={r['is_plus']}")
# Set feature image with auto-generated attribution caption
post = g.set_unsplash_feature_image("POST_ID", results[0]["id"])
To build a caption without setting the feature image (e.g. for manual use):
caption = g.unsplash_caption(photo_id="abc123")
# or skip the API call if you already have user info from search results:
caption = g.unsplash_caption(user_name="Author Name", user_username="author")
Embedding YouTube Videos (Native Lexical Cards)
Prefer native Lexical embed cards over raw HTML iframes. Native embeds render correctly in Ghost's editor and frontend without sizing issues.
To embed a YouTube video, fetch oembed data and write a Lexical embed node directly:
import json
import urllib.request
# Step 1: Fetch oembed metadata from YouTube
video_url = "https://www.youtube.com/watch?v=VIDEO_ID"
oembed_api = f"https://www.youtube.com/oembed?url={video_url}&format=json"with urllib.request.urlopen(oembed_api) as resp:
oembed = json.loads(resp.read())
# Step 2: GET the post as Lexical
post = g.get("admin/posts/POST_ID", formats="lexical")["posts"][0]
lexical = json.loads(post["lexical"])
# Step 3: Build the native embed node
embed_node = {
"type": "embed",
"version": 1,
"url": video_url,
"embedType": "video",
"html": oembed["html"],
"metadata": oembed
}
# Step 4: Insert or replace a node in the children array
lexical["root"]["children"].insert(1, embed_node) # or replace: lexical["root"]["children"][N] = embed_node# Step 5: PUT with Lexical JSON (no source="html")
g.put("admin/posts/POST_ID",
{"posts": [{
"lexical": json.dumps(lexical),
"updated_at": post["updated_at"],
}]})
This works for any oembed provider (YouTube, Vimeo, Twitter, etc.). The key differences from HTML-based updates:
Use formats="lexical" when reading (not formats="html")
Send "lexical": json.dumps(...) in the PUT body (not "html": ...)
Do NOT pass source="html" when updating via Lexical
Other Resources
Tags
g.get("admin/tags", limit="all") # List
g.post("admin/tags", {"tags": [{"name": "New Tag", "description": "..."}]}) # Create
g.delete("admin/tags/TAG_ID") # Delete
Internal tags use a # prefix in the name.
Pages
Same as posts: replace posts with pages in all paths.
g.get("admin/newsletters") # List# Send post as email via newsletter
g.put("admin/posts/POST_ID",
{"posts": [{"status": "published", "updated_at": post["updated_at"]}]},
newsletter="newsletter-slug")
Tiers, Site Info, Users
g.get("admin/tiers", include="monthly_price,yearly_price,benefits")
g.get("content/settings") # Public settings
g.get("admin/site") # Admin site info
g.get("admin/users", include="roles,count.posts")
Use meta.pagination to handle paging. Use limit=all sparingly; prefer pagination for large datasets.
Error Handling
The Ghost class raises exceptions with HTTP status and Ghost's error message on failure:
401 Unauthorized: JWT expired or invalid (auto-generated per request, so usually means bad key)
404 Not Found: Resource ID or slug doesn't exist
422 Validation Error: Missing updated_at, invalid field, etc.
429 Too Many Requests: Rate limited, wait and retry
Markdown Workflow
For editing posts as markdown instead of HTML, use ghost_md.py (requires uv):
# Pull a post as markdown
<base_directory_of_this_skill>/ghost_md.py pull POST_ID /tmp/post.md
# Edit the markdown file, then push it back
<base_directory_of_this_skill>/ghost_md.py push POST_ID /tmp/post.md
This is the preferred workflow for editing post content. Pull the post as markdown, edit the .md file using standard text editing tools, then push it back. Ghost receives HTML converted from the markdown.
For creating new posts or updating metadata (title, tags, status, feature image, etc.), use the ghost_api.py module directly.
Choosing the Right Content Format
Approach
When to use
HTML with source="html"
Default for creating/updating posts. Simple and works for most content.
Markdown workflow (ghost_md.py)
Preferred for editing existing post content.
Lexical JSON
Native embed cards (YouTube, Twitter, bookmarks) or surgical edits to specific nodes. Use formats="lexical" to read, send "lexical": json.dumps(...) to write, and do NOT pass source="html".
HTML cards
When Ghost's HTML-to-Lexical conversion causes formatting issues (e.g., converting bold paragraphs back to headings).
HTML Cards (Raw HTML Escape Hatch)
When source="html" causes Ghost to reinterpret your markup (changing heading levels, merging paragraphs, etc.), wrap content in HTML card markers to preserve it exactly:
<!--kg-card-begin: html--><divclass="custom-section"><p><strong>Bold title that stays a paragraph</strong></p><p>Supporting text that won't be merged or reformatted.</p></div><!--kg-card-end: html-->
Ghost stores this as a single opaque HTML card and won't convert it to Lexical nodes. Use inline <style> blocks inside the card for custom styling. Note that HTML card content is not editable in Ghost's visual editor (it shows as a code block).
Guidelines
Always use the PYTHONPATH=... python3 << 'PY' pattern for all Ghost API operations
Use ghost_md.py pull/push for editing post content as markdown
Use the Admin API for writes and drafts; Content API for simple public reads
Always GET before PUT to obtain updated_at for collision detection
Confirm destructive actions (delete, unpublish) with the user first
Use source="html" when creating/updating posts with HTML content
Use formats="html" when reading posts via Admin API (defaults to Lexical only)
Use include="tags,authors" when listing posts for full context
Always set custom_excerpt when creating a new post. Ghost uses it for cards, social previews, and SEO descriptions. After pushing content, ask the user for an excerpt if one wasn't provided, or draft one and confirm before setting it.
Always check custom_excerpt when updating a post's content or title. If the post content or title changes significantly (e.g. translation, rewrite, new topic), the excerpt likely needs updating too. After any major edit, verify the excerpt still matches the current content and update it if not.