Publishing phase - publish articles to WordPress, Ghost, generate images, and send webhooks.
Install
npx skillscat add egebese/suparank/suparank-publish Install via the SkillsCat registry.
Suparank Publishing Phase
You handle publishing content to CMS platforms, generating images, and sending webhooks. All publishing uses curl commands via the Bash tool.
Before Starting
- Read credentials from
~/.claude/suparank-credentials.json - If it doesn't exist, check the legacy path
~/.suparank/credentials.jsonas fallback - If neither exists, tell the user:
- "No publishing credentials found. Run
/suparank/setupto configure WordPress, Ghost, or image generation." - They can still save content locally without credentials.
- "No publishing credentials found. Run
- Read the project config from
.claude/suparank.jsonfor site context
Detect Publishing Action
Publish to WordPress
Triggers: "publish to WordPress", "post to WordPress", "send to WordPress"
Requirements: ~/.claude/suparank-credentials.json must have a wordpress section with:
site_url: WordPress site URLusername: WordPress usernameapp_password: WordPress application password
Steps:
Get the article to publish:
- Read from
.claude/suparank-session.jsonfor the latest saved article - Or let the user specify which article (by number or title)
- Read the article content from
.claude/suparank-content/[folder]/article.md - Read metadata from
metadata.json
- Read from
Convert markdown content to HTML:
- Convert the markdown article to clean HTML
- Preserve heading hierarchy (h1, h2, h3)
- Convert lists, bold, italic, links, code blocks
- Convert images to
<img>tags
Publish via WordPress REST API using curl:
curl -s -X POST "${site_url}/wp-json/wp/v2/posts" \
-H "Authorization: Basic $(echo -n '${username}:${app_password}' | base64)" \
-H "Content-Type: application/json" \
-d '{
"title": "Article Title",
"content": "<html content>",
"status": "draft",
"excerpt": "Meta description"
}'Parse the response to get:
- Post ID
- Post URL
- Edit URL
If a featured image URL is available (from image generation), set it:
- First, upload the image via media endpoint
- Then set as featured_media on the post
Report success:
- "Published to WordPress as draft!"
- "Title: [title]"
- "URL: [url]"
- "Edit: [edit_url]"
Update session: mark article as published_to: ["wordpress"]
Status options: Ask the user: "Publish as draft (default) or live?"
draft(default, safer)publish(immediately live)
Publish to Ghost
Triggers: "publish to Ghost", "post to Ghost", "send to Ghost"
Requirements: ~/.claude/suparank-credentials.json must have a ghost section with:
api_url: Ghost site API URLadmin_api_key: Ghost Admin API key (format:id:secret)
Steps:
Get the article (same as WordPress flow above)
Convert markdown to HTML
Generate a Ghost Admin API JWT token using a Node.js one-liner:
TOKEN=$(node -e "
const c = require('crypto');
const [id, secret] = '${admin_api_key}'.split(':');
const h = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT',kid:id})).toString('base64url');
const now = Math.floor(Date.now()/1000);
const p = Buffer.from(JSON.stringify({iat:now,exp:now+300,aud:'/admin/'})).toString('base64url');
const sig = c.createHmac('sha256', Buffer.from(secret,'hex')).update(h+'.'+p).digest('base64url');
console.log(h+'.'+p+'.'+sig);
")- Create the Ghost mobiledoc payload:
{
"posts": [{
"title": "Article Title",
"mobiledoc": "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[[\"html\",{\"html\":\"<html content>\"}]],\"markups\":[],\"sections\":[[10,0]]}",
"status": "draft",
"tags": [{"name": "keyword1"}, {"name": "keyword2"}],
"feature_image": "image_url_if_available"
}]
}- Publish via Ghost Admin API:
curl -s -X POST "${api_url}/ghost/api/admin/posts/" \
-H "Authorization: Ghost ${TOKEN}" \
-H "Content-Type: application/json" \
-d '${post_data}'- Parse response and report success
- Update session: mark article as published_to: ["ghost"]
Generate Images
Triggers: "generate images", "create images", "generate hero image", "make images for article"
Requirements: ~/.claude/suparank-credentials.json must have image provider config:
image_provider: "fal" (currently the only supported provider)fal.api_key: fal.ai API key
If image_provider is missing or set to an unsupported value (anything other than "fal"):
- Skip image generation with message: "Skipped images (unsupported provider '[value]', only 'fal' is currently supported)"
- Do NOT block the rest of publishing
Steps:
- Read the article from session to understand content
- Read image prompt template from
~/.claude/skills/suparank/templates/image-prompt-designer.md - Generate image prompts based on article content and visual style from config
- For each image, call fal.ai via curl:
curl -s -X POST "https://fal.run/fal-ai/flux/schnell" \
-H "Authorization: Key ${fal_api_key}" \
-H "Content-Type: application/json" \
-d '{
"prompt": "detailed image prompt here",
"image_size": "landscape_16_9",
"num_images": 1
}'- Parse response to get image URL from
result.images[0].url - Store image URLs in session metadata
- Report: "Generated [N] images. Cover: [url]. Ready to publish."
Image count calculation:
- 1 cover/hero image (always)
- 1 inline image per 300 words of content (if config.content.include_images is true)
Send Webhook
Triggers: "send webhook", "notify Slack", "send to Make", "trigger webhook"
Requirements: ~/.claude/suparank-credentials.json must have webhooks section
Steps:
- Determine webhook type: slack / make / n8n / zapier / default
- Build the payload:
For Slack:
curl -s -X POST "${slack_url}" \
-H "Content-Type: application/json" \
-d '{"text": "New article published: [title]\n[url]"}'For Make/n8n/Zapier:
curl -s -X POST "${webhook_url}" \
-H "Content-Type: application/json" \
-d '{
"source": "suparank",
"timestamp": "2026-02-23T10:30:00Z",
"title": "Article Title",
"url": "https://...",
"keywords": ["keyword1", "keyword2"]
}'- Report success: "Webhook sent to [type]!"
Batch Publishing
When publishing multiple articles (from multi-article pipeline):
- Read all articles from session
- Publish each one sequentially
- Track progress: "Publishing article 1 of 3..."
- Report summary: "Published 3 articles to WordPress as drafts."
When Called from Pipeline
When invoked by the pipeline orchestrator:
- Generate images first (if enabled and credentials available)
- Then publish to all configured platforms
- Use draft status by default
- Report results but don't ask questions - the pipeline is automated
Graceful Degradation
If credentials are missing for a specific platform:
- Skip that platform silently
- Report which platforms were skipped and why
- Never fail the entire publish phase because one platform is missing
Example: "Published to WordPress. Skipped Ghost (not configured). Skipped images (no fal.ai key)."
Image Generation Error Handling
If image generation fails, handle gracefully:
- Timeout (>60s): "Image generation timed out. Publishing without images."
- Rate limiting: "Image provider rate limited. You can retry with
/suparank/publishlater." - Invalid prompt: Try a simplified prompt. If it still fails, skip that image.
- API key invalid: "fal.ai API key is invalid. Update with
/suparank/setup."
Never block the entire publishing phase because image generation failed.
Security Notes
- Credentials are read from
~/.claude/suparank-credentials.json(or legacy~/.suparank/credentials.json) - NEVER log or display full API keys, passwords, or tokens in output
- When showing curl commands to the user, ALWAYS mask sensitive values:
- API keys: show first 4 chars only →
sk_l**** - Passwords: replace entirely →
**** - Tokens: show first 10 chars →
eyJhbGci... - Example:
Authorization: Basic ****(never the actual base64 string)
- API keys: show first 4 chars only →
- Application passwords are safer than regular WordPress passwords
- If an error message contains credentials, sanitize before displaying