feat: Add OurDigital custom skills package (10 skills)
Complete implementation of OurDigital skills with dual-platform support (Claude Desktop + Claude Code) following standardized structure. Skills created: - 01-ourdigital-brand-guide: Brand reference & style guidelines - 02-ourdigital-blog: Korean blog drafts (blog.ourdigital.org) - 03-ourdigital-journal: English essays (journal.ourdigital.org) - 04-ourdigital-research: Research prompts & workflows - 05-ourdigital-document: Notion-to-presentation pipeline - 06-ourdigital-designer: Visual/image prompt generation - 07-ourdigital-ad-manager: Ad copywriting & keyword research - 08-ourdigital-trainer: Training materials & workshop planning - 09-ourdigital-backoffice: Quotes, proposals, cost analysis - 10-ourdigital-skill-creator: Meta skill for creating new skills Features: - YAML frontmatter with "ourdigital" or "our" prefix triggers - Standardized directory structure (code/, desktop/, shared/, docs/) - Shared environment setup (_ourdigital-shared/) - Comprehensive reference documentation - Cross-skill integration support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
166
custom-skills/02-ourdigital-blog/shared/scripts/ghost_publish.py
Normal file
166
custom-skills/02-ourdigital-blog/shared/scripts/ghost_publish.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ghost CMS Publisher for OurDigital Blog
|
||||
|
||||
Publishes markdown content to Ghost CMS via Admin API.
|
||||
|
||||
Usage:
|
||||
python ghost_publish.py --file post.md --draft
|
||||
python ghost_publish.py --file post.md --publish
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import jwt
|
||||
import time
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(os.path.expanduser("~/.env.ourdigital"))
|
||||
|
||||
|
||||
def create_ghost_token(admin_key: str) -> str:
|
||||
"""Create JWT token for Ghost Admin API."""
|
||||
key_id, secret = admin_key.split(":")
|
||||
|
||||
iat = int(time.time())
|
||||
header = {"alg": "HS256", "typ": "JWT", "kid": key_id}
|
||||
payload = {
|
||||
"iat": iat,
|
||||
"exp": iat + 5 * 60, # 5 minutes
|
||||
"aud": "/admin/"
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, bytes.fromhex(secret), algorithm="HS256", headers=header)
|
||||
return token
|
||||
|
||||
|
||||
def parse_frontmatter(content: str) -> tuple[dict, str]:
|
||||
"""Parse YAML frontmatter from markdown content."""
|
||||
import yaml
|
||||
|
||||
pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
|
||||
match = re.match(pattern, content, re.DOTALL)
|
||||
|
||||
if match:
|
||||
frontmatter = yaml.safe_load(match.group(1))
|
||||
body = match.group(2)
|
||||
return frontmatter, body
|
||||
|
||||
return {}, content
|
||||
|
||||
|
||||
def publish_to_ghost(
|
||||
file_path: str,
|
||||
ghost_url: str,
|
||||
admin_key: str,
|
||||
draft: bool = True
|
||||
) -> dict:
|
||||
"""Publish markdown file to Ghost CMS."""
|
||||
|
||||
# Read and parse file
|
||||
content = Path(file_path).read_text(encoding="utf-8")
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
|
||||
# Create JWT token
|
||||
token = create_ghost_token(admin_key)
|
||||
|
||||
# Prepare post data
|
||||
post_data = {
|
||||
"posts": [{
|
||||
"title": frontmatter.get("title", "Untitled"),
|
||||
"slug": frontmatter.get("slug"),
|
||||
"html": markdown_to_html(body),
|
||||
"meta_description": frontmatter.get("meta_description", ""),
|
||||
"tags": [{"name": tag} for tag in frontmatter.get("tags", [])],
|
||||
"status": "draft" if draft else "published",
|
||||
"authors": [{"email": "andrew.yim@ourdigital.org"}]
|
||||
}]
|
||||
}
|
||||
|
||||
# Add feature image if provided
|
||||
if frontmatter.get("featured_image"):
|
||||
post_data["posts"][0]["feature_image"] = frontmatter["featured_image"]
|
||||
|
||||
# Make API request
|
||||
api_url = f"{ghost_url}/ghost/api/admin/posts/"
|
||||
headers = {
|
||||
"Authorization": f"Ghost {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = requests.post(api_url, json=post_data, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def markdown_to_html(markdown_text: str) -> str:
|
||||
"""Convert markdown to HTML."""
|
||||
try:
|
||||
import markdown
|
||||
return markdown.markdown(
|
||||
markdown_text,
|
||||
extensions=["tables", "fenced_code", "codehilite"]
|
||||
)
|
||||
except ImportError:
|
||||
# Basic conversion if markdown library not available
|
||||
html = markdown_text.replace("\n\n", "</p><p>")
|
||||
html = f"<p>{html}</p>"
|
||||
return html
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Publish to Ghost CMS")
|
||||
parser.add_argument("--file", required=True, help="Markdown file to publish")
|
||||
parser.add_argument("--draft", action="store_true", help="Publish as draft")
|
||||
parser.add_argument("--publish", action="store_true", help="Publish immediately")
|
||||
parser.add_argument("--channel", default="blog",
|
||||
choices=["blog", "journal", "ourstory"],
|
||||
help="Ghost channel to publish to")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get credentials based on channel
|
||||
channel_map = {
|
||||
"blog": ("GHOST_BLOG_URL", "GHOST_BLOG_ADMIN_KEY"),
|
||||
"journal": ("GHOST_JOURNAL_URL", "GHOST_JOURNAL_ADMIN_KEY"),
|
||||
"ourstory": ("GHOST_OURSTORY_URL", "GHOST_OURSTORY_ADMIN_KEY")
|
||||
}
|
||||
|
||||
url_var, key_var = channel_map[args.channel]
|
||||
ghost_url = os.getenv(url_var)
|
||||
admin_key = os.getenv(key_var)
|
||||
|
||||
if not ghost_url or not admin_key:
|
||||
print(f"Error: Missing credentials for {args.channel}")
|
||||
print(f"Set {url_var} and {key_var} in ~/.env.ourdigital")
|
||||
return 1
|
||||
|
||||
try:
|
||||
result = publish_to_ghost(
|
||||
args.file,
|
||||
ghost_url,
|
||||
admin_key,
|
||||
draft=not args.publish
|
||||
)
|
||||
|
||||
post = result["posts"][0]
|
||||
status = "Draft" if not args.publish else "Published"
|
||||
print(f"{status}: {post['title']}")
|
||||
print(f"URL: {ghost_url}/{post['slug']}/")
|
||||
print(f"Edit: {ghost_url}/ghost/#/editor/post/{post['id']}")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user