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:
@@ -0,0 +1,149 @@
|
||||
# OurDigital Blog Style Guide
|
||||
|
||||
Detailed writing guidelines for blog.ourdigital.org.
|
||||
|
||||
## Channel Identity
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Domain** | blog.ourdigital.org |
|
||||
| **Tagline** | 사람, 디지털 그리고 문화 |
|
||||
| **Language** | Korean (전문용어 영문 병기) |
|
||||
| **Tone** | Analytical & Personal, Educational |
|
||||
| **Target** | 교양 있는 일반 독자 - 기술의 문화적 영향에 호기심 있는 독자 |
|
||||
|
||||
## Writing Characteristics
|
||||
|
||||
### 1. 철학-기술 융합체
|
||||
|
||||
기술 분석과 실존적 질문을 자연스럽게 결합한다.
|
||||
|
||||
**Good Example:**
|
||||
> AI가 우리의 업무를 대체할 수 있다는 사실은 분명하다. 그러나 더 중요한 질문은 "AI가 대체할 수 없는 것은 무엇인가?"이다.
|
||||
|
||||
**Bad Example:**
|
||||
> AI는 업무 효율성을 높여준다. 다양한 분야에서 활용되고 있다.
|
||||
|
||||
### 2. 역설(Paradox) 활용
|
||||
|
||||
논증을 긴장과 모순 구조로 전개한다.
|
||||
|
||||
**Paradox Patterns:**
|
||||
- "~하면서 동시에 ~하다"
|
||||
- "~인 것 같지만 실은 ~이다"
|
||||
- "~를 얻었지만 ~를 잃었다"
|
||||
|
||||
### 3. 수사적 질문
|
||||
|
||||
선언적 권위보다 질문을 통한 참여를 선호한다.
|
||||
|
||||
**Good:**
|
||||
> 우리는 정말 데이터를 이해하고 있는 것일까?
|
||||
|
||||
**Bad:**
|
||||
> 데이터를 이해하는 것이 중요하다.
|
||||
|
||||
### 4. 우울한 낙관주의
|
||||
|
||||
불안과 상실을 인정하되 절망하지 않는다.
|
||||
|
||||
**Tone Spectrum:**
|
||||
```
|
||||
비관 ←――――――――――――――――――→ 낙관
|
||||
↑
|
||||
우울한 낙관주의
|
||||
(여기에 위치)
|
||||
```
|
||||
|
||||
## 문장 구조
|
||||
|
||||
| Element | Pattern |
|
||||
|---------|---------|
|
||||
| 문장 길이 | 긴 복합문 허용 - 상호연결된 개념 반영 |
|
||||
| 단락 구조 | 관찰 → 분석 → 철학적 함의 |
|
||||
| 근거 제시 | 역사적 사례 + 기술 명세 + 문화적 참조 |
|
||||
| 결론 | 열린 결말, 답보다 질문 |
|
||||
|
||||
## 언어 규칙
|
||||
|
||||
### 전문용어 병기
|
||||
|
||||
```
|
||||
✓ 검색엔진최적화(SEO)
|
||||
✓ 핵심성과지표(KPI)
|
||||
✓ 고객관계관리(CRM)
|
||||
✗ SEO (첫 등장 시 한글 없이)
|
||||
✗ 서치엔진옵티마이제이션
|
||||
```
|
||||
|
||||
### 외래어 표기
|
||||
|
||||
- 영문 브랜드/제품명: 원어 유지 (Google, ChatGPT)
|
||||
- 일반 외래어: 한글화 (데이터, 마케팅, 콘텐츠)
|
||||
|
||||
## 포스트 구조
|
||||
|
||||
### 도입부 (10-15%)
|
||||
|
||||
1. **Hook**: 독자의 관심을 끄는 질문/통계/역설
|
||||
2. **Context**: 주제의 배경 설명
|
||||
3. **Preview**: 글에서 다룰 내용 암시
|
||||
|
||||
### 본론 (70-80%)
|
||||
|
||||
3-5개의 핵심 포인트, 각각:
|
||||
1. **주장**: 명확한 포인트 제시
|
||||
2. **근거**: 데이터, 사례, 전문가 의견
|
||||
3. **함의**: 이것이 의미하는 바
|
||||
|
||||
### 결론 (10-15%)
|
||||
|
||||
1. **Summary**: 핵심 내용 요약
|
||||
2. **Reflection**: 더 넓은 맥락에서의 의미
|
||||
3. **Open Question**: 독자가 생각할 질문
|
||||
|
||||
## SEO Guidelines
|
||||
|
||||
### Title (제목)
|
||||
|
||||
- 60자 이내
|
||||
- 핵심 키워드 포함
|
||||
- 호기심 유발 또는 가치 제안
|
||||
|
||||
**Patterns:**
|
||||
- "[주제]의 역설: ~하면서 ~하는 시대"
|
||||
- "[주제]를 다시 생각한다"
|
||||
- "왜 [주제]가 중요한가"
|
||||
|
||||
### Meta Description
|
||||
|
||||
- 155자 이내
|
||||
- 글의 핵심 가치 요약
|
||||
- 클릭 유도 문구
|
||||
|
||||
### URL Slug
|
||||
|
||||
- 영문 소문자
|
||||
- 하이픈으로 구분
|
||||
- 3-5 단어
|
||||
|
||||
## Content Calendar
|
||||
|
||||
| Category | Frequency | Example Topics |
|
||||
|----------|-----------|----------------|
|
||||
| SEO/마케팅 | 주 1회 | Technical SEO, AEO, 콘텐츠 전략 |
|
||||
| 데이터 분석 | 격주 | GA4, BigQuery, 대시보드 |
|
||||
| AI/기술 트렌드 | 월 2회 | LLM, 자동화, 마케팅 AI |
|
||||
| 인사이트/에세이 | 월 1회 | 디지털 문화, 세대론, 직업의 미래 |
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before publishing:
|
||||
|
||||
- [ ] 제목이 60자 이내인가?
|
||||
- [ ] 메타 설명이 155자 이내인가?
|
||||
- [ ] 전문용어에 영문이 병기되었는가?
|
||||
- [ ] 수사적 질문이 포함되었는가?
|
||||
- [ ] 기술 내용에 인간적 함의가 있는가?
|
||||
- [ ] 결론이 열린 질문으로 끝나는가?
|
||||
- [ ] 1,500-3,000자 범위인가?
|
||||
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())
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "{제목}"
|
||||
meta_description: "{155자 이내 메타 설명}"
|
||||
slug: "{english-url-slug}"
|
||||
tags: ["{tag1}", "{tag2}", "{tag3}"]
|
||||
author: "Andrew Yim"
|
||||
date: "{YYYY-MM-DD}"
|
||||
featured_image: "{이미지 URL 또는 프롬프트}"
|
||||
---
|
||||
|
||||
# {제목}
|
||||
|
||||
{도입부: Hook으로 시작. 독자의 관심을 끄는 질문, 통계, 또는 역설로 시작한다.}
|
||||
|
||||
{Context: 주제의 배경을 설명하고, 왜 지금 이 주제가 중요한지 언급한다.}
|
||||
|
||||
{Preview: 이 글에서 다룰 핵심 내용을 암시한다.}
|
||||
|
||||
---
|
||||
|
||||
## {첫 번째 핵심 포인트 제목}
|
||||
|
||||
{주장: 명확한 포인트를 제시한다.}
|
||||
|
||||
{근거: 데이터, 사례, 또는 전문가 의견으로 뒷받침한다.}
|
||||
|
||||
> {인용문이나 통계가 있다면 여기에}
|
||||
|
||||
{함의: 이것이 독자에게 의미하는 바를 설명한다.}
|
||||
|
||||
## {두 번째 핵심 포인트 제목}
|
||||
|
||||
{주장}
|
||||
|
||||
{근거}
|
||||
|
||||
{함의}
|
||||
|
||||
## {세 번째 핵심 포인트 제목}
|
||||
|
||||
{주장}
|
||||
|
||||
{근거}
|
||||
|
||||
{함의}
|
||||
|
||||
---
|
||||
|
||||
## 마치며
|
||||
|
||||
{Summary: 핵심 내용을 1-2문장으로 요약한다.}
|
||||
|
||||
{Reflection: 더 넓은 맥락에서 이 주제가 갖는 의미를 성찰한다.}
|
||||
|
||||
{Open Question: 독자가 계속 생각할 수 있는 열린 질문으로 마무리한다.}
|
||||
|
||||
> {마지막 수사적 질문}
|
||||
|
||||
---
|
||||
|
||||
*이 글은 [OurDigital Blog](https://blog.ourdigital.org)에 게재된 콘텐츠입니다.*
|
||||
|
||||
<!--
|
||||
SEO Checklist:
|
||||
- [ ] 제목 60자 이내
|
||||
- [ ] 메타 설명 155자 이내
|
||||
- [ ] 전문용어 영문 병기
|
||||
- [ ] 수사적 질문 포함
|
||||
- [ ] 1,500-3,000자
|
||||
-->
|
||||
Reference in New Issue
Block a user