Install
openclaw skills install @themeix/openclaw-ghost-cmsSupport for Ghost CMS installation, theme development with Handlebars, CLI commands, API integration, theme validation, dynamic routing, deployment, and trou...
openclaw skills install @themeix/openclaw-ghost-cmsA comprehensive skill for Ghost CMS development, theme creation, and management.
This skill provides complete Ghost CMS development support including installation, theme development with Handlebars, Ghost CLI commands, API integration (Content and Admin APIs), GScan validation, custom theme settings, dynamic routing, local development workflows, publishing, and deployment best practices.
Use this skill when you need help with:
# Install Ghost CLI globally
npm install -g ghost-cli@latest
# Install Ghost locally for development
ghost install local
# Install Ghost in production
ghost install
ghost start # Start Ghost
ghost stop # Stop Ghost
ghost restart # Restart Ghost
ghost update # Update Ghost to latest version
ghost ls # List Ghost instances
ghost log # View logs
your-theme/
├── assets/
│ ├── css/
│ ├── fonts/
│ ├── images/
│ └── js/
├── partials/
│ ├── header.hbs
│ └── footer.hbs
├── default.hbs # Base layout
├── index.hbs # Required - post list
├── post.hbs # Required - single post
├── page.hbs # Static pages
├── tag.hbs # Tag archive
├── author.hbs # Author archive
├── error.hbs # Error pages
└── package.json # Required - theme metadata
{{ghost_head}} <!-- In <head> section -->
{{ghost_foot}} <!-- Before </body> -->
{{body_class}} <!-- Dynamic body classes -->
{{asset "path"}} <!-- Link to theme assets -->
{{#post}}...{{/post}} <!-- Post context -->
{{#foreach posts}}...{{/foreach}} <!-- Loop posts -->
{{title}} <!-- Page/post title -->
{{content}} <!-- Post content -->
{{excerpt}} <!-- Post excerpt -->
{{date format="YYYY-MM-DD"}} <!-- Format dates -->
{{img_url feature_image}} <!-- Image URL -->
{{url}} <!-- Current page URL -->
{{navigation}} <!-- Main navigation -->
{{pagination}} <!-- Pagination links -->
<!-- Post-specific: post-my-slug.hbs -->
<!-- Page-specific: page-about.hbs -->
<!-- Tag-specific: tag-javascript.hbs -->
<!-- Author-specific: author-john.hbs -->
{
"name": "your-theme-name",
"version": "1.0.0",
"description": "Brief description",
"license": "MIT",
"author": {
"name": "Your Name",
"email": "your@email.com"
},
"screenshots": {
"desktop": "assets/screenshot-desktop.jpg",
"mobile": "assets/screenshot-mobile.jpg"
},
"config": {
"posts_per_page": 10,
"image_sizes": {
"xxl": { "width": 2000 },
"xl": { "width": 1200 },
"lg": { "width": 1000 },
"md": { "width": 800 },
"sm": { "width": 600 },
"xs": { "width": 300 }
},
"card_assets": true,
"custom": {
"color_scheme": {
"type": "select",
"default": "Light",
"options": ["Light", "Dark", "Auto"]
},
"accent_color": {
"type": "color",
"default": "#3eb0ef"
}
}
}
}
// Install
npm install @tryghost/content-api
// Initialize
const GhostContentAPI = require('@tryghost/content-api');
const api = new GhostContentAPI({
url: 'https://your-site.com',
key: 'content_api_key',
version: 'v5.0'
});
// Fetch posts
api.posts.browse({limit: 5, include: 'tags,authors'})
.then((posts) => {
posts.forEach((post) => console.log(post.title));
});
// Filter posts
api.posts.browse({
filter: 'tag:javascript+featured:true',
limit: 10
});
// Fetch single post
api.posts.read({slug: 'post-slug'});
The Admin API provides comprehensive content management capabilities with three authentication methods:
1. Integration Token Authentication (Recommended for server-side apps)
2. Staff Access Token Authentication (For specific users)
3. User Authentication (For browser apps with login)
| Resource | Methods | Description |
|---|---|---|
/posts/ | Browse, Read, Edit, Add, Copy, Delete | Full post management |
/pages/ | Browse, Read, Edit, Add, Copy, Delete | Static page management |
/tags/ | Browse, Read, Edit, Add, Delete | Tag management |
/tiers/ | Browse, Read, Edit, Add | Membership tier management |
/newsletters/ | Browse, Read, Edit, Add | Newsletter management |
/offers/ | Browse, Read, Edit, Add | Discount offers for members |
/members/ | Browse, Read, Edit, Add | Member management with subscriptions |
/users/ | Browse, Read | User management |
/images/ | Upload | Image upload and storage |
/themes/ | Upload, Activate | Theme management |
/site/ | Read | Site configuration |
/webhooks/ | Edit, Add, Delete | Webhook configuration |
Bash with OpenSSL:
#!/usr/bin/env bash
KEY="YOUR_ADMIN_API_KEY"
IFS=':' read ID SECRET <<< "$KEY"
NOW=$(date +'%s')
FIVE_MINS=$(($NOW + 300))
HEADER="{\"alg\": \"HS256\",\"typ\": \"JWT\", \"kid\": \"$ID\"}"
PAYLOAD="{\"iat\":$NOW,\"exp\":$FIVE_MINS,\"aud\": \"/admin/\"}"
base64_url_encode() {
printf '%s' "${1}" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_'
}
header_base64=$(base64_url_encode "$HEADER")
payload_base64=$(base64_url_encode "$PAYLOAD")
header_payload="${header_base64}.${payload_base64}"
signature=$(printf '%s' "${header_payload}" | openssl dgst -binary -sha256 -mac HMAC -macopt hexkey:$SECRET | base64_url_encode)
TOKEN="${header_payload}.${signature}"
curl -H "Authorization: Ghost $TOKEN" -H "Content-Type: application/json" \
-d '{"posts":[{"title":"Hello world"}]}' \
"http://localhost:2368/ghost/api/admin/posts/"
JavaScript (without client library):
const jwt = require('jsonwebtoken');
const axios = require('axios');
const key = 'YOUR_ADMIN_API_KEY';
const [id, secret] = key.split(':');
const token = jwt.sign({}, Buffer.from(secret, 'hex'), {
keyid: id,
algorithm: 'HS256',
expiresIn: '5m',
audience: `/admin/`
});
const url = 'http://localhost:2368/ghost/api/admin/posts/';
const headers = { Authorization: `Ghost ${token}` };
const payload = { posts: [{ title: 'Hello World' }] };
axios.post(url, payload, { headers })
.then(response => console.log(response))
.catch(error => console.error(error));
Python:
import requests
import jwt
from datetime import datetime
key = 'YOUR_ADMIN_API_KEY'
id, secret = key.split(':')
iat = int(datetime.now().timestamp())
header = {'alg': 'HS256', 'typ': 'JWT', 'kid': id}
payload = {
'iat': iat,
'exp': iat + 5 * 60,
'aud': '/admin/'
}
token = jwt.encode(payload, bytes.fromhex(secret), algorithm='HS256', headers=header)
url = 'http://localhost:2368/ghost/api/admin/posts/'
headers = {'Authorization': 'Ghost {}'.format(token)}
body = {'posts': [{'title': 'Hello World'}]}
r = requests.post(url, json=body, headers=headers)
print(r)
Create Member:
api.members.add({
email: 'user@example.com',
name: 'Jamie',
labels: [{ name: 'VIP', slug: 'vip' }],
newsletters: [{ id: 'newsletter_id' }]
});
Update Member:
api.members.edit({
id: 'member_id',
name: 'Updated Name',
note: 'VIP customer'
});
Member Fields:
email (required)namenotelabels (array with name and slug)newsletters (array of IDs)status (free, paid, comped)Create Newsletter:
api.newsletters.add({
name: 'My Newsletter',
description: 'Description',
sender_reply_to: 'newsletter', // or 'support'
status: 'active',
subscribe_on_signup: true,
show_header_icon: true,
show_header_title: true,
title_font_category: 'sans_serif',
title_alignment: 'center',
show_feature_image: true,
body_font_category: 'sans_serif',
show_badge: true
});
Newsletter Fields:
name (required)descriptionstatus (active, archived)sender_namesender_email (requires verification)sender_reply_to (newsletter, support)subscribe_on_signupheader_image (1200x600 recommended)show_header_iconshow_header_titleshow_header_nametitle_font_category (serif, sans_serif)title_alignmentshow_feature_imagebody_font_category (serif, sans_serif)footer_contentshow_badgeCreate Offer:
api.offers.add({
name: 'Black Friday',
code: 'black-friday',
display_title: 'Black Friday Sale!',
display_description: '10% off on yearly plan',
type: 'percent', // or 'fixed'
cadence: 'year', // or 'month'
amount: 12, // percentage or cents
duration: 'once', // 'once', 'forever', 'repeating'
tier: { id: 'tier_id' },
status: 'active'
});
Offer Fields:
name (required, unique)code (required, short code)display_titledisplay_descriptiontype (percent, fixed)cadence (year, month)amount (in smallest currency unit)duration (once, forever, repeating)duration_in_months (for repeating)currency (required for fixed type)status (active, archived)redemption_counttier (tier object with id and name)Control who sees specific cards in posts:
// Card with visibility controls
const postWithVisibility = {
title: 'Premium Content',
lexical: {
root: {
type: 'root',
children: [
{
type: 'html',
html: '<div>Premium analysis...</div>',
visibility: {
web: {
nonMember: false,
memberSegment: 'status:free,status:-free'
},
email: {
memberSegment: 'status:free'
}
}
}
]
}
}
};
Visibility Options:
web.nonMember: true/false (visible to non-members)web.memberSegment: NQL filter (e.g., "status:free" for all members, "status:-free" for paid only)email.memberSegment: NQL filter for email deliveryCommon Configurations:
Visible to all members (free and paid), hidden from non-members:
{
"visibility": {
"web": { "nonMember": false, "memberSegment": "status:free,status:-free" },
"email": { "memberSegment": "status:free,status:-free" }
}
}
Visible to paid members only:
{
"visibility": {
"web": { "nonMember": false, "memberSegment": "status:-free" },
"email": { "memberSegment": "status:-free" }
}
}
Upload Image:
const FormData = require('form-data');
const fs = require('fs');
const form = new FormData();
form.append('file', fs.createReadStream('/path/to/image.jpg'));
form.append('ref', 'path/to/image.jpg');
form.append('purpose', 'image'); // 'image', 'profile_image', or 'icon'
fetch('http://localhost:2368/ghost/api/admin/images/upload/', {
method: 'POST',
headers: {
'Authorization': `Ghost ${token}`,
'Accept-Version': 'v5.0'
},
body: form
})
.then(res => res.json())
.then(data => console.log(data));
Image Upload Fields:
file (required) - Blob or File objectpurpose - 'image' (default), 'profile_image', or 'icon'ref (optional) - Reference identifierSupported Formats:
image, icon: WEBP, JPEG, GIF, PNG, SVGprofile_image: Must be square, WEBP, JPEG, GIF, PNG, SVGInstallation:
npm install @tryghost/admin-api
# or
yarn add @tryghost/admin-api
Usage:
const GhostAdminAPI = require('@tryghost/admin-api');
const api = new GhostAdminAPI({
url: 'http://localhost:2368/',
key: 'YOUR_ADMIN_API_KEY',
version: "v6.0"
});
// Browse posts
api.posts.browse();
// Read post
api.posts.read({id: 'abcd1234'});
// Add post
api.posts.add({
title: 'My first draft API post',
lexical: '{"root":{"children":[...]}}' // Lexical JSON structure
});
// Edit post
api.posts.edit({
id: 'abcd1234',
title: 'Renamed my post',
updated_at: post.updated_at
});
// Delete post
api.posts.delete({id: 'abcd1234'});
// Upload image
api.images.upload({
file: '/path/to/local/file'
});
Complete Publishing Example with Image Processing:
const GhostAdminAPI = require('@tryghost/admin-api');
const path = require('path');
const fs = require('fs');
const api = new GhostAdminAPI({
url: 'http://localhost:2368/',
version: "v6.0",
key: 'YOUR_ADMIN_API_KEY'
});
function processImagesInHTML(html) {
const imageRegex = /="([^"]*?(?:\.jpg|\.jpeg|\.gif|\.png|\.svg|\.webp))"/gmi;
const imagePromises = [];
let result;
while((result = imageRegex.exec(html)) !== null) {
const file = result[1];
imagePromises.push(api.images.upload({
ref: file,
file: path.resolve(file)
}));
}
return Promise.all(imagePromises)
.then(images => {
images.forEach(image => {
html = html.replace(image.ref, image.url);
});
return html;
});
}
let html = '<p>My test post content.</p><figure><img src="/path/to/my/image.jpg" /><figcaption>My awesome photo</figcaption></figure>';
processImagesInHTML(html)
.then(html => {
return api.posts.add(
{title: 'My Test Post', html},
{source: 'html'}
);
})
.then(res => console.log(JSON.stringify(res)))
.catch(err => console.log(err));
Authentication
Pagination
page and limit parametersmeta.pagination for navigationError Handling
meta for validation errorsRate Limiting
Versioning
Accept-Version headerconst GhostAdminAPI = require('@tryghost/admin-api');
const api = new GhostAdminAPI({
url: 'https://your-site.com',
key: 'admin_api_key',
version: 'v5.0'
});
// Create post
const post = await api.posts.add({
title: 'My New Post',
html: '<p>Content</p>',
status: 'published'
});
// Update post
await api.posts.edit({
id: post.id,
title: 'Updated Title'
});
Define in package.json:
{
"config": {
"custom": {
"show_featured_posts": {
"type": "boolean",
"default": true
},
"posts_per_page": {
"type": "select",
"default": "6",
"options": ["3", "6", "9", "12"]
}
}
}
}
Use in templates:
{{#if @custom.show_featured_posts}}
<section class="featured">
{{#get "posts" filter="featured:true" limit="3"}}
{{#foreach posts}}
<article>{{title}}</article>
{{/foreach}}
{{/get}}
</section>
{{/if}}
# Install GScan
npm install -g gscan
# Validate theme
gscan your-theme-directory
# Validate with specific Ghost version
gscan your-theme-directory --version 5.0
# Output as JSON
gscan your-theme-directory --format json
# Online validation
# Visit https://gscan.ghost.org and upload theme zip
Common validation checks:
routes:
/about/:
template: about
/blog/:
template: blog
collections:
/blog/:
permalink: /blog/{slug}/
template: blog-post
filter: tag:blog
taxonomies:
tag: /tag/{slug}/
author: /author/{slug}/
# Start Ghost locally
ghost start
# Access admin at http://localhost:2368/ghost
# Site at http://localhost:2368/
# Development cycle:
# 1. Edit templates in content/themes/your-theme/
# 2. Edit CSS/JS in assets/
# 3. Restart for HBS changes: ghost restart
# 4. CSS/JS changes: instant refresh
# 5. Test at http://localhost:2368/
# 6. Validate with GScan
# Create theme zip (exclude unnecessary files)
cd content/themes/your-theme
zip -r ../your-theme.zip . -x "node_modules/*" ".git/*"
# Upload via Ghost Admin
# Settings > Design > Upload theme
# Edit Ghost config
ghost config
# Set URL
ghost config url https://your-site.com
# Set mail configuration
ghost config mail {transport, options}
# Restart after config changes
ghost restart
# Full production stack (Nginx, SSL, Systemd)
ghost install
# With custom database
ghost install --db=mysql --dbhost=localhost --dbuser=ghost --dbpass=password --dbname=ghost_prod
ghost update# Check Ghost status
ghost ls
# Restart Ghost
ghost restart
# View logs
ghost log
{{#if}}...{{/if}}web-development, cms, theming, handlebars, blogging, publishing
Published by Themeix
MIT
1.0.0