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:
2026-01-31 16:50:17 +07:00
parent 7d20abe811
commit 0bc24d00b9
169 changed files with 9970 additions and 741 deletions

View File

@@ -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자 범위인가?

View 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())

View File

@@ -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자
-->