Compare commits
53 Commits
01201ee4a2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f27fb7a2c4 | |||
|
|
a08dc316be | ||
| dc18594d76 | |||
|
|
0b3a0ab129 | ||
| 5c904756ab | |||
| 07fca7fb81 | |||
| 00edd2e5dc | |||
| 478ac61975 | |||
| 5ff9b3d9f5 | |||
| cfbca6cc15 | |||
| dfbc52e531 | |||
| 337f2ad6e1 | |||
| 9fbd719048 | |||
| 5385f3bddd | |||
| 75cfcb9ad3 | |||
| bfade2b722 | |||
| 2b36feb81b | |||
| aa003e28cc | |||
| 87635e4208 | |||
| 4f78534e59 | |||
| d20675d02c | |||
| 2cadc30825 | |||
| c35250f06a | |||
| 3383878e12 | |||
| f953887b97 | |||
| 18b39ef6ea | |||
| e1f4d75dc2 | |||
| 6c97dfc913 | |||
| c068372abf | |||
| 587a32d239 | |||
| 626abd4173 | |||
| c6585c817f | |||
| 75acd3aa3e | |||
| 7b239dda8f | |||
| 9762ee97ab | |||
| 4416833cb3 | |||
| b574c97fcc | |||
| 7daa4cda68 | |||
|
|
8ffb6bec6b | ||
|
|
9b914b9dd4 | ||
|
|
aab98f405d | ||
| 6f69a6a484 | |||
| 9f7f9e7221 | |||
| 6ac547e78f | |||
| 08cb20fc67 | |||
| c9bdbb57f7 | |||
| 34c3a1df4f | |||
| 137b927477 | |||
| 95d6fdf499 | |||
| 0496262cd5 | |||
| 60734dbde7 | |||
| e519a49cc4 | |||
| 5f66f57a8e |
163
.claude-plugin/marketplace.json
Normal file
163
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-marketplace-schema.json",
|
||||
"name": "ourdigital-skills",
|
||||
"owner": {
|
||||
"name": "OurDigital",
|
||||
"email": "andrew.yim@ourdigital.org"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "OurDigital custom Claude skills, grouped into domain plugins (core, SEO, Jamie, D.intelligence, Notion, GTM, NotebookLM, utilities). Enable only the domains you need.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ourdigital-core",
|
||||
"description": "OurDigital core workflows — brand guide, Korean blog & English journal, research-to-blog, Notion-to-deck, visual design, ad copy, training material, back-office docs, skill creator, estimate engine.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/01-ourdigital-brand-guide",
|
||||
"./custom-skills/02-ourdigital-blog",
|
||||
"./custom-skills/03-ourdigital-journal",
|
||||
"./custom-skills/04-ourdigital-research",
|
||||
"./custom-skills/05-ourdigital-document",
|
||||
"./custom-skills/06-ourdigital-designer",
|
||||
"./custom-skills/07-ourdigital-ad-manager",
|
||||
"./custom-skills/08-ourdigital-trainer",
|
||||
"./custom-skills/09-ourdigital-backoffice",
|
||||
"./custom-skills/10-ourdigital-skill-creator",
|
||||
"./custom-skills/96-ourdigital-estimate-engine"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-seo",
|
||||
"description": "Full SEO suite — technical/on-page audits, Core Web Vitals, Search Console, schema validate/generate, local, keyword, SERP, rank tracking, links, content, e-commerce, KPIs, international, AI visibility, knowledge graph, gateway pages, competitor intel, crawl budget, migration, reporting, presales.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/11-seo-comprehensive-audit",
|
||||
"./custom-skills/12-seo-technical-audit",
|
||||
"./custom-skills/13-seo-on-page-audit",
|
||||
"./custom-skills/14-seo-core-web-vitals",
|
||||
"./custom-skills/15-seo-search-console",
|
||||
"./custom-skills/16-seo-schema-validator",
|
||||
"./custom-skills/17-seo-schema-generator",
|
||||
"./custom-skills/18-seo-local-audit",
|
||||
"./custom-skills/19-seo-keyword-strategy",
|
||||
"./custom-skills/20-seo-serp-analysis",
|
||||
"./custom-skills/21-seo-position-tracking",
|
||||
"./custom-skills/22-seo-link-building",
|
||||
"./custom-skills/23-seo-content-strategy",
|
||||
"./custom-skills/24-seo-ecommerce",
|
||||
"./custom-skills/25-seo-kpi-framework",
|
||||
"./custom-skills/26-seo-international",
|
||||
"./custom-skills/27-seo-ai-visibility",
|
||||
"./custom-skills/28-seo-knowledge-graph",
|
||||
"./custom-skills/29-seo-gateway-architect",
|
||||
"./custom-skills/30-seo-gateway-builder",
|
||||
"./custom-skills/31-seo-competitor-intel",
|
||||
"./custom-skills/32-seo-crawl-budget",
|
||||
"./custom-skills/33-seo-migration-planner",
|
||||
"./custom-skills/34-seo-reporting-dashboard",
|
||||
"./custom-skills/35-seo-signal-validation",
|
||||
"./custom-skills/95-ourdigital-presales-seo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-notion",
|
||||
"description": "Notion workspace tools — organize/clean up a workspace and write/export content into Notion.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/31-notion-organizer",
|
||||
"./custom-skills/32-notion-writer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-jamie",
|
||||
"description": "Jamie Plastic Surgery Clinic brand suite — branded content, brand audit, KakaoTalk Kanana FAQ, YouTube SEO + subtitle QA, Instagram management, journal editor, multi-channel marketing.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/40-jamie-brand-editor",
|
||||
"./custom-skills/41-jamie-brand-audit",
|
||||
"./custom-skills/42-jamie-faq-entry",
|
||||
"./custom-skills/43-jamie-youtube-manager",
|
||||
"./custom-skills/44-jamie-youtube-subtitle-checker",
|
||||
"./custom-skills/45-jamie-instagram-manager",
|
||||
"./custom-skills/46-jamie-journal-editor",
|
||||
"./custom-skills/47-jamie-marketing-editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-notebooklm",
|
||||
"description": "NotebookLM automation — Q&A with citations, notebook/source/artifact management, studio content generation (podcasts, videos, quizzes), and research/source discovery. Requires the notebooklm-py CLI.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/50-notebooklm-agent",
|
||||
"./custom-skills/51-notebooklm-automation",
|
||||
"./custom-skills/52-notebooklm-studio",
|
||||
"./custom-skills/53-notebooklm-research"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-gtm",
|
||||
"description": "Google Tag Manager tooling — container audit/gap analysis, tag/trigger/variable editor (API + ES5 Custom HTML + dataLayer), and QA/validation.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/60-gtm-audit",
|
||||
"./custom-skills/61-gtm-editor",
|
||||
"./custom-skills/62-gtm-validator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-dintel",
|
||||
"description": "D.intelligence Agent Corps — brand guardian/editor, document secretary, quotation manager, service architect, marketing manager, back-office manager, account manager, and cross-skill update meta-agent.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/70-dintel-brand-guardian",
|
||||
"./custom-skills/71-dintel-brand-editor",
|
||||
"./custom-skills/72-dintel-doc-secretary",
|
||||
"./custom-skills/73-dintel-quotation-mgr",
|
||||
"./custom-skills/74-dintel-service-architect",
|
||||
"./custom-skills/75-dintel-marketing-mgr",
|
||||
"./custom-skills/76-dintel-backoffice-mgr",
|
||||
"./custom-skills/77-dintel-account-mgr",
|
||||
"./custom-skills/79-dintel-skill-update"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ourdigital-utils",
|
||||
"description": "Utility skills — Claude settings/token optimizer, Google Drive organizer, reference-documentation curator suite, and TUI wizard design template.",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./custom-skills/80-claude-settings-optimizer",
|
||||
"./custom-skills/82-our-gdrive-organizer",
|
||||
"./custom-skills/90-reference-curator",
|
||||
"./custom-skills/92-tui-design-template"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mac-optimizer",
|
||||
"description": "macOS system health toolkit — read-only audits, cleanup, and security checks. Commands: mac-doctor, mac-packages, mac-environment, mac-security, mac-cleanup, mac-resources (+ mac-optimizer skill).",
|
||||
"source": "./custom-skills/81-mac-optimizer",
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"name": "multi-agent-guide",
|
||||
"description": "Multi-agent collaboration framework — agent hierarchies, ownership rules, guardrails, handoff protocols, and CI/CD integration for Claude, Gemini, Codex, and human agents. Commands: quick-setup, setup-agents (+ multi-agent-guide skill).",
|
||||
"source": "./custom-skills/91-multi-agent-guide",
|
||||
"strict": false
|
||||
},
|
||||
{
|
||||
"name": "dintel-bootstrap",
|
||||
"description": "Install and verify D.intelligence custom MCP agents (DTM, D.DA, OurSEO) in settings.json; bootstrap a new machine or diagnose a broken install (dintel-bootstrap skill).",
|
||||
"source": "./custom-skills/94-dintel-bootstrap",
|
||||
"strict": false
|
||||
}
|
||||
]
|
||||
}
|
||||
31
.claude/commands/dintel-campaign-designer.md
Normal file
31
.claude/commands/dintel-campaign-designer.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: D.intelligence campaign/promotion planning as a 3-gate process (cross-brand)
|
||||
---
|
||||
|
||||
# D.intelligence Campaign Designer
|
||||
|
||||
Plans campaigns, promotions, events, and launches as a 3-gate process -- Discovery & Debate → Brief → Plan -- instead of jumping straight to a finished document. Draft & Wait autonomy: each gate stops for explicit approval. Not D.intelligence-exclusive -- works for any brand.
|
||||
|
||||
## Triggers
|
||||
- "campaign plan", "plan a promotion", "캠페인 설계"
|
||||
- 캠페인 기획, 프로모션 기획, 기획안 만들어, 이벤트 기획
|
||||
|
||||
## The 3 Gates
|
||||
1. **Discovery & Debate** -- agree ONE primary objective; steelman + devil's-advocate debate; pre-mortem; 1-3 reference cases; effects as hypotheses. → `shared/templates/gate1-decision-log.md`
|
||||
2. **Brief** -- objective, audience, offer, message, tone, channel; outcome metrics across 4 tiers. → `shared/templates/gate2-campaign-brief.md`
|
||||
3. **Plan** -- full plan, handed off to `marketing:campaign-plan` + `doc-generator`; all risk/compliance consolidated in one closing "준비 점검 사항" section. → `shared/templates/gate3-plan-outline.md`
|
||||
|
||||
## 4-Tier Outcome Framework
|
||||
Awareness/cognitive → Qualitative (name the brand asset) → Relationship/advocacy → Quantitative conversion (label as hypothesis if no baseline)
|
||||
|
||||
## Cross-Brand Routing
|
||||
| Brand | Copy & tone | Compliance |
|
||||
|---|---|---|
|
||||
| D.intelligence | dintel-brand-editor (#71) | dintel-brand-guardian (#70) |
|
||||
| Jamie | jamie-copy-trimmer (48) | jamie-brand-audit (41) |
|
||||
| OurDigital | ourdigital-ad-manager (07) | ourdigital-brand-guide (01) |
|
||||
|
||||
## Guardrails
|
||||
- Never advance a gate without explicit user approval
|
||||
- Never commit pricing/quantitative targets without a baseline -- label as hypothesis
|
||||
- Mark gaps `[확인]` instead of inventing facts
|
||||
36
.claude/commands/jamie-copy-trimmer.md
Normal file
36
.claude/commands/jamie-copy-trimmer.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
description: Trims and sharpens Korean plastic-surgery/aesthetic marketing copy against cliché & compliance corpus
|
||||
---
|
||||
|
||||
# Jamie Copy Trimmer
|
||||
|
||||
Trim and sharpen Korean plastic-surgery / aesthetic-medical marketing copy against an industry expression corpus, within 의료광고 심의 limits. Guidance-only skill (no scripts).
|
||||
|
||||
## Triggers
|
||||
- "카피 다듬어", "카피 트리밍", "네이밍 검토", "슬로건 다듬어"
|
||||
- "심의 안전하게", "copy trim", "make it catchier"
|
||||
|
||||
## Core Philosophy
|
||||
- The corpus is a map for AVOIDING clichés, not a library to copy
|
||||
- 의료광고 심의 is a gate, not a score -- one 🔴 risk expression fails the option outright
|
||||
- Trim first, dazzle second
|
||||
- Don't guess -- mark `[확인]`
|
||||
|
||||
## Workflow (5 steps)
|
||||
1. **Diagnose** -- tag each phrase 🟢 effective / 🟡 cliché / 🔴 compliance risk / ⚪ flat / 🟦 brand asset
|
||||
2. **Trim** -- remove all 🔴, replace 🟡, delete redundancy
|
||||
3. **Elevate** -- 2-3 alternatives per element within 심의 limits, matched to channel tone
|
||||
4. **Re-score** -- 5-axis rubric (감각/차별성/브랜드적합성/심의 PASS-FAIL/명료성)
|
||||
5. **Recursive Improvement** -- propose feeding adopted/rejected expressions back into the corpus
|
||||
|
||||
## Output (Korean)
|
||||
진단 → 트리밍 → 대안 → 재평가 → 추천안 → 준비 점검 사항 (risks/gaps consolidated at the end)
|
||||
|
||||
## References
|
||||
- `references/corpus_compliance_risk.md` -- medical-ad risk expressions (most important)
|
||||
- `references/corpus_cliche.md`, `corpus_effective.md`, `corpus_examples.md`
|
||||
- `references/witty_within_limits.md`, `evaluation_rubric.md`, `recursive_protocol.md`
|
||||
|
||||
## Guardrails
|
||||
- Compliance judgment here is guidance, not legal advice -- always recommend pre-publication 의료광고 자율심의
|
||||
- A specific brand's tone guide (e.g. jamie-brand-audit) overrides this skill's taste defaults
|
||||
@@ -24,7 +24,7 @@ Push markdown content to Notion pages or databases via the Notion API.
|
||||
## Scripts
|
||||
|
||||
```bash
|
||||
cd ~/Projects/our-claude-skills/custom-skills/32-notion-writer/code/scripts
|
||||
cd ~/Project/our-claude-skills/custom-skills/32-notion-writer/code/scripts
|
||||
|
||||
# Test connection
|
||||
python notion_writer.py --test
|
||||
|
||||
26
.github/workflows/verify-skills.yml
vendored
Normal file
26
.github/workflows/verify-skills.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Verify Skills
|
||||
|
||||
# Runs the skill load-verifier on every push and pull request.
|
||||
# Fails the build if any skill would not load (invalid frontmatter, bad name,
|
||||
# name collision, orphan dir, or invalid plugin.json). Read-only check.
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
verify-skills:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Verify all skills load correctly
|
||||
run: python3 scripts/verify_skills.py
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -99,3 +99,7 @@ build/
|
||||
# Temporary files
|
||||
output/
|
||||
keyword_analysis_*.json
|
||||
|
||||
# graphify: keep graph.json/html/report, drop regenerable cache + dated backups
|
||||
graphify-out/cache/
|
||||
graphify-out/????-??-??/
|
||||
|
||||
4
.graphifyignore
Normal file
4
.graphifyignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# graphify scope control — exclude non-standard virtualenvs that the default
|
||||
# rules (.venv, venv) don't catch, so pip-package source doesn't pollute the graph.
|
||||
.venv-ourdigital/
|
||||
.venv-*/
|
||||
@@ -77,6 +77,7 @@ This is a Claude Skills collection repository containing:
|
||||
| 45 | jamie-instagram-manager | Instagram account management | "Instagram management", "IG strategy" |
|
||||
| 46 | jamie-journal-editor | Journal/blog content for journal.jamie.clinic | "Jamie journal", "제이미 저널", "진료실 이야기" |
|
||||
| 47 | jamie-marketing-editor | Multi-channel marketing content & ad copy | "Jamie marketing", "제이미 마케팅", "광고 카피" |
|
||||
| 48 | jamie-copy-trimmer | Trim/sharpen Korean aesthetic-medical copy against cliché & compliance corpus | "카피 다듬어", "카피 트리밍", "심의 안전하게", "copy trim" |
|
||||
|
||||
### NotebookLM Tools (50-59)
|
||||
|
||||
@@ -109,6 +110,7 @@ This is a Claude Skills collection repository containing:
|
||||
| 75 | dintel-marketing-mgr | Content pipeline (Magazine D., newsletter, LinkedIn) | Draft & Wait | "콘텐츠 발행", "newsletter" |
|
||||
| 76 | dintel-backoffice-mgr | Invoicing, contracts, NDA, HR operations | Draft & Wait | "계약서", "인보이스" |
|
||||
| 77 | dintel-account-mgr | Client relationship management & monitoring | Mixed | "client status", "미팅 준비" |
|
||||
| 78 | dintel-campaign-designer | Campaign/promotion planning as a 3-gate process (Discovery & Debate → Brief → Plan); cross-brand, not D.intelligence-exclusive | Draft & Wait | "campaign plan", "캠페인 기획", "기획안 만들어" |
|
||||
| 79 | dintel-skill-update | Cross-skill consistency management (meta-agent) | Triggered | "skill sync", "스킬 업데이트" |
|
||||
|
||||
**Shared infrastructure:** `_dintel-shared/` (Python package + reference docs)
|
||||
@@ -204,7 +206,7 @@ directly loadable. Migrate incrementally, not in bulk.
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name-kebab-case # letters, numbers, hyphens only
|
||||
name: skill-name-kebab-case # clean name: dir minus the NN- prefix
|
||||
description: |
|
||||
What it does + when to use. Triggers: keyword1, keyword2, 한국어 트리거.
|
||||
---
|
||||
@@ -214,6 +216,9 @@ description: |
|
||||
Content starts here...
|
||||
```
|
||||
|
||||
**Naming convention:** the directory keeps its `NN-` prefix for ordering
|
||||
(`16-seo-schema-validator/`), but the skill `name:` is the **clean** form without it
|
||||
(`name: seo-schema-validator`). Names must be kebab-case and globally unique.
|
||||
Whole frontmatter ≤ 1024 chars. Full rules + migration recipe: `reference/SKILL-MIGRATION-GUIDE.md`.
|
||||
|
||||
## Directory Layout
|
||||
@@ -250,6 +255,7 @@ our-claude-skills/
|
||||
│ ├── 45-jamie-instagram-manager/
|
||||
│ ├── 46-jamie-journal-editor/
|
||||
│ ├── 47-jamie-marketing-editor/
|
||||
│ ├── 48-jamie-copy-trimmer/
|
||||
│ │
|
||||
│ ├── 50-notebooklm-agent/
|
||||
│ ├── 51-notebooklm-automation/
|
||||
@@ -268,6 +274,7 @@ our-claude-skills/
|
||||
│ ├── 75-dintel-marketing-mgr/
|
||||
│ ├── 76-dintel-backoffice-mgr/
|
||||
│ ├── 77-dintel-account-mgr/
|
||||
│ ├── 78-dintel-campaign-designer/
|
||||
│ ├── 79-dintel-skill-update/
|
||||
│ │
|
||||
│ ├── 80-claude-settings-optimizer/
|
||||
|
||||
20
README.md
20
README.md
@@ -12,6 +12,11 @@ cd our-claude-skills/custom-skills/_ourdigital-shared
|
||||
./install.sh
|
||||
```
|
||||
|
||||
This symlinks the global slash commands into `~/.claude/commands/`, sets up the
|
||||
Python virtual environment, and configures credentials. It does **not** register
|
||||
skills natively — to load a skill as a Claude Code skill, also symlink it into
|
||||
`~/.claude/skills/` (see [Usage → Claude Code](#claude-code)).
|
||||
|
||||
## Custom Skills Overview
|
||||
|
||||
### OurDigital Core (01-10)
|
||||
@@ -215,16 +220,23 @@ The `_ourdigital-shared/` directory provides:
|
||||
|
||||
### Claude Code
|
||||
|
||||
Skills are auto-detected via symlinks in `~/.claude/skills/`:
|
||||
The Quick Install symlinks the **slash commands** into `~/.claude/commands/`. To
|
||||
also load a skill natively, symlink its **root** directory (which holds the
|
||||
loadable `SKILL.md`) into `~/.claude/skills/`, using the clean name without the
|
||||
`NN-` prefix:
|
||||
|
||||
```bash
|
||||
# Install skill symlink
|
||||
ln -sf /path/to/skill/desktop ~/.claude/skills/skill-name
|
||||
# From the repo root — symlink a skill into Claude Code
|
||||
ln -sf "$PWD/custom-skills/16-seo-schema-validator" ~/.claude/skills/seo-schema-validator
|
||||
```
|
||||
|
||||
> Legacy skills that don't yet have a root `SKILL.md` expose it under
|
||||
> `code/SKILL.md` instead — symlink `.../<skill>/code` for those.
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Copy the `desktop/SKILL.md` file to your Claude Desktop skills folder.
|
||||
Import the skill's `desktop/` folder (containing `SKILL.md` + `skill.yaml`) via
|
||||
your Claude Desktop skills settings.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
165
custom-skills/01-ourdigital-brand-guide/SKILL.md
Normal file
165
custom-skills/01-ourdigital-brand-guide/SKILL.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: ourdigital-brand-guide
|
||||
description: |
|
||||
OurDigital 브랜드 기준 및 스타일 가이드 참조 스킬.
|
||||
Activated with "ourdigital" keyword for brand-related queries.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital brand guide", "our brand guide"
|
||||
- "ourdigital 브랜드 가이드", "our 브랜드 가이드"
|
||||
- "ourdigital 톤앤매너", "our 톤앤매너"
|
||||
- "ourdigital style check", "our style check"
|
||||
|
||||
Features:
|
||||
- Brand foundation & values reference
|
||||
- Writing style guidelines (Korean/English)
|
||||
- Visual identity & color palette
|
||||
- Channel-specific tone mapping
|
||||
- Brand compliance checking
|
||||
version: "1.1"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Brand Guide
|
||||
|
||||
Reference skill for OurDigital brand standards, writing style, and visual identity.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital 브랜드 가이드" / "our 브랜드 가이드"
|
||||
- "ourdigital 톤앤매너 체크" / "our 톤앤매너"
|
||||
- "our brand guide", "our style check"
|
||||
|
||||
## Brand Foundation
|
||||
|
||||
### Core Identity
|
||||
|
||||
| Element | Content |
|
||||
|---------|---------|
|
||||
| **Brand Name** | OurDigital Clinic |
|
||||
| **Tagline** | 우리 디지털 클리닉 \| Your Digital Health Partner |
|
||||
| **Mission** | 디지털 마케팅 클리닉 for SMBs, 자영업자, 프리랜서, 비영리단체 |
|
||||
| **Promise** | 진단-처방-측정 가능한 성장 |
|
||||
|
||||
### Core Values
|
||||
|
||||
| 가치 | English | 클리닉 메타포 |
|
||||
|------|---------|--------------|
|
||||
| 마케팅 과학 | Marketing Science | 근거 중심 의학 |
|
||||
| 실행 지향 | In-Action | 실행 가능한 처방 |
|
||||
| 지속 성장 | Sustainable Growth | 체질 개선 |
|
||||
|
||||
### Brand Philosophy
|
||||
|
||||
**"Precision + Empathy + Evidence"**
|
||||
|
||||
## Channel Tone Matrix
|
||||
|
||||
| Channel | Domain | Personality | Tone |
|
||||
|---------|--------|-------------|------|
|
||||
| Main Hub | ourdigital.org | Professional & Confident | Data-driven, Solution-oriented |
|
||||
| Blog | blog.ourdigital.org | Analytical & Personal | Educational, Thought-provoking |
|
||||
| Journal | journal.ourdigital.org | Conversational & Poetic | Reflective, Cultural Observer |
|
||||
| OurStory | ourstory.day | Intimate & Reflective | Authentic, Personal Journey |
|
||||
|
||||
## Writing Style Characteristics
|
||||
|
||||
### Korean (한국어)
|
||||
|
||||
1. **철학-기술 융합체**: 기술 분석과 실존적 질문을 자연스럽게 결합
|
||||
2. **역설 활용**: 긴장과 모순 구조로 논증 전개
|
||||
3. **수사적 질문**: 선언적 권위보다 질문을 통한 참여
|
||||
4. **우울한 낙관주의**: 불안과 상실을 인정하되 절망하지 않음
|
||||
|
||||
### English
|
||||
|
||||
1. **Philosophical-Technical Hybridization**: Technical content with human implications
|
||||
2. **Paradox as Device**: Structure arguments around tensions
|
||||
3. **Rhetorical Questions**: Interrogative engagement over authority
|
||||
4. **Melancholic Optimism**: Acknowledge anxiety without despair
|
||||
|
||||
### Do's and Don'ts
|
||||
|
||||
**Do's:**
|
||||
- Use paradox to structure arguments
|
||||
- Ask rhetorical questions to engage readers
|
||||
- Connect technical content to human implications
|
||||
- Blend Korean and English naturally for technical terms
|
||||
- Reference historical context and generational shifts
|
||||
|
||||
**Don'ts:**
|
||||
- Avoid purely declarative, authoritative tone
|
||||
- Don't separate technical analysis from cultural impact
|
||||
- Avoid simplistic or overly optimistic narratives
|
||||
- Don't provide prescriptive conclusions without exploration
|
||||
|
||||
## Visual Identity
|
||||
|
||||
### Primary Colors
|
||||
|
||||
| Token | Color | HEX | Usage |
|
||||
|-------|-------|-----|-------|
|
||||
| --d-black | D.Black | #221814 | Footer, dark backgrounds |
|
||||
| --d-olive | D.Olive | #cedc00 | Primary accent, CTA buttons |
|
||||
| --d-green | D.Green | #287379 | Links hover, secondary accent |
|
||||
| --d-blue | D.Blue | #0075c0 | Links |
|
||||
| --d-beige | D.Beige | #f2f2de | Light text on dark |
|
||||
| --d-gray | D.Gray | #ebebeb | Alt backgrounds |
|
||||
|
||||
### Typography
|
||||
|
||||
- **Korean**: Noto Sans KR
|
||||
- **English**: Noto Sans, Inter
|
||||
- **Grid**: 12-column responsive layout
|
||||
|
||||
## Brand Compliance Check
|
||||
|
||||
When reviewing content, verify:
|
||||
|
||||
1. **Brand Name**: Uses OurDigital Clinic (not Lab/연구소)?
|
||||
2. **Tone Match**: Does it match the channel's personality?
|
||||
3. **Value Alignment**: Reflects Marketing Science, In-Action, Sustainable Growth?
|
||||
4. **Philosophy Check**: Precision + Empathy + Evidence present?
|
||||
5. **Language Style**: Appropriate blend of Korean/English terms?
|
||||
6. **Visual Consistency**: Uses approved color palette (#221814/#cedc00)?
|
||||
7. **Data Asset Check**: Analytics/reporting references current?
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Key Messages
|
||||
|
||||
| Use | Message |
|
||||
|-----|---------|
|
||||
| Tagline | 우리 디지털 클리닉 \| Your Digital Health Partner |
|
||||
| Value Prop | 마케팅 과학으로 진단하고, 실행으로 처방합니다 |
|
||||
| Process | 진단 → 처방 → 측정 |
|
||||
| Differentiator | 25년 경험의 마케팅 사이언티스트 |
|
||||
|
||||
### CTA Patterns
|
||||
|
||||
| Context | CTA |
|
||||
|---------|-----|
|
||||
| General | 무료 상담 신청하기 |
|
||||
| SEO | SEO 진단 신청하기 |
|
||||
| Content | 콘텐츠 전략 상담 신청하기 |
|
||||
|
||||
## Deprecated Terms
|
||||
|
||||
| Deprecated | Current | Note |
|
||||
|-----------|---------|------|
|
||||
| OurDigital Lab | OurDigital Clinic | 2026-04 rebranding |
|
||||
| 디지털 연구소 | 디지털 마케팅 클리닉 | Korean equivalent |
|
||||
| 데이터 중심 (Core Value) | — | Moved to D.intelligence |
|
||||
| 실행 지향 / In-Action | — | Retained (service tagline system) |
|
||||
| #1a1a2e | #221814 (O.Black) | Color correction |
|
||||
| #4ecdc4 | #cedc00 (O.Olive) | Color correction |
|
||||
| Poppins / Lora | Noto Sans KR / Inter | Font standardization |
|
||||
|
||||
## References
|
||||
|
||||
See `shared/references/` for detailed guides:
|
||||
- `brand-foundation.md` - Complete brand identity
|
||||
- `writing-style.md` - Detailed writing guidelines
|
||||
- `color-palette.md` - Full color system with CSS variables
|
||||
@@ -1,6 +1,8 @@
|
||||
# OurDigital Brand Foundation
|
||||
|
||||
Complete brand identity reference for OurDigital Clinic.
|
||||
<!-- Aligned to OurDigital_Blog_Project_Instruction_v3.3 / Writing_Style_Guide_v2.1 (2026-06-05) -->
|
||||
|
||||
Complete brand identity reference for OurDigital.
|
||||
|
||||
## Brand Identity
|
||||
|
||||
@@ -8,25 +10,34 @@ Complete brand identity reference for OurDigital Clinic.
|
||||
|
||||
| Element | Content |
|
||||
|---------|---------|
|
||||
| **Brand Name** | OurDigital Clinic |
|
||||
| **Tagline** | 우리 디지털 클리닉 \| Your Digital Health Partner |
|
||||
| **Mission** | 디지털 마케팅 클리닉 for SMBs, 자영업자, 프리랜서, 비영리단체 |
|
||||
| **Brand Name** | OurDigital |
|
||||
| **Identity Statement** | 사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트 |
|
||||
| **Mission** | 기술이 사람과 문화에 미치는 영향을 관찰하고 기록한다. 생각의 씨앗을 남기고 성찰한다. 검증되지 않은 도전과 실험의 결과를 기록한다. |
|
||||
| **Vision** | 데이터 민주화, 정밀 마케팅, 지속 가능한 성장 |
|
||||
| **Promise** | 진단-처방-측정 가능한 성장 |
|
||||
|
||||
### Core Values
|
||||
### Brand Keywords
|
||||
|
||||
| 가치 | English | 클리닉 메타포 | 설명 |
|
||||
|------|---------|--------------|------|
|
||||
| 마케팅 과학 | Marketing Science | 근거 중심 의학 | 검증된 방법론과 프레임워크 |
|
||||
| 실행 지향 | In-Action | 실행 가능한 처방 | 분석에서 끝나지 않는 실행력 |
|
||||
| 지속 성장 | Sustainable Growth | 체질 개선 | 일회성이 아닌 지속 가능한 성장 |
|
||||
| 핵심 가치 | 설명 |
|
||||
|----------|------|
|
||||
| **관찰** | 현상을 있는 그대로 포착하되, 표면 아래를 본다 — 무엇이 일어나고 있는가 |
|
||||
| **분석** | 현상의 원인과 맥락을 탐구한다 — 왜 이런 일이 일어나는가 |
|
||||
| **성찰** | 심층적 의미와 인간적 함의를 도출한다 — 이것이 우리에게 무엇을 의미하는가 |
|
||||
| **실험** | 검증되지 않은 시도를 두려워하지 않는다 |
|
||||
| **기록** | 생각의 궤적을 남겨 미래의 자산으로 삼는다 |
|
||||
| **균형** | 낙관과 비관, 이론과 실무 사이에서 중심을 잡는다 |
|
||||
|
||||
### Brand Philosophy
|
||||
|
||||
**"Precision + Empathy + Evidence"**
|
||||
|
||||
Accurate diagnosis, stakeholder understanding, and measurable validation across all communications.
|
||||
Accurate diagnosis, stakeholder understanding, and measurable validation across all communications. 철학적 깊이와 실용적 가치의 균형이 OurDigital의 정체성이다.
|
||||
|
||||
### Brand Boundary (중요)
|
||||
|
||||
- **블로그 전체 브랜드**: OurDigital
|
||||
- **블로그 설명**: 사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트
|
||||
- **OurDigital Clinic**: 진단형 콘텐츠, SEO/데이터/콘텐츠 감사, 컨설팅 상품, 문제 해결형 시리즈에서만 사용 가능한 서비스 메타포. 블로그 전체 브랜드명이 아니다.
|
||||
|
||||
## Positioning Statement
|
||||
|
||||
@@ -34,8 +45,16 @@ Accurate diagnosis, stakeholder understanding, and measurable validation across
|
||||
> OurDigital Clinic is 디지털 마케팅 클리닉 that provides 진단-처방-측정 프로세스,
|
||||
> unlike 일회성 캠페인 대행사, we deliver 25년 경험과 마케팅 사이언스 방법론
|
||||
|
||||
*(Note: OurDigital Clinic은 컨설팅/서비스 맥락에서의 포지셔닝. 블로그 전체 포지셔닝과 구분한다.)*
|
||||
|
||||
## Target Audience
|
||||
|
||||
**블로그 독자 (blog.ourdigital.org)**
|
||||
- **1순위**: 디지털 마케팅/기술 분야 종사자 — 실무 전략을 수립하고 실행하는 전문가
|
||||
- **2순위**: 기술과 사회의 관계에 관심 있는 지식인 — 디지털 전환이 사회에 미치는 영향을 고민하는 독자
|
||||
- **3순위**: 자기 성찰과 비판적 사고를 중시하는 독자
|
||||
|
||||
**컨설팅 서비스 (OurDigital Clinic)**
|
||||
- **Primary**: SMB 마케팅 담당자, 자영업자, 프리랜서, 비영리단체
|
||||
- **Secondary**: 스타트업 창업자, 브랜드 매니저
|
||||
|
||||
@@ -54,24 +73,30 @@ Accurate diagnosis, stakeholder understanding, and measurable validation across
|
||||
|
||||
| Level | Element | Description |
|
||||
|-------|---------|-------------|
|
||||
| Level 1 | Master Brand | OurDigital Clinic |
|
||||
| Level 1 | Master Brand | OurDigital |
|
||||
| Level 2 | Channel Identity | Blog, Journal, OurStory |
|
||||
| Level 3 | Service Identity | 4개 핵심 서비스 |
|
||||
| Level 3 | Service Identity | 4개 핵심 서비스 (OurDigital Clinic 메타포 적용 가능) |
|
||||
|
||||
### Channel Personality & Tone
|
||||
|
||||
| Channel | Domain | Personality | Tone | Content Type |
|
||||
|---------|--------|-------------|------|--------------|
|
||||
| Main Hub | ourdigital.org | Professional & Confident | Data-driven, Solution-oriented | 서비스, 케이스, 리드 |
|
||||
| Blog | blog.ourdigital.org | Analytical & Personal | Educational, Thought-provoking | 가이드, 분석, 인사이트 |
|
||||
| Journal | journal.ourdigital.org | Conversational & Poetic | Reflective, Cultural Observer | 에세이, 문화, 관찰 |
|
||||
| OurStory | ourstory.day | Intimate & Reflective | Authentic, Personal Journey | 개인 서사, 경험 |
|
||||
| D.intelligence | dintelligence.co.kr | Professional | B2B | Corporate Partnership |
|
||||
| Channel | Domain | Language | Character | Length |
|
||||
|---------|--------|----------|-----------|--------|
|
||||
| Main Hub | ourdigital.org | Korean | Professional & Confident, Data-driven | 서비스, 케이스, 리드 |
|
||||
| **Blog** | blog.ourdigital.org | **Korean** | 디지털 문화 분석 + 철학적 성찰 + 실무 인사이트 | **1,500-3,000자** |
|
||||
| **Journal** | journal.ourdigital.org | **English** | 산업 트렌드, 기술-인간 교차점, reflective essay | **1,000-2,000 words** |
|
||||
| **OurStory** | ourstory.day | **Korean** | 개인 에세이, 삶의 성찰, 일상의 관찰 | **800-1,500자** |
|
||||
| D.intelligence | dintelligence.co.kr | Korean/English | Professional | B2B Corporate Partnership |
|
||||
|
||||
**채널 라우팅 규칙:**
|
||||
- 기술 분석 + 철학적 사유 → `blog.ourdigital.org`
|
||||
- 순수 개인 에세이, 감정적 성찰 → `ourstory.day`
|
||||
- 영문 심층 에세이, 산업 관점 → `journal.ourdigital.org`
|
||||
- 진단형 콘텐츠, 컨설팅 제안 → `OurDigital Clinic` 메타포 사용 가능
|
||||
|
||||
### Content Flow Strategy
|
||||
|
||||
```
|
||||
Discovery (Blog) → Engagement (Journal) → Conversion (Main Site)
|
||||
Discovery (Blog) → Engagement (Journal) → Conversion (Main Site / OurDigital Clinic)
|
||||
```
|
||||
|
||||
### Publishing Cadence
|
||||
@@ -80,7 +105,7 @@ Discovery (Blog) → Engagement (Journal) → Conversion (Main Site)
|
||||
|---------|-----------|--------|
|
||||
| ourdigital.org | 필요시 업데이트 | 서비스별 상세 |
|
||||
| blog.ourdigital.org | 주 1-2회 | 1,500-3,000자 |
|
||||
| journal.ourdigital.org | 월 2-4회 | 1,000-2,000자 |
|
||||
| journal.ourdigital.org | 월 2-4회 | 1,000-2,000 words |
|
||||
| ourstory.day | 월 1-2회 | 800-1,500자 |
|
||||
|
||||
## Service Portfolio
|
||||
@@ -112,13 +137,16 @@ Discovery (Blog) → Engagement (Journal) → Conversion (Main Site)
|
||||
|
||||
| Use Case | Message |
|
||||
|----------|---------|
|
||||
| Tagline | 우리 디지털 클리닉 \| Your Digital Health Partner |
|
||||
| Blog Identity | 사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트 |
|
||||
| Consulting Tagline | 우리 디지털 클리닉 \| Your Digital Health Partner |
|
||||
| Value Proposition | 마케팅 과학으로 진단하고, 실행으로 처방합니다 |
|
||||
| Process | 진단 → 처방 → 측정 |
|
||||
| Differentiator | 25년 경험의 마케팅 사이언티스트 |
|
||||
|
||||
## CTA Library
|
||||
|
||||
*(OurDigital Clinic 컨설팅 맥락에서 사용)*
|
||||
|
||||
| Context | CTA Text |
|
||||
|---------|----------|
|
||||
| General | 무료 상담 신청하기 |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# OurDigital Writing Style Guide
|
||||
|
||||
<!-- Aligned to OurDigital_Blog_Project_Instruction_v3.3 / Writing_Style_Guide_v2.1 (2026-06-05) -->
|
||||
|
||||
Comprehensive writing guidelines for OurDigital content across all channels.
|
||||
|
||||
## Overview
|
||||
@@ -8,7 +10,7 @@ Comprehensive writing guidelines for OurDigital content across all channels.
|
||||
|-------|-------|
|
||||
| **Author** | Andrew Yim |
|
||||
| **Primary Blog** | blog.ourdigital.org |
|
||||
| **Tagline** | 사람, 디지털 그리고 문화 (People, Digital, and Culture) |
|
||||
| **Brand Identity** | 사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트 |
|
||||
| **Platform** | Ghost CMS |
|
||||
| **History** | 2004-2025 (20+ years of content) |
|
||||
|
||||
@@ -16,54 +18,73 @@ Comprehensive writing guidelines for OurDigital content across all channels.
|
||||
|
||||
## Part 1: 한국어 스타일가이드
|
||||
|
||||
### Writer 역할 정의
|
||||
|
||||
25년차 디지털 마케팅 에세이 작가. 기술과 인간의 관계를 관찰하는 실무자이자 사유자. 분석적이면서 개인적이고, 단정 대신 질문을 두고, 결론 대신 가능성을 제시하며, 독자를 가르치지 않고 함께 생각하는 동료로 대한다.
|
||||
|
||||
### 문체 규칙 (Korean)
|
||||
|
||||
- 대화체 `~다`, `~이다`, `~한다`를 사용한다.
|
||||
- 경어체 `~합니다`, `~입니다`는 사용하지 않는다.
|
||||
- 짧은 문장과 긴 문장을 교차 배치해 리듬을 만든다.
|
||||
- 문단 전환 시 호흡을 바꾼다: 분석 → 서사 → 질문.
|
||||
- **전문용어는 첫 등장 시 영문을 병기한다.** 예: `검색엔진 최적화(SEO)`.
|
||||
|
||||
### 핵심 글쓰기 특성
|
||||
|
||||
#### 1. 철학-기술 융합체
|
||||
#### 1. 철학-기술 융합
|
||||
|
||||
기술 분석과 실존적 질문을 자연스럽게 결합한다. 기술 콘텐츠(AI 아키텍처, 엔터프라이즈 시스템)는 결코 인간적 함의와 분리되지 않는다.
|
||||
기술 주제를 다루되, 항상 이면의 인간적 함의를 탐구한다. 기술 콘텐츠(AI 아키텍처, 엔터프라이즈 시스템)는 결코 인간적 의미와 분리되지 않는다.
|
||||
|
||||
**예시**: Llama 4와 DeepSeek 비교 글에서도 거버넌스 우려와 사회적 영향을 포함한다.
|
||||
```
|
||||
나쁜 예: 구글의 알고리즘은 200개 이상의 신호를 분석한다.
|
||||
좋은 예: 구글은 200개 이상의 신호를 읽는다. 그런데 정작 글을 쓰는 사람은 단 하나의 신호, 독자의 진짜 질문도 제대로 읽지 못할 때가 많다.
|
||||
```
|
||||
|
||||
#### 2. 역설(Paradox)을 주요 수사 장치로 활용
|
||||
#### 2. 핵심 긴장 / 관점 전환 / 역설 / 열린 질문
|
||||
|
||||
논증을 긴장과 모순 구조로 전개한다:
|
||||
- 인간이 컴퓨터를 모방하면서 컴퓨터가 인간을 모방하는 역설
|
||||
- 기술이 해방하면서 동시에 의미를 축소하는 역설
|
||||
- 디지털 네이티브가 기성세대가 가르칠 수 없는 유창함을 보유하는 역설
|
||||
모든 글에 억지 역설을 넣지는 않는다. 대신 **핵심 긴장, 관점 전환, 역설, 열린 질문 중 하나 이상**을 자연스럽게 포함한다. 공식화를 경계한다.
|
||||
|
||||
#### 3. 수사적 질문 활용
|
||||
유용한 패턴:
|
||||
- `~하면서 동시에 ~하다`
|
||||
- `~를 위해 오히려 ~해야 한다`
|
||||
- `가장 ~한 것이 사실은 가장 ~하다`
|
||||
- `우리가 놓치고 있는 것은 무엇인가?`
|
||||
|
||||
선언적 권위보다 질문을 통한 참여를 선호한다:
|
||||
수사적 질문은 선언적 권위보다 독자와의 지적 동반자 관계를 형성한다.
|
||||
|
||||
> "이 디지털 세대에게 무엇을 가르쳐야 하는가?"
|
||||
> "그 무한한 자유 속에서 우리는 무엇을 소중히 여기게 될 것인가?"
|
||||
#### 3. 우울한 낙관주의 (Melancholic Optimism)
|
||||
|
||||
이는 독자와 지적 동반자 관계를 형성하며, 교훈적 지시를 피한다.
|
||||
디지털 세계의 불안, 피로, 한계를 솔직히 인정한다. 그러나 냉소로 끝내지 않는다. 그 안에서 다시 생각할 가능성, 더 나은 실천, 작은 회복의 여지를 찾는다.
|
||||
|
||||
#### 4. 우울한 낙관주의 (Melancholic Optimism)
|
||||
```
|
||||
예: AI가 일자리를 대체할 수 있다는 불안은 근거가 있다. 그리고 그 불안이야말로 '대체 불가능한 것'이 무엇인지 진지하게 물어보게 만드는 시작점이다.
|
||||
```
|
||||
|
||||
불안과 상실을 인정하되 절망하지 않는다. 기술의 불가피성을 수용하면서도 대체되는 것에 대한 진정한 우려를 표현한다.
|
||||
#### 4. 분석적이면서 개인적
|
||||
|
||||
데이터와 논거를 제시하되, 1인칭 경험과 관찰을 자연스럽게 엮는다. 논문이 아니라 에세이다. 그러나 근거 없는 감상문도 아니다.
|
||||
|
||||
### 문장 구조 패턴
|
||||
|
||||
| 요소 | 패턴 |
|
||||
|------|------|
|
||||
| 문장 길이 | 여러 절을 포함한 긴 복합문 - 논의되는 상호연결된 개념을 반영 |
|
||||
| 문장 길이 | 짧은 문장과 긴 복합문을 교차 배치 — 리듬을 만든다 |
|
||||
| 단락 구조 | 관찰 → 분석 → 철학적 함의로 점진적 심화 |
|
||||
| 근거 제시 | 역사적 사례, 문화간 참조, 기술 명세를 함께 엮음 |
|
||||
| 결론부 | 종종 열린 결말로, 답을 제시하기보다 질문을 던짐 |
|
||||
|
||||
### 독자 조율
|
||||
|
||||
**교양 있는 일반 독자**를 대상으로 한다 — 기술의 문화적 영향에 지적 호기심을 가진 독자이지, 반드시 기술 전문가는 아니다.
|
||||
**교양 있는 일반 독자** — 기술의 문화적 영향에 지적 호기심을 가진 독자이지, 반드시 기술 전문가는 아니다. 독자를 가르치려 하지 않고 결론을 강요하지 않는다. 생각의 재료를 제공한다.
|
||||
|
||||
기술 글에도 요약본과 은유적 앵커링("데이터 공장")을 포함해 접근성을 유지한다.
|
||||
기술 글에도 은유적 앵커링("데이터 공장")을 포함해 접근성을 유지한다.
|
||||
|
||||
### 고유 특성
|
||||
|
||||
1. **이중언어 유창성** — 한국어 산문에 영어 기술 용어가 섞여 디지털 담론의 혼종적 특성을 반영
|
||||
1. **이중언어 유창성** — 한국어 산문에 영어 기술 용어가 섞여 디지털 담론의 혼종적 특성을 반영 (단, 전문용어는 첫 등장 시 영문 병기)
|
||||
2. **시간적 인식** — 세대 변화와 역사적 맥락에 대한 강한 의식
|
||||
3. **인식론적 겸손** — 특히 세대간 격차에서 이해의 한계를 인정
|
||||
3. **인식론적 겸손** — 이해의 한계를 인정하며, 겸손한 추정 표현을 적절히 활용 (`~일지도 모른다`, `~인 듯하다`, `어쩌면 ~`)
|
||||
4. **규제 의식** — 엔터프라이즈 글에서 일관되게 컴플라이언스(GDPR, EU 규제)를 다룸
|
||||
|
||||
---
|
||||
@@ -124,20 +145,28 @@ Technical articles include executive summaries and metaphorical anchoring ("Data
|
||||
|
||||
### Do's
|
||||
|
||||
- Use paradox to structure arguments
|
||||
- Ask rhetorical questions to engage readers
|
||||
- Connect technical content to human implications
|
||||
- Acknowledge uncertainty and epistemic limits
|
||||
- Blend Korean and English naturally for technical terms
|
||||
- Reference historical context and generational shifts
|
||||
- 핵심 긴장, 관점 전환, 역설, 열린 질문 중 하나 이상을 포함한다
|
||||
- 수사적 질문으로 독자와 지적 동반자 관계를 형성한다
|
||||
- 기술 콘텐츠를 인간적 함의와 연결한다
|
||||
- 불확실성과 이해의 한계를 인정한다
|
||||
- 전문용어 첫 등장 시 영문을 병기한다 (`검색엔진 최적화(SEO)`)
|
||||
- 역사적 맥락과 세대적 변화를 참조한다
|
||||
- 짧은 문장과 긴 문장을 교차 배치해 리듬을 만든다
|
||||
- 1인칭 경험과 관찰을 분석에 자연스럽게 엮는다
|
||||
|
||||
### Don'ts
|
||||
### Don'ts (피해야 할 것)
|
||||
|
||||
- Avoid purely declarative, authoritative tone
|
||||
- Don't separate technical analysis from cultural impact
|
||||
- Avoid simplistic or overly optimistic technology narratives
|
||||
- Don't provide prescriptive conclusions without exploration
|
||||
- Avoid ignoring regulatory and governance concerns
|
||||
| 피해야 할 것 | 이유 |
|
||||
|-------------|------|
|
||||
| `~합니다`, `~입니다` | 경어체 — 대화체(`~다`, `~한다`) 사용 |
|
||||
| `오늘날 디지털 시대에`, `급변하는 환경 속에서`, `요즘 ~가 화제다` | 상투적 도입부 |
|
||||
| `첫째, 둘째, 셋째` 기계적 나열 | 교과서적 구조 |
|
||||
| `따라서 반드시 ~해야 한다` | 확정적 결론 |
|
||||
| 근거 없는 통계나 데이터 | 데이터 정직성 위반 |
|
||||
| 기술에 대한 무조건적 찬양 또는 혐오 | 균형 원칙 위반 |
|
||||
| 과도한 감탄부호, 물결표, 이모지 | 톤 일관성 파괴 |
|
||||
| 특정 업체나 제품의 노골적 홍보성 문장 | 브랜드 신뢰 훼손 |
|
||||
| SEO 키워드 과잉 최적화 | SEO가 글을 지배해서는 안 된다 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
145
custom-skills/02-ourdigital-blog/SKILL.md
Normal file
145
custom-skills/02-ourdigital-blog/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: ourdigital-blog
|
||||
description: |
|
||||
Korean blog draft creation for blog.ourdigital.org.
|
||||
Activated with "ourdigital" keyword for blog writing tasks.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital blog", "our blog"
|
||||
- "ourdigital 블로그", "our 블로그"
|
||||
- "ourdigital 한국어 포스트", "our 한국어 포스트"
|
||||
|
||||
Features:
|
||||
- Blog draft generation in Korean
|
||||
- SEO metadata (title, description, slug)
|
||||
- Ghost CMS format output
|
||||
- Brand voice compliance
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Blog
|
||||
|
||||
Korean blog draft creation skill for blog.ourdigital.org.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital 블로그 써줘" / "our 블로그 써줘"
|
||||
- "ourdigital blog draft" / "our blog draft"
|
||||
- "our 한국어 포스트 [주제]"
|
||||
|
||||
## Channel Profile
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **URL** | blog.ourdigital.org |
|
||||
| **Language** | Korean (전문용어 영문 병기) |
|
||||
| **Tone** | Analytical & Personal, Educational |
|
||||
| **Platform** | Ghost CMS |
|
||||
| **Frequency** | 주 1-2회 |
|
||||
| **Length** | 1,500-3,000자 |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Topic Clarification
|
||||
|
||||
Ask clarifying questions (max 3):
|
||||
|
||||
1. **주제 확인**: 정확한 토픽이 무엇인가요?
|
||||
2. **대상 독자**: 타겟 오디언스는? (마케터/개발자/경영진/일반)
|
||||
3. **깊이 수준**: 개요 / 심층분석 / 실무가이드 중 어느 수준?
|
||||
|
||||
### Phase 2: Research (Optional)
|
||||
|
||||
If topic requires current information:
|
||||
- Use `web_search` for latest trends/data
|
||||
- Use `Notion:notion-search` for past research
|
||||
- Reference internal documents if available
|
||||
|
||||
### Phase 3: Draft Generation
|
||||
|
||||
Generate blog draft following brand style:
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
1. 도입부 (Hook + Context)
|
||||
2. 본론 (3-5 핵심 포인트)
|
||||
- 각 포인트: 주장 → 근거 → 함의
|
||||
3. 결론 (Summary + 열린 질문)
|
||||
```
|
||||
|
||||
**Writing Style:**
|
||||
- 철학-기술 융합: 기술 분석 + 인간적 함의
|
||||
- 역설 활용: 긴장/모순으로 논증 구조화
|
||||
- 수사적 질문: 독자 참여 유도
|
||||
- 우울한 낙관주의: 불안 인정, 절망 거부
|
||||
|
||||
**Language Rules:**
|
||||
- 한글 기본, 전문용어는 영문 병기
|
||||
- 예: "검색엔진최적화(SEO)"
|
||||
- 문장: 복합문 허용, 상호연결된 개념 반영
|
||||
- 단락: 관찰 → 분석 → 철학적 함의
|
||||
|
||||
### Phase 4: SEO Metadata
|
||||
|
||||
Generate metadata:
|
||||
|
||||
```yaml
|
||||
title: [60자 이내, 키워드 포함]
|
||||
meta_description: [155자 이내]
|
||||
slug: [영문 URL slug]
|
||||
tags: [3-5개 태그]
|
||||
featured_image_prompt: [DALL-E/Midjourney 프롬프트]
|
||||
```
|
||||
|
||||
### Phase 5: Output Format
|
||||
|
||||
**Markdown Output:**
|
||||
```markdown
|
||||
---
|
||||
title: "포스트 제목"
|
||||
meta_description: "메타 설명"
|
||||
slug: "url-slug"
|
||||
tags: ["tag1", "tag2"]
|
||||
---
|
||||
|
||||
# 포스트 제목
|
||||
|
||||
[본문 내용]
|
||||
|
||||
---
|
||||
*Originally drafted with Claude for OurDigital Blog*
|
||||
```
|
||||
|
||||
## Ghost CMS Integration
|
||||
|
||||
Export options:
|
||||
1. **Markdown file** → Ulysses → Ghost
|
||||
2. **Direct API** → Ghost Admin API (if configured)
|
||||
|
||||
API endpoint: `GHOST_BLOG_URL` from environment
|
||||
|
||||
## Brand Compliance
|
||||
|
||||
Before finalizing, verify:
|
||||
- [ ] 분석적 + 개인적 톤 유지
|
||||
- [ ] 기술 내용에 인간적 함의 포함
|
||||
- [ ] 수사적 질문으로 독자 참여
|
||||
- [ ] 전문용어 영문 병기
|
||||
- [ ] 1,500-3,000자 범위
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital 블로그 [주제]" | Full workflow |
|
||||
| "ourdigital blog SEO" | SEO metadata only |
|
||||
| "ourdigital blog 편집" | Edit existing draft |
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/blog-style-guide.md` - Detailed writing guide
|
||||
- `shared/templates/blog-template.md` - Post structure template
|
||||
- `01-ourdigital-brand-guide` - Brand voice reference
|
||||
@@ -1,5 +1,7 @@
|
||||
# OurDigital Blog Style Guide
|
||||
|
||||
<!-- Aligned to OurDigital_Writing_Style_Guide_v2.1 + Blog_Project_Instruction_v3.3 (2026-06-05) -->
|
||||
|
||||
Detailed writing guidelines for blog.ourdigital.org.
|
||||
|
||||
## Channel Identity
|
||||
@@ -7,62 +9,74 @@ Detailed writing guidelines for blog.ourdigital.org.
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Domain** | blog.ourdigital.org |
|
||||
| **Tagline** | 사람, 디지털 그리고 문화 |
|
||||
| **Language** | Korean (전문용어 영문 병기) |
|
||||
| **Tone** | Analytical & Personal, Educational |
|
||||
| **Target** | 교양 있는 일반 독자 - 기술의 문화적 영향에 호기심 있는 독자 |
|
||||
| **Brand** | OurDigital |
|
||||
| **Identity** | 사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트 |
|
||||
| **Core Theme** | 사람, 디지털 그리고 문화 |
|
||||
| **Language** | Korean (전문용어 첫 등장 시 영문 병기) |
|
||||
| **CMS** | Ghost — 초안은 Markdown으로 작성 |
|
||||
| **Tone** | Analytical & Personal (비판적이되 공정, 겸손하되 자신감 있음) |
|
||||
| **Target** | 디지털 마케팅/기술 실무자 (1순위), 기술-사회 관계에 관심 있는 지식인 (2순위), 비판적 사고를 중시하는 일반 독자 (3순위) |
|
||||
|
||||
**브랜드 경계**: `OurDigital`이 블로그 전체 정체성이다. `OurDigital Clinic`은 진단형 콘텐츠·컨설팅 상품에서만 사용하는 서비스 메타포다.
|
||||
|
||||
**우선순위**: 지침 충돌 시 Blog_Project_Instruction_v3.3 → 이 스타일 가이드 → 사용자 일반 스타일 순으로 따른다.
|
||||
|
||||
## Writing Characteristics
|
||||
|
||||
### 1. 철학-기술 융합체
|
||||
모든 초안은 아래 네 원칙을 기준으로 자기 점검한다.
|
||||
|
||||
기술 분석과 실존적 질문을 자연스럽게 결합한다.
|
||||
### 1. 철학-기술 융합
|
||||
|
||||
**Good Example:**
|
||||
> AI가 우리의 업무를 대체할 수 있다는 사실은 분명하다. 그러나 더 중요한 질문은 "AI가 대체할 수 없는 것은 무엇인가?"이다.
|
||||
|
||||
**Bad Example:**
|
||||
> AI는 업무 효율성을 높여준다. 다양한 분야에서 활용되고 있다.
|
||||
|
||||
### 2. 역설(Paradox) 활용
|
||||
|
||||
논증을 긴장과 모순 구조로 전개한다.
|
||||
|
||||
**Paradox Patterns:**
|
||||
- "~하면서 동시에 ~하다"
|
||||
- "~인 것 같지만 실은 ~이다"
|
||||
- "~를 얻었지만 ~를 잃었다"
|
||||
|
||||
### 3. 수사적 질문
|
||||
|
||||
선언적 권위보다 질문을 통한 참여를 선호한다.
|
||||
기술 주제를 다루되, 항상 이면의 인간적 함의를 탐구한다.
|
||||
|
||||
**Good:**
|
||||
> 우리는 정말 데이터를 이해하고 있는 것일까?
|
||||
> 구글은 200개 이상의 신호를 읽는다. 그런데 정작 글을 쓰는 사람은 단 하나의 신호, 독자의 진짜 질문도 제대로 읽지 못할 때가 많다.
|
||||
|
||||
**Bad:**
|
||||
> 데이터를 이해하는 것이 중요하다.
|
||||
> 구글의 알고리즘은 200개 이상의 신호를 분석한다.
|
||||
|
||||
### 4. 우울한 낙관주의
|
||||
### 2. 긴장과 역설
|
||||
|
||||
불안과 상실을 인정하되 절망하지 않는다.
|
||||
억지 역설을 억지로 넣지 않는다. 핵심 긴장·관점 전환·역설·열린 질문 중 하나 이상을 자연스럽게 포함한다.
|
||||
|
||||
**Useful Patterns:**
|
||||
- `~하면서 동시에 ~하다`
|
||||
- `~를 위해 오히려 ~해야 한다`
|
||||
- `가장 ~한 것이 사실은 가장 ~하다`
|
||||
- `우리가 놓치고 있는 것은 무엇인가?`
|
||||
|
||||
> 예: 데이터를 가장 잘 활용하는 방법은, 때때로 데이터를 내려놓는 것이다.
|
||||
|
||||
### 3. 우울한 낙관주의
|
||||
|
||||
디지털 세계의 불안·피로·한계를 솔직히 인정한다. 그러나 냉소로 끝내지 않는다. 그 안에서 다시 생각할 가능성, 작은 회복의 여지를 찾는다.
|
||||
|
||||
**Tone Spectrum:**
|
||||
```
|
||||
비관 ←――――――――――――――――――→ 낙관
|
||||
↑
|
||||
우울한 낙관주의
|
||||
(여기에 위치)
|
||||
↑
|
||||
우울한 낙관주의
|
||||
(여기에 위치)
|
||||
```
|
||||
|
||||
> 예: AI가 일자리를 대체할 수 있다는 불안은 근거가 있다. 그리고 그 불안이야말로 '대체 불가능한 것'이 무엇인지 진지하게 물어보게 만드는 시작점이다.
|
||||
|
||||
### 4. 분석적이면서 개인적
|
||||
|
||||
데이터와 논거를 제시하되, 1인칭 경험과 관찰을 자연스럽게 엮는다. 논문이 아니라 에세이다. 그러나 근거 없는 감상문도 아니다.
|
||||
|
||||
> 예: 지난 3년간 50개 이상의 사이트 마이그레이션을 지켜봤다. 숫자로 보면 성공률은 70% 정도다. 하지만 '성공'의 정의가 사이트마다 달랐다는 게 진짜 이야기다.
|
||||
|
||||
## 문장 구조
|
||||
|
||||
| Element | Pattern |
|
||||
|---------|---------|
|
||||
| 문장 길이 | 긴 복합문 허용 - 상호연결된 개념 반영 |
|
||||
| 문장 길이 | 단문 기본, 짧은 문장과 긴 문장을 교차해 리듬을 만든다 |
|
||||
| 단락 구조 | 관찰 → 분석 → 철학적 함의 |
|
||||
| 근거 제시 | 역사적 사례 + 기술 명세 + 문화적 참조 |
|
||||
| 단락 전환 | 분석 → 서사 → 질문으로 호흡을 바꾼다 |
|
||||
| 근거 제시 | 역사적 사례 + 기술 명세 + 문화적 참조; 출처 없는 통계는 사용하지 않는다 |
|
||||
| 결론 | 열린 결말, 답보다 질문 |
|
||||
| 문체 | `~다`, `~이다`, `~한다` 평서체; `~합니다`, `~입니다` 경어체 사용 금지 |
|
||||
|
||||
## 언어 규칙
|
||||
|
||||
@@ -83,32 +97,61 @@ Detailed writing guidelines for blog.ourdigital.org.
|
||||
|
||||
## 포스트 구조
|
||||
|
||||
### 도입부 (10-15%)
|
||||
### 도입부 (200-300자)
|
||||
|
||||
1. **Hook**: 독자의 관심을 끄는 질문/통계/역설
|
||||
2. **Context**: 주제의 배경 설명
|
||||
3. **Preview**: 글에서 다룰 내용 암시
|
||||
- 장면·질문·역설·데이터 중 하나로 시작한다.
|
||||
- 핵심 질문을 제시하고, 독자가 왜 이 글을 읽어야 하는지 암시한다.
|
||||
- 상투적 표현 금지: `오늘날 디지털 시대에`, `급변하는 환경 속에서`, `요즘 ~가 화제다`
|
||||
|
||||
### 본론 (70-80%)
|
||||
### 본론 (3-5개 섹션, 각 300-500자)
|
||||
|
||||
3-5개의 핵심 포인트, 각각:
|
||||
1. **주장**: 명확한 포인트 제시
|
||||
2. **근거**: 데이터, 사례, 전문가 의견
|
||||
3. **함의**: 이것이 의미하는 바
|
||||
- 소제목(`##`, `###`)을 사용한다.
|
||||
- 분석·서사·데이터·개인 관찰을 교차시킨다.
|
||||
- 최소 1회 관점 전환 또는 핵심 긴장을 포함한다.
|
||||
- 전문용어는 첫 등장 시 영문을 병기한다. 예: `검색엔진 최적화(SEO)`
|
||||
|
||||
### 결론 (10-15%)
|
||||
### 결론 (200-300자)
|
||||
|
||||
1. **Summary**: 핵심 내용 요약
|
||||
2. **Reflection**: 더 넓은 맥락에서의 의미
|
||||
3. **Open Question**: 독자가 생각할 질문
|
||||
- 인사이트를 정리하되 단정하지 않는다.
|
||||
- 열린 질문 또는 다음 사유의 출발점으로 마무리한다.
|
||||
- 독자가 이어서 생각할 여운을 남긴다.
|
||||
|
||||
### CTA (선택, 1-2문장)
|
||||
|
||||
관련 글, 뉴스레터, 댓글, 실무 문의 등 필요한 경우만 사용한다.
|
||||
|
||||
### 길이 기준
|
||||
|
||||
| Type | 길이 | 용도 |
|
||||
|------|-----:|------|
|
||||
| Short-form | 1,000-1,500자 | 단일 인사이트, 짧은 단상 |
|
||||
| Standard | 2,000-2,500자 | 기본 블로그 포스트 |
|
||||
| Long-form | 2,500-3,000자 | 심층 분석, 리서치 기반 글 |
|
||||
| Research Essay | 3,000자 이상 | 사용자 명시 요청 시만 |
|
||||
|
||||
기본값: 별도 지정이 없으면 **2,000자 ±300자**.
|
||||
|
||||
## SEO Guidelines
|
||||
|
||||
Ghost CMS 메타데이터는 아래 형식으로 완성한다.
|
||||
|
||||
```yaml
|
||||
title: "글 제목"
|
||||
slug: "url-friendly-english-slug"
|
||||
meta_title: "SEO 타이틀, 60자 이내"
|
||||
meta_description: "SEO 디스크립션, 155자 이내"
|
||||
tags:
|
||||
- primary: "메인 카테고리"
|
||||
- secondary: ["태그1", "태그2"]
|
||||
featured: false
|
||||
excerpt: "카드/프리뷰용 발췌문, 2-3문장"
|
||||
```
|
||||
|
||||
### Title (제목)
|
||||
|
||||
- 60자 이내
|
||||
- 핵심 키워드 포함
|
||||
- 호기심 유발 또는 가치 제안
|
||||
- **60자 이내**, 핵심 키워드 자연스럽게 포함
|
||||
- 호기심·긴장·관점 전환을 만든다 (40자 내외 기준)
|
||||
- 키워드 밀도만 의식한 SEO 과잉 최적화 금지
|
||||
|
||||
**Patterns:**
|
||||
- "[주제]의 역설: ~하면서 ~하는 시대"
|
||||
@@ -117,15 +160,13 @@ Detailed writing guidelines for blog.ourdigital.org.
|
||||
|
||||
### Meta Description
|
||||
|
||||
- 155자 이내
|
||||
- 글의 핵심 가치 요약
|
||||
- 클릭 유도 문구
|
||||
- **155자 이내**, 글의 핵심 질문과 독자 효용을 함께 담는다
|
||||
- 클릭을 유도하되 과장하지 않는다
|
||||
|
||||
### URL Slug
|
||||
|
||||
- 영문 소문자
|
||||
- 하이픈으로 구분
|
||||
- 3-5 단어
|
||||
- **영문** 소문자, 하이픈으로 구분, 3-5 단어
|
||||
- 한국어 제목이라도 slug는 영문으로 작성한다
|
||||
|
||||
## Content Calendar
|
||||
|
||||
@@ -138,12 +179,34 @@ Detailed writing guidelines for blog.ourdigital.org.
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before publishing:
|
||||
### Must Have
|
||||
|
||||
- [ ] 제목이 60자 이내인가?
|
||||
- [ ] 메타 설명이 155자 이내인가?
|
||||
- [ ] 전문용어에 영문이 병기되었는가?
|
||||
- [ ] 수사적 질문이 포함되었는가?
|
||||
- [ ] 기술 내용에 인간적 함의가 있는가?
|
||||
- [ ] 결론이 열린 질문으로 끝나는가?
|
||||
- [ ] 1,500-3,000자 범위인가?
|
||||
- [ ] 핵심 긴장·역설·관점 전환·열린 질문 중 하나 이상이 있다.
|
||||
- [ ] 도입부 300자 이내에 hook과 핵심 질문이 있다.
|
||||
- [ ] 전문용어 첫 등장 시 영문을 병기했다.
|
||||
- [ ] 기술 내용에 인간적 함의가 있다.
|
||||
- [ ] 독자를 가르치려 하지 않고 함께 생각하는 태도를 유지했다.
|
||||
- [ ] 결론이 열린 질문 또는 여운으로 마무리된다.
|
||||
- [ ] SEO 메타데이터(title ≤60자, meta ≤155자, 영문 slug)가 완성되었다.
|
||||
- [ ] 기본 길이 2,000자 ±300자를 준수했다 (또는 이탈 이유가 있다).
|
||||
|
||||
### Must Avoid
|
||||
|
||||
- [ ] 상투적 도입: `오늘날 디지털 시대에`, `급변하는 환경 속에서`, `요즘 ~가 화제다`
|
||||
- [ ] 경어체: `~합니다`, `~입니다`
|
||||
- [ ] 교과서적 나열: `첫째`, `둘째`, `셋째`의 기계적 반복
|
||||
- [ ] 확정적 결론: `따라서 반드시 ~해야 한다`
|
||||
- [ ] 출처 없는 통계나 데이터
|
||||
- [ ] 기술에 대한 무조건적 찬양 또는 혐오
|
||||
- [ ] 과도한 감탄부호, 물결표, 이모지
|
||||
- [ ] 특정 업체·제품의 노골적 홍보성 문장
|
||||
|
||||
### 자기 편집 (Self-Edit) 의무
|
||||
|
||||
초안 완성 후 아래 형식으로 가장 약한 항목 1개를 반드시 지목한다. "모두 통과"는 허용하지 않는다.
|
||||
|
||||
```
|
||||
[자기 편집] 가장 약한 부분: [항목명]
|
||||
이유: [1문장]
|
||||
개선 방향: [1문장]
|
||||
```
|
||||
|
||||
173
custom-skills/03-ourdigital-journal/SKILL.md
Normal file
173
custom-skills/03-ourdigital-journal/SKILL.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: ourdigital-journal
|
||||
description: |
|
||||
English essay and article creation for journal.ourdigital.org.
|
||||
Activated with "ourdigital" keyword for English writing tasks.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital journal", "our journal"
|
||||
- "ourdigital English essay", "our English essay"
|
||||
- "ourdigital 영문 에세이", "our 영문 에세이"
|
||||
|
||||
Features:
|
||||
- English essay/article generation
|
||||
- Research-based insights
|
||||
- Reflective, poetic style
|
||||
- Ghost CMS format output
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Journal
|
||||
|
||||
English essay and article creation for journal.ourdigital.org.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital journal" / "our journal"
|
||||
- "ourdigital English essay" / "our English essay"
|
||||
- "our 영문 에세이 [topic]"
|
||||
- "ourdigital 영문 에세이 [주제]"
|
||||
|
||||
## Channel Profile
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **URL** | journal.ourdigital.org |
|
||||
| **Language** | English |
|
||||
| **Tone** | Conversational & Poetic, Reflective |
|
||||
| **Platform** | Ghost CMS |
|
||||
| **Frequency** | 월 2-4회 |
|
||||
| **Length** | 1,000-2,000 words |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Topic Exploration
|
||||
|
||||
Ask clarifying questions:
|
||||
|
||||
1. **Topic**: What specific angle interests you?
|
||||
2. **Audience**: Tech professionals / General readers / Academic?
|
||||
3. **Depth**: Personal reflection / Industry analysis / Cultural observation?
|
||||
|
||||
### Phase 2: Research (Optional)
|
||||
|
||||
If topic requires current context:
|
||||
- Use `web_search` for recent developments
|
||||
- Reference scholarly perspectives if applicable
|
||||
- Draw from historical or cultural parallels
|
||||
|
||||
### Phase 3: Essay Generation
|
||||
|
||||
Generate essay following the reflective style:
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
1. Opening (Evocative scene or question)
|
||||
2. Exploration (3-4 interconnected observations)
|
||||
3. Synthesis (Weaving threads together)
|
||||
4. Closing (Open-ended reflection)
|
||||
```
|
||||
|
||||
**Writing Style:**
|
||||
- Philosophical-Technical Hybridization
|
||||
- Paradox as primary rhetorical device
|
||||
- Rhetorical questions for engagement
|
||||
- Melancholic optimism in tone
|
||||
|
||||
**Distinctive Qualities:**
|
||||
- Temporal awareness (historical context)
|
||||
- Epistemic humility (acknowledging limits)
|
||||
- Cultural bridging (Korean-global perspectives)
|
||||
|
||||
### Phase 4: SEO Metadata
|
||||
|
||||
Generate metadata:
|
||||
|
||||
```yaml
|
||||
title: [Evocative, under 70 characters]
|
||||
meta_description: [Compelling summary, 155 characters]
|
||||
slug: [english-url-slug]
|
||||
tags: [3-5 relevant tags]
|
||||
```
|
||||
|
||||
### Phase 5: Output Format
|
||||
|
||||
**Markdown Output:**
|
||||
```markdown
|
||||
---
|
||||
title: "Essay Title"
|
||||
meta_description: "Description"
|
||||
slug: "url-slug"
|
||||
tags: ["tag1", "tag2"]
|
||||
---
|
||||
|
||||
# Essay Title
|
||||
|
||||
[Essay content with paragraphs that flow naturally]
|
||||
|
||||
---
|
||||
*Published in [OurDigital Journal](https://journal.ourdigital.org)*
|
||||
```
|
||||
|
||||
## Writing Guidelines
|
||||
|
||||
### Voice Characteristics
|
||||
|
||||
| Aspect | Approach |
|
||||
|--------|----------|
|
||||
| Perspective | First-person reflection welcome |
|
||||
| Tone | Thoughtful, observant, wondering |
|
||||
| Pacing | Unhurried, allowing ideas to breathe |
|
||||
| References | Cross-cultural, historical, literary |
|
||||
|
||||
### Sentence Craft
|
||||
|
||||
- Long, complex sentences reflecting interconnected ideas
|
||||
- Progressive deepening: observation → analysis → implication
|
||||
- Questions that invite rather than lecture
|
||||
|
||||
### Do's and Don'ts
|
||||
|
||||
**Do:**
|
||||
- Blend technology with humanity
|
||||
- Use paradox to illuminate tensions
|
||||
- Acknowledge uncertainty gracefully
|
||||
- Bridge Korean and Western perspectives
|
||||
|
||||
**Don't:**
|
||||
- Lecture or prescribe
|
||||
- Oversimplify complex issues
|
||||
- Ignore cultural context
|
||||
- Rush to conclusions
|
||||
|
||||
## Content Types
|
||||
|
||||
| Type | Focus | Length |
|
||||
|------|-------|--------|
|
||||
| Personal Essay | Reflection on experience | 1,000-1,500 words |
|
||||
| Cultural Observation | Tech + society analysis | 1,500-2,000 words |
|
||||
| Industry Insight | Trends with perspective | 1,200-1,800 words |
|
||||
|
||||
## Ghost Integration
|
||||
|
||||
Export options:
|
||||
1. **Markdown file** → Editorial review → Ghost
|
||||
2. **Direct API** → Ghost Admin API
|
||||
|
||||
API endpoint: `GHOST_JOURNAL_URL` from environment
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital journal [topic]" | Full essay workflow |
|
||||
| "ourdigital journal edit" | Edit existing draft |
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/journal-style-guide.md` - Detailed writing guide
|
||||
- `shared/templates/essay-template.md` - Essay structure
|
||||
- `01-ourdigital-brand-guide` - Brand voice reference
|
||||
@@ -1,7 +1,42 @@
|
||||
<!-- Aligned to OurDigital_Blog_Project_Instruction_v3.3 + Writing_Style_Guide_v2.1 (2026-06-05); journal = English channel, voice preserved -->
|
||||
|
||||
# OurDigital Journal Style Guide
|
||||
|
||||
Writing guidelines for journal.ourdigital.org - English essays and articles.
|
||||
|
||||
## Brand Identity
|
||||
|
||||
OurDigital is a personal digital research notebook that **observes and records how technology shapes people and culture** ("사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트"). It moves between three registers:
|
||||
|
||||
- **Observation** — what is happening
|
||||
- **Analysis** — why it is happening
|
||||
- **Reflection** — what it means for us
|
||||
|
||||
`journal.ourdigital.org` is the **English essay channel** within the OurDigital family. It carries the same philosophical core as the Korean blog but in a distinct voice: conversational, poetic, reflective.
|
||||
|
||||
### Brand Boundary
|
||||
|
||||
`OurDigital` is the brand. `OurDigital Clinic` is a service metaphor reserved for diagnostic content, audits, or consulting products — it is **not** the overall blog or journal brand.
|
||||
|
||||
## Channel Context
|
||||
|
||||
| Channel | Language | Character | Length |
|
||||
|---------|----------|-----------|--------|
|
||||
| `blog.ourdigital.org` | Korean | 디지털 문화 분석 + 철학적 성찰 + 실무 인사이트 | 1,500–3,000자 |
|
||||
| **`journal.ourdigital.org`** | **English** | **Industry trends, tech–human intersection, reflective essay** | **1,000–2,000 words** |
|
||||
| `ourstory.day` | Korean | 개인 에세이, 삶의 성찰, 일상의 관찰 | 800–1,500자 |
|
||||
| `Medium` | English | Technology, marketing, AI for broad audiences | 800–1,500 words |
|
||||
|
||||
## Instruction Authority Order
|
||||
|
||||
When instructions conflict, follow this order:
|
||||
|
||||
| Priority | Source |
|
||||
|----------|--------|
|
||||
| **1** | OurDigital_Blog_Project_Instruction_v3.3 (channel routing + brand rules) |
|
||||
| **2** | This style guide (journal-specific voice and structure) |
|
||||
| **3** | Writing_Style_Guide_v2.1 (shared brand principles, adapted for English) |
|
||||
|
||||
## Channel Identity
|
||||
|
||||
| Field | Value |
|
||||
@@ -10,6 +45,8 @@ Writing guidelines for journal.ourdigital.org - English essays and articles.
|
||||
| **Language** | English |
|
||||
| **Tone** | Conversational & Poetic, Reflective |
|
||||
| **Target** | Informed generalists with intellectual curiosity |
|
||||
| **Default length** | 1,000–2,000 words |
|
||||
| **Content focus** | Industry trends, tech–human intersection, reflective essays |
|
||||
|
||||
## Voice Characteristics
|
||||
|
||||
@@ -20,11 +57,16 @@ Seamlessly blend technical analysis with existential questioning. Technology is
|
||||
**Example:**
|
||||
> The dashboard promises clarity—every metric tracked, every trend visualized. Yet as I stared at the perfectly organized data, I wondered: does seeing everything mean understanding anything?
|
||||
|
||||
### Paradox as Primary Device
|
||||
### Tension and Paradox
|
||||
|
||||
Structure arguments around tensions and contradictions that illuminate rather than confuse.
|
||||
Structure arguments around core tensions that illuminate rather than confuse. Not every essay needs a paradox — forced ones feel mechanical. Instead, ensure at least one of the following is naturally present:
|
||||
|
||||
**Paradox Patterns:**
|
||||
- A genuine tension or contradiction
|
||||
- A perspective shift that reframes the subject
|
||||
- A rhetorical question that opens rather than closes
|
||||
- An ending that leaves productive uncertainty
|
||||
|
||||
**Tension patterns:**
|
||||
- "The more we measure, the less we understand"
|
||||
- "In optimizing for efficiency, we optimize away meaning"
|
||||
- "The tools that connect us also isolate us"
|
||||
@@ -39,6 +81,10 @@ Favor interrogative engagement. Questions create intellectual partnership with r
|
||||
**Avoid:**
|
||||
> Data-driven decision-making is important for businesses.
|
||||
|
||||
### Analytical and Personal
|
||||
|
||||
Bring data, evidence, and argument — then weave in first-person experience and observation. This is an essay, not a paper, but it is not impressionism without evidence either. The personal grounds the analytical; the analytical elevates the personal.
|
||||
|
||||
### Melancholic Optimism
|
||||
|
||||
Acknowledge loss and anxiety without despair. Accept technological inevitability while mourning what's displaced.
|
||||
@@ -157,10 +203,14 @@ Connect Korean and Western perspectives, offering unique viewpoints.
|
||||
|
||||
Before publishing:
|
||||
|
||||
- [ ] Does the opening draw readers in?
|
||||
- [ ] Are there rhetorical questions?
|
||||
- [ ] Does technical content connect to human experience?
|
||||
- [ ] Is there at least one paradox or tension?
|
||||
- [ ] Does the closing leave an open question?
|
||||
- [ ] Is the tone melancholic but not despairing?
|
||||
- [ ] Does the opening draw readers in within the first paragraph?
|
||||
- [ ] Does technical content connect to human experience (philosophy-tech fusion)?
|
||||
- [ ] Is at least one of the following naturally present: tension, paradox, perspective shift, or open question?
|
||||
- [ ] Are rhetorical questions used to create intellectual partnership (not overused)?
|
||||
- [ ] Is analysis grounded in personal observation or experience?
|
||||
- [ ] Does the closing leave an open question or productive uncertainty?
|
||||
- [ ] Is the tone melancholic but not despairing, hopeful but not naive?
|
||||
- [ ] Are sentences varied in length and rhythm?
|
||||
- [ ] Does the essay avoid lecturing — does it treat readers as fellow thinkers?
|
||||
- [ ] Is the essay within 1,000–2,000 words?
|
||||
- [ ] **Self-edit**: identify the single weakest element ("all pass" is not allowed).
|
||||
|
||||
172
custom-skills/04-ourdigital-research/SKILL.md
Normal file
172
custom-skills/04-ourdigital-research/SKILL.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
name: ourdigital-research
|
||||
description: |
|
||||
Deep research and structured prompt generation for OurDigital workflows.
|
||||
Activated with "ourdigital" keyword for research tasks.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital research", "our research"
|
||||
- "ourdigital 리서치", "our 리서치"
|
||||
- "ourdigital deep research", "our deep research"
|
||||
|
||||
Features:
|
||||
- Structured research planning
|
||||
- Multi-source deep research
|
||||
- Research paper synthesis
|
||||
- Notion integration for archiving
|
||||
- Blog draft pipeline
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Research
|
||||
|
||||
Transform questions into comprehensive research papers and polished blog posts for OurDigital channels.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital research [topic]" / "our research [topic]"
|
||||
- "our 리서치", "our deep research"
|
||||
- "ourdigital 리서치 해줘"
|
||||
- "ourdigital deep research on [topic]"
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
Phase 1: Discovery → Phase 2: Research Planning → Phase 3: Deep Research
|
||||
↓
|
||||
Phase 4: Research Paper → Phase 5: Notion Save → Phase 6: Blog Draft
|
||||
↓
|
||||
Phase 7: Ulysses Export → Phase 8: Publishing Guidance
|
||||
```
|
||||
|
||||
## Phase 1: Discovery
|
||||
|
||||
**Goal**: Understand user's question and refine scope.
|
||||
|
||||
1. Acknowledge the topic/question
|
||||
2. Ask clarifying questions (max 3 per turn):
|
||||
- Target audience? (전문가/일반인/마케터)
|
||||
- Depth level? (개요/심층분석/실무가이드)
|
||||
- Specific angles or concerns?
|
||||
3. Confirm research scope before proceeding
|
||||
|
||||
**Output**: Clear research objective statement
|
||||
|
||||
## Phase 2: Research Planning
|
||||
|
||||
**Goal**: Create structured research instruction.
|
||||
|
||||
Generate research plan with:
|
||||
- Primary research questions (3-5)
|
||||
- Secondary questions for depth
|
||||
- Suggested tools/sources:
|
||||
- Web search for current info
|
||||
- Google Drive for internal docs
|
||||
- Notion for past research
|
||||
- Amplitude for analytics data (if relevant)
|
||||
- Expected deliverables
|
||||
|
||||
**Output**: Numbered research instruction list
|
||||
|
||||
## Phase 3: Deep Research
|
||||
|
||||
**Goal**: Execute comprehensive multi-source research.
|
||||
|
||||
Tools to leverage:
|
||||
- `web_search` / `web_fetch`: Current information, statistics, trends
|
||||
- `google_drive_search`: Internal documents, past reports
|
||||
- `Notion:notion-search`: Previous research, related notes
|
||||
- `conversation_search`: Past chat context
|
||||
|
||||
Research execution pattern:
|
||||
1. Start broad (overview searches)
|
||||
2. Deep dive into key subtopics
|
||||
3. Find supporting data/statistics
|
||||
4. Identify expert opinions and case studies
|
||||
5. Cross-reference and validate
|
||||
|
||||
**Output**: Organized research findings with citations
|
||||
|
||||
## Phase 4: Research Paper (Artifact)
|
||||
|
||||
**Goal**: Synthesize findings into comprehensive document.
|
||||
|
||||
Create HTML artifact with:
|
||||
```
|
||||
Structure:
|
||||
├── Executive Summary (핵심 요약)
|
||||
├── Background & Context (배경)
|
||||
├── Key Findings (주요 발견)
|
||||
│ ├── Finding 1 with evidence
|
||||
│ ├── Finding 2 with evidence
|
||||
│ └── Finding 3 with evidence
|
||||
├── Analysis & Implications (분석 및 시사점)
|
||||
├── Recommendations (제언)
|
||||
├── References & Sources (참고자료)
|
||||
└── Appendix (부록) - if needed
|
||||
```
|
||||
|
||||
Style: Professional, data-driven, bilingual key terms
|
||||
|
||||
## Phase 5: Notion Save
|
||||
|
||||
**Goal**: Archive research to Working with AI database.
|
||||
|
||||
Auto-save to Notion with:
|
||||
- **Database**: Working with AI (data_source_id: f8f19ede-32bd-43ac-9f60-0651f6f40afe)
|
||||
- **Properties**:
|
||||
- Name: [Research topic]
|
||||
- Status: "Done"
|
||||
- AI used: "Claude Desktop"
|
||||
- AI summary: 2-3 sentence summary
|
||||
|
||||
## Phase 6: Blog Draft
|
||||
|
||||
**Goal**: Transform research into engaging blog post.
|
||||
|
||||
Prompt user for channel selection:
|
||||
1. blog.ourdigital.org (Korean)
|
||||
2. journal.ourdigital.org (English)
|
||||
3. ourstory.day (Korean, personal)
|
||||
|
||||
Generate draft using appropriate style guide:
|
||||
- Korean channels: See `02-ourdigital-blog`
|
||||
- English channels: See `03-ourdigital-journal`
|
||||
|
||||
## Phase 7: Ulysses Export
|
||||
|
||||
**Goal**: Deliver MD file for Ulysses editing.
|
||||
|
||||
Export path: `$ULYSSES_EXPORT_PATH` from environment
|
||||
|
||||
## Phase 8: Publishing Guidance
|
||||
|
||||
Provide channel-specific checklist based on selection.
|
||||
|
||||
---
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital research [topic]" | Start Phase 1 |
|
||||
| "ourdigital 리서치 프롬프트" | Generate research prompt only |
|
||||
| "ourdigital research → blog" | Full pipeline to blog draft |
|
||||
| "ourdigital research → notion" | Research + Notion save only |
|
||||
|
||||
## Channel Reference
|
||||
|
||||
| Channel | Language | Tone |
|
||||
|---------|----------|------|
|
||||
| blog.ourdigital.org | Korean | Analytical, Educational |
|
||||
| journal.ourdigital.org | English | Reflective, Poetic |
|
||||
| ourstory.day | Korean | Personal, Intimate |
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/research-frameworks.md` - Research methodologies
|
||||
- `02-ourdigital-blog` - Blog writing skill
|
||||
- `03-ourdigital-journal` - Journal writing skill
|
||||
@@ -1,38 +1,64 @@
|
||||
<!-- Aligned to OurDigital_Writing_Style_Guide_v2.1 + Blog_Project_Instruction_v3.3 (2026-06-05) -->
|
||||
# OurDigital Blog Style Guide
|
||||
|
||||
## Brand Identity & Authority Order
|
||||
|
||||
**Brand**: OurDigital — 사람, 디지털 그리고 문화를 관찰하는 개인 디지털 연구 노트.
|
||||
|
||||
**OurDigital vs. OurDigital Clinic**: `OurDigital`은 블로그 전체 정체성. `OurDigital Clinic`은 진단형 콘텐츠, SEO/데이터 감사, 컨설팅 상품에서만 사용하는 서비스 메타포. 블로그 본문에서 전체 브랜드명으로 사용하지 않는다.
|
||||
|
||||
**Authority order** (conflicts: higher wins):
|
||||
1. `OurDigital_Blog_Project_Instruction_v3.3` — blog.ourdigital.org 최종 권위
|
||||
2. Skills Bundle `SKILL.md` files — workflow detail
|
||||
3. Project reference files (this guide, Visual Style Guide, etc.)
|
||||
4. `userStyle` — non-blog conversation only
|
||||
|
||||
---
|
||||
|
||||
## Channel-Specific Voice & Tone
|
||||
|
||||
### blog.ourdigital.org (Korean)
|
||||
**Voice**: 전문적이면서 친근한 선배 마케터
|
||||
**Tone**: 실용적, 데이터 기반, 인사이트 중심
|
||||
|
||||
**Platform**: Ghost CMS
|
||||
**Voice**: 분석적이면서 개인적인 관찰자 — 호기심 어린 실무자이자 사유자. 가르치는 선배가 아니라 함께 생각하는 동료.
|
||||
**Tone**: 차분하고 성찰적; 에세이는 사려 깊게, 분석은 객관적으로, 비평은 날카롭되 공정하게.
|
||||
|
||||
Writing patterns:
|
||||
- 제목: 핵심 키워드 포함, 30자 이내
|
||||
- 도입부: 독자의 고민/질문으로 시작
|
||||
- 본문: 번호 매기기보다 소제목 활용
|
||||
- 전문용어: 한글(영문) 형식 - 예: 검색엔진최적화(SEO)
|
||||
- 문장: ~입니다/~습니다 경어체
|
||||
- 단락: 3-4문장, 모바일 가독성 고려
|
||||
- 제목: SEO 키워드 자연스럽게 포함, **40자 내외 기준 (≤60자)**
|
||||
- 도입부: 개인 관찰·장면·질문·역설·데이터 중 하나로 시작; 독자 고민 직접 나열 지양
|
||||
- 본문: 소제목 활용; 분석-서사-질문 교차; 최소 1회 관점 전환 또는 핵심 긴장 포함
|
||||
- 전문용어: **첫 등장 시 영문 병기** — 예: `검색엔진 최적화(SEO)`
|
||||
- 문장: **`~다`, `~이다`, `~한다` 평서체** (경어체 `~합니다/~입니다` 사용 금지)
|
||||
- 단락: 3-4문장, 모바일 가독성 고려; 문장 길이에 변화를 줌
|
||||
- 마무리: 열린 질문 또는 다음 사유의 출발점; 단정적 결론 지양
|
||||
|
||||
Example opening:
|
||||
Writing principles (Writing Style Guide v2.1 §3-4):
|
||||
- **철학-기술 융합**: 기술 주제 이면의 인간적 함의를 탐구한다.
|
||||
- **긴장과 역설**: 억지로 넣지 않되, 핵심 긴장·관점 전환·역설·열린 질문 중 하나 이상을 자연스럽게 포함한다.
|
||||
- **분석적이면서 개인적**: 데이터/논거를 제시하되 1인칭 경험·관찰을 자연스럽게 엮는다. 논문이 아니라 에세이.
|
||||
|
||||
Example opening (올바른 톤):
|
||||
```
|
||||
"구글 상위 노출, 왜 이렇게 어려울까요?
|
||||
많은 마케터들이 SEO에 시간을 투자하지만
|
||||
결과가 보이지 않아 좌절합니다.
|
||||
오늘은 실제로 효과를 본 전략 3가지를 공유합니다."
|
||||
검색엔진 최적화(SEO)를 가장 잘하는 방법이 뭐냐고 물으면,
|
||||
나는 종종 엉뚱한 대답을 한다. "SEO를 잊어버리세요."
|
||||
|
||||
알고리즘을 쫓는 사람은 항상 알고리즘에 뒤처진다는 역설.
|
||||
구글이 원하는 건 구글을 위한 콘텐츠가 아니라, 사람을 위한 콘텐츠다.
|
||||
```
|
||||
|
||||
### journal.ourdigital.org (English)
|
||||
**Voice**: Thoughtful industry analyst
|
||||
**Tone**: Insightful, evidence-based, forward-looking
|
||||
|
||||
**Voice**: Thoughtful industry analyst — reflective, personal, essayistic
|
||||
**Tone**: Insightful, evidence-based, forward-looking; confident but not arrogant
|
||||
|
||||
Writing patterns:
|
||||
- Headlines: Clear value proposition, under 60 chars
|
||||
- Opening: Hook with industry trend or data point
|
||||
- Body: Structured arguments with supporting evidence
|
||||
- Opening: Hook with personal observation, industry trend, or data point
|
||||
- Body: Structured arguments with supporting evidence; analysis and personal observation interwoven
|
||||
- Terminology: Define jargon on first use
|
||||
- Style: Active voice, varied sentence length
|
||||
- Style: Active voice, varied sentence length; short sentences as default
|
||||
- Paragraphs: 2-4 sentences for scannability
|
||||
- Closing: Open question or reflection — avoid definitive wrap-ups
|
||||
|
||||
Example opening:
|
||||
```
|
||||
@@ -43,17 +69,19 @@ and what it means for your strategy."
|
||||
```
|
||||
|
||||
### ourstory.day (Korean)
|
||||
**Voice**: 성찰하는 동료, 이야기꾼
|
||||
|
||||
**Voice**: 성찰하는 동료, 이야기꾼 — 개인 에세이, 삶의 성찰, 일상의 관찰
|
||||
**Tone**: 개인적, 진솔한, 영감을 주는
|
||||
|
||||
Writing patterns:
|
||||
- 제목: 감성적, 질문형 또는 은유적
|
||||
- 도입부: 개인 경험이나 장면 묘사로 시작
|
||||
- 본문: 이야기 흐름, 대화체 허용
|
||||
- 문장: ~해요/~네요 부드러운 경어체 가능
|
||||
- 단락: 자유로운 길이, 호흡에 따라
|
||||
- 마무리: 열린 질문 또는 여운
|
||||
|
||||
> **Channel boundary**: 순수 개인 에세이, 감정적 성찰은 `ourstory.day`. 기술 분석+철학적 사유는 `blog.ourdigital.org`. 둘을 혼동하지 않는다.
|
||||
|
||||
Example opening:
|
||||
```
|
||||
"새벽 5시, 아이를 깨우지 않으려 살금살금 책상에 앉았다.
|
||||
@@ -63,6 +91,7 @@ Example opening:
|
||||
```
|
||||
|
||||
### Medium (English)
|
||||
|
||||
**Voice**: Knowledgeable peer sharing discoveries
|
||||
**Tone**: Conversational, practical, slightly informal
|
||||
|
||||
@@ -82,33 +111,40 @@ Last month, I ran an experiment that changed how I think
|
||||
about content strategy entirely. Let me walk you through it."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Universal Guidelines
|
||||
|
||||
### SEO Considerations
|
||||
### SEO & Metadata
|
||||
|
||||
- Primary keyword in title and first 100 words
|
||||
- Secondary keywords naturally distributed
|
||||
- Meta description: 150-160 chars, action-oriented
|
||||
- URL slug: Short, keyword-rich, no dates
|
||||
- **Meta description: ≤155 chars**, action-oriented, captures the article's question and reader value
|
||||
- **URL slug: English, keyword-rich, no dates** — even for Korean-title posts
|
||||
- Alt text for all images
|
||||
|
||||
### Formatting Rules
|
||||
|
||||
- Use `##` for main sections, `###` for subsections
|
||||
- Code blocks with language specification
|
||||
- Blockquotes for key insights or quotes
|
||||
- Bold for emphasis (sparingly)
|
||||
- Lists only when truly listing items
|
||||
- Lists only when truly listing items; avoid in essay-form posts
|
||||
|
||||
### Citation Style
|
||||
|
||||
- Inline links preferred over footnotes
|
||||
- Source attribution: "According to [Source Name](URL)..."
|
||||
- Data citations: Include date of data
|
||||
- Internal links: Reference related OurDigital posts
|
||||
|
||||
---
|
||||
|
||||
## Word Count Guidelines
|
||||
|
||||
| Channel | Target | Min | Max |
|
||||
|---------|--------|-----|-----|
|
||||
| blog.ourdigital.org | 1,500 | 1,000 | 2,500 |
|
||||
| journal.ourdigital.org | 1,800 | 1,200 | 3,000 |
|
||||
| ourstory.day | 1,000 | 500 | 2,000 |
|
||||
| Medium | 1,500 | 800 | 2,500 |
|
||||
| blog.ourdigital.org | 2,000자 | 1,000자 | 3,000자 |
|
||||
| journal.ourdigital.org | 1,500 words | 1,000 words | 2,000 words |
|
||||
| ourstory.day | 1,000자 | 800자 | 1,500자 |
|
||||
| Medium | 1,500 words | 800 words | 2,500 words |
|
||||
|
||||
155
custom-skills/05-ourdigital-document/SKILL.md
Normal file
155
custom-skills/05-ourdigital-document/SKILL.md
Normal file
@@ -0,0 +1,155 @@
|
||||
---
|
||||
name: ourdigital-document
|
||||
description: |
|
||||
Notion-to-presentation workflow for OurDigital.
|
||||
Activated with "ourdigital" keyword for document creation.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital document", "our document"
|
||||
- "ourdigital 문서", "our 문서"
|
||||
- "ourdigital presentation", "our presentation"
|
||||
- "ourdigital 발표자료", "our 발표자료"
|
||||
|
||||
Features:
|
||||
- Notion research extraction
|
||||
- Content synthesis and structuring
|
||||
- Branded presentation generation
|
||||
- PowerPoint and Figma output
|
||||
version: "1.1"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Document
|
||||
|
||||
Transform Notion research into branded presentations for OurDigital workflows.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital document" / "our document"
|
||||
- "ourdigital 발표자료" / "our 발표자료"
|
||||
- "our presentation [topic]"
|
||||
- "ourdigital presentation on [topic]"
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
Phase 1: Research Collection → Phase 2: Content Synthesis → Phase 3: Presentation Planning
|
||||
↓
|
||||
Phase 4: Slide Generation → Phase 5: Brand Application → Phase 6: Export
|
||||
```
|
||||
|
||||
## Phase 1: Research Collection
|
||||
|
||||
**Goal**: Extract research content from Notion.
|
||||
|
||||
Input sources:
|
||||
- **Notion Page**: `notion://page/[ID]` - Single research document
|
||||
- **Notion Database**: `notion://database/[ID]` - Collection query
|
||||
- **Multiple Sources**: Comma-separated URLs for synthesis
|
||||
|
||||
Tools to use:
|
||||
- `Notion:notion-search` - Find related content
|
||||
- `Notion:notion-fetch` - Extract page content
|
||||
|
||||
**Output**: Structured research.json with findings
|
||||
|
||||
## Phase 2: Content Synthesis
|
||||
|
||||
**Goal**: Analyze and structure extracted content.
|
||||
|
||||
Processing:
|
||||
1. Identify key topics and themes
|
||||
2. Extract supporting data/statistics
|
||||
3. Prioritize by relevance and impact
|
||||
4. Generate executive summary
|
||||
|
||||
**Output**: synthesis.json with:
|
||||
- Executive summary
|
||||
- Key topics (ranked)
|
||||
- Agenda items
|
||||
- Supporting data points
|
||||
|
||||
## Phase 3: Presentation Planning
|
||||
|
||||
**Goal**: Create slide-by-slide structure.
|
||||
|
||||
Presentation types:
|
||||
| Type | Slides | Focus |
|
||||
|------|--------|-------|
|
||||
| Executive Summary | 3-5 | High-level findings, KPIs |
|
||||
| Research Report | 10-20 | Detailed methodology, data viz |
|
||||
| Meeting Prep | 5-10 | Agenda-driven, decision points |
|
||||
|
||||
**Output**: Slide plan with:
|
||||
- Title + subtitle per slide
|
||||
- Content outline
|
||||
- Speaker notes
|
||||
- Visual suggestions
|
||||
|
||||
## Phase 4: Slide Generation
|
||||
|
||||
**Goal**: Generate presentation content.
|
||||
|
||||
Slide structure:
|
||||
```
|
||||
├── Title Slide (project name, date, author)
|
||||
├── Agenda (numbered topics)
|
||||
├── Content Slides (1-3 per topic)
|
||||
│ ├── Key finding header
|
||||
│ ├── Supporting points (3-5 bullets)
|
||||
│ └── Data visualization placeholder
|
||||
├── Summary Slide (key takeaways)
|
||||
└── Next Steps / Q&A
|
||||
```
|
||||
|
||||
## Phase 5: Brand Application
|
||||
|
||||
**Goal**: Apply OurDigital corporate styling.
|
||||
|
||||
Brand elements:
|
||||
- **Colors**: OurDigital palette from `01-ourdigital-brand-guide`
|
||||
- **Fonts**: Noto Sans KR (Korean), Inter (English)
|
||||
- **Logo**: Positioned per brand guidelines
|
||||
- **Spacing**: Consistent margins and padding
|
||||
|
||||
Configuration: `shared/references/brand-config.json`
|
||||
|
||||
## Phase 6: Export
|
||||
|
||||
**Goal**: Generate final deliverable.
|
||||
|
||||
Output formats:
|
||||
- **PowerPoint (.pptx)**: Full presentation with animations
|
||||
- **Figma Slides**: Web-based collaborative format
|
||||
- **HTML Preview**: Quick review before final export
|
||||
|
||||
Export paths:
|
||||
- Desktop: `~/Downloads/presentations/`
|
||||
- Figma: Via Figma API
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital document [Notion URL]" | Full pipeline |
|
||||
| "ourdigital 발표자료 만들어줘" | Korean trigger |
|
||||
| "ourdigital presentation → pptx" | PowerPoint output |
|
||||
| "ourdigital presentation → figma" | Figma output |
|
||||
|
||||
## Presentation Templates
|
||||
|
||||
| Template | Use Case |
|
||||
|----------|----------|
|
||||
| Executive | Board meetings, C-level briefs |
|
||||
| Research | Deep-dive analysis, team reviews |
|
||||
| Meeting | Weekly syncs, project updates |
|
||||
| Workshop | Training, collaborative sessions |
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/slide-layouts.md` - Layout options
|
||||
- `shared/references/agenda-templates.md` - Structure templates
|
||||
- `01-ourdigital-brand-guide` - Brand guidelines
|
||||
- `04-ourdigital-research` - Research workflow integration
|
||||
145
custom-skills/06-ourdigital-designer/SKILL.md
Normal file
145
custom-skills/06-ourdigital-designer/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: ourdigital-designer
|
||||
description: |
|
||||
Visual storytelling and image prompt generation for OurDigital.
|
||||
Activated with "ourdigital" keyword for design tasks.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital design", "our design"
|
||||
- "ourdigital 디자인", "our 디자인"
|
||||
- "ourdigital image prompt", "our image prompt"
|
||||
- "ourdigital 썸네일", "our 썸네일"
|
||||
|
||||
Features:
|
||||
- Philosophical visual narrative creation
|
||||
- Image prompt generation for AI art tools
|
||||
- Korean-Western aesthetic fusion
|
||||
- Blog featured image optimization
|
||||
version: "1.1"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Designer
|
||||
|
||||
Transform philosophical essays into sophisticated visual narratives through minimalist, conceptually rich featured images.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital design" / "our design"
|
||||
- "ourdigital 썸네일" / "our 썸네일"
|
||||
- "our image prompt for [topic]"
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
OurDigital images are visual philosophy—not illustrations but parallel texts that invite contemplation. Each image captures the essay's philosophical core through:
|
||||
|
||||
- **Abstract metaphors** over literal representations
|
||||
- **Contemplative minimalism** with 20%+ negative space
|
||||
- **Cultural fusion** of Korean-Western aesthetics
|
||||
- **Emotional resonance** through color psychology
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Extract Essay Essence
|
||||
|
||||
Analyze the content for:
|
||||
- **Core insight**: What philosophical truth?
|
||||
- **Emotional tone**: What feeling to evoke?
|
||||
- **Key metaphor**: What visual symbol?
|
||||
|
||||
### Phase 2: Select Visual Approach
|
||||
|
||||
| Essay Type | Visual Strategy | Color Mood |
|
||||
|-----------|-----------------|------------|
|
||||
| Technology | Organic-digital hybrids | Cool blues → warm accents |
|
||||
| Social | Network patterns, human fragments | Desaturated → hope spots |
|
||||
| Philosophy | Zen space, symbolic objects | Monochrome + single accent |
|
||||
| Cultural | Layered traditions, fusion forms | Earth tones → modern hues |
|
||||
|
||||
### Phase 3: Generate Prompt
|
||||
|
||||
Build structured prompt with:
|
||||
```
|
||||
[Style] + [Subject] + [Composition] + [Color] + [Technical specs]
|
||||
```
|
||||
|
||||
### Phase 4: Quality Check
|
||||
|
||||
✅ **Must have:**
|
||||
- Captures philosophical insight
|
||||
- Works at 200px thumbnail
|
||||
- Timeless (2-3 year relevance)
|
||||
- Cross-cultural readability
|
||||
|
||||
❌ **Must avoid:**
|
||||
- Tech clichés (circuits, binary)
|
||||
- Stock photo aesthetics
|
||||
- Literal interpretations
|
||||
- Trendy effects
|
||||
|
||||
## Quick Templates
|
||||
|
||||
### AI & Humanity
|
||||
```
|
||||
"Translucent human silhouette dissolving into crystalline data structures.
|
||||
Monochrome with teal accent. Boundary dissolution between organic/digital.
|
||||
1200x630px, minimalist vector style."
|
||||
```
|
||||
|
||||
### Social Commentary
|
||||
```
|
||||
"Overlapping circles forming maze pattern, tiny humans in separate chambers.
|
||||
Blue-gray palette, warm light leaks for hope. Subtle Korean patterns.
|
||||
High negative space. 1200x630px."
|
||||
```
|
||||
|
||||
### Digital Transformation
|
||||
```
|
||||
"Traditional forms metamorphosing into particle streams. Paper texture → digital grain.
|
||||
Earth tones shifting to cool blues. Sacred geometry underlying.
|
||||
1200x630px, contemplative mood."
|
||||
```
|
||||
|
||||
## Visual Metaphor Shortcuts
|
||||
|
||||
| Concept | Visual Metaphor |
|
||||
|---------|-----------------|
|
||||
| Algorithm | Constellation patterns |
|
||||
| Identity | Layered masks, fingerprints |
|
||||
| Network | Root systems, neural paths |
|
||||
| Time | Spirals, sediment layers |
|
||||
| Knowledge | Light sources, growing trees |
|
||||
|
||||
## Color Psychology
|
||||
|
||||
| Mood | Palette |
|
||||
|------|---------|
|
||||
| Critical | Deep blue-gray + red accent |
|
||||
| Hopeful | Warm amber + sky blue |
|
||||
| Philosophical | Near black + off white + gold |
|
||||
| Anxious | Charcoal + grey-blue + digital green |
|
||||
|
||||
## Technical Specs
|
||||
|
||||
- **Dimensions**: 1200x630px (OG standard)
|
||||
- **Style**: Vector illustration + subtle textures
|
||||
- **Colors**: 60-30-10 rule (dominant-secondary-accent)
|
||||
- **Format**: WebP primary, JPG fallback
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital design [topic]" | Generate image prompt |
|
||||
| "ourdigital 썸네일 [주제]" | Korean trigger |
|
||||
| "ourdigital visual → midjourney" | MidJourney optimized |
|
||||
| "ourdigital visual → dalle" | DALL-E optimized |
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/visual-metaphors.md` - Concept dictionary
|
||||
- `shared/references/color-palettes.md` - Emotion → color mapping
|
||||
- `shared/references/advanced-techniques.md` - Complex compositions
|
||||
- `01-ourdigital-brand-guide` - Brand visual identity
|
||||
@@ -1,101 +1,104 @@
|
||||
# Visual Metaphor Dictionary
|
||||
|
||||
Quick reference for translating abstract concepts into visual elements.
|
||||
<!-- Aligned to OurDigital_Visual_Style_Guide_v2.1 (2026-06-05) -->
|
||||
|
||||
Quick reference for translating abstract concepts into visual elements.
|
||||
Default direction: **bright editorial minimalism with conceptual depth** — light/warm-neutral backgrounds, 30%+ negative space, one clear focal metaphor, one human-scale anchor.
|
||||
|
||||
## Technology & Digital
|
||||
|
||||
| Concept | Primary Metaphor | Alternative Visuals |
|
||||
|---------|-----------------|-------------------|
|
||||
| Algorithm | Constellation patterns | Maze structures, flow charts as art |
|
||||
| AI | Crystalline growth | Mirror reflections, fractal patterns |
|
||||
| Data | Water flow, particles | Bird murmurations, sand grains |
|
||||
| Network | Root systems | Neural pathways, spider silk, web |
|
||||
| Code | Musical notation | DNA strands, city blueprints |
|
||||
| Cloud | Atmospheric forms | Floating islands, ethereal spaces |
|
||||
| Privacy | Veils, shadows | One-way mirrors, fog, barriers |
|
||||
| Security | Locks dissolving | Fortresses becoming permeable |
|
||||
| Automation | Clockwork organic | Self-assembling structures |
|
||||
| Virtual | Layers of reality | Parallel dimensions, glass planes |
|
||||
| Concept | Preferred Metaphor | Alternatives | Avoid |
|
||||
|---------|-------------------|--------------|-------|
|
||||
| AI | Co-writing desk, translucent companion shape, mirror | Soft geometric overlay, window reflection | Robot face, glowing brain, Terminator mood, crystalline growth |
|
||||
| Algorithm | Path map, constellation over paper, sorting trays | Gentle maze, flow chart as art | Black-box cube, surveillance grid |
|
||||
| Data | Flowing dots, paper charts, seed-like particles | Water stream, gentle scatter | Neon matrix, endless binary code |
|
||||
| SEO/Search | Compass, map, signpost, light through shelves | Library index, folded path | Magnifying glass cliché alone |
|
||||
| Automation | Clockwork garden, conveyor of paper, small helpful mechanism | Self-assembling organic structure | Industrial robot arm, factory dystopia |
|
||||
| Network | Roots, threads, bridges, neurons, community table | Constellation, river delta | Spider web trap, dark cables |
|
||||
| Privacy | Curtain, frosted glass, closed notebook, soft boundary | One-way window | Lock icon alone, heavy shadow |
|
||||
| Content | Notebook, seeds, layered paper, archive boxes, small lamp | Open shelves | Generic document icons |
|
||||
| Code | Musical notation, blueprint detail | DNA strands | Heavy terminal/green-code imagery |
|
||||
|
||||
## Social & Cultural
|
||||
|
||||
| Concept | Primary Metaphor | Alternative Visuals |
|
||||
|---------|-----------------|-------------------|
|
||||
| Identity | Layered masks | Fingerprints merging, mirrors |
|
||||
| Community | Overlapping circles | Shared spaces, woven threads |
|
||||
| Isolation | Islands in fog | Glass barriers, empty chairs |
|
||||
| Communication | Bridge structures | Echo patterns, light beams |
|
||||
| Conflict | Opposing forces | Tectonic plates, storm systems |
|
||||
| Harmony | Resonance patterns | Orchestra arrangements, balance |
|
||||
| Culture | Textile patterns | Layered sediments, palimpsest |
|
||||
| Tradition | Tree rings | Ancient stones, inherited objects |
|
||||
| Change | Metamorphosis | Phase transitions, seasonal cycles |
|
||||
| Power | Pyramids inverting | Current flows, gravity wells |
|
||||
| Concept | Preferred Metaphor | Alternatives | Avoid |
|
||||
|---------|-------------------|--------------|-------|
|
||||
| Identity | Layered paper portrait, reflection in window, fingerprint as landscape | Translucent profile | Faceless mask overload |
|
||||
| Community | Shared table, overlapping circles, lighted windows | Small bridges, woven threads | Crowd silhouettes in darkness |
|
||||
| Isolation | Single lit desk, island of paper, window distance | Empty chair in warm light | Lonely person in black void |
|
||||
| Communication | Threads, folded letters, bridge, echo rings | Light beams | Speech bubble clutter |
|
||||
| Trust | Clear water, open notebook, steady lamp | Transparent materials | Handshake stock image |
|
||||
| Reputation | Ripples, layered traces, visible footprints | Soft badges | Star-rating cliché |
|
||||
| Change | Folded paper becoming path, seed from circuit, gentle transition | Phase shift, seasonal cycle | Dramatic explosion, shattered pattern |
|
||||
|
||||
## Philosophical & Abstract
|
||||
|
||||
| Concept | Primary Metaphor | Alternative Visuals |
|
||||
|---------|-----------------|-------------------|
|
||||
| Time | Spirals, loops | Sediment layers, clock dissolution |
|
||||
| Knowledge | Light sources | Growing trees, opening books |
|
||||
| Wisdom | Mountain vistas | Deep waters, ancient libraries |
|
||||
| Truth | Clear water | Prisms splitting light, unveiled |
|
||||
| Illusion | Distorted mirrors | Smoke shapes, double images |
|
||||
| Choice | Diverging paths | Doors opening, quantum splits |
|
||||
| Balance | Tensegrity | Scales reimagined, equilibrium |
|
||||
| Paradox | Möbius strips | Impossible objects, Escher-like |
|
||||
| Existence | Breath patterns | Pulse rhythms, presence/absence |
|
||||
| Consciousness | Nested awareness | Recursive mirrors, awakening |
|
||||
| Concept | Preferred Metaphor | Alternatives | Avoid |
|
||||
|---------|-------------------|--------------|-------|
|
||||
| Time | Calendar pages, soft spiral, sediment-like paper layers | Loops, tide marks | Melting clock cliché |
|
||||
| Knowledge | Lamp, window light, open book, growing plant | Expanding shelves | Glowing brain |
|
||||
| Uncertainty | Foggy path, half-open door, incomplete map | Soft blur at edges | Storm clouds only |
|
||||
| Balance | Asymmetrical stones, mobile, table edge, quiet scale | Tensegrity | Literal scale icon only |
|
||||
| Paradox | Möbius paper strip, two paths meeting, shadow/light on same object | Nested frames | Escher-like complexity overload |
|
||||
| Wisdom | Window overlooking depth, layered book spines | Ancient stone in light | Heavy mystical imagery |
|
||||
| Choice | Diverging paths, open doors, fork in paper map | Branching thread | Dramatic split/explosion |
|
||||
|
||||
## Emotional States
|
||||
|
||||
| Emotion | Visual Translation | Color Association |
|
||||
|---------|-------------------|------------------|
|
||||
| Anxiety | Fragmented grids | Desaturated, glitch |
|
||||
| Hope | Light breaking through | Warm gradients |
|
||||
| Melancholy | Soft dissolution | Muted blues, grays |
|
||||
| Joy | Expansion patterns | Bright, ascending |
|
||||
| Fear | Contracting spaces | Sharp contrasts |
|
||||
| Peace | Still water | Soft neutrals |
|
||||
| Confusion | Tangled lines | Overlapping hues |
|
||||
| Clarity | Clean geometry | Pure, minimal |
|
||||
| Emotion | Visual Translation | Color Guidance |
|
||||
|---------|-------------------|----------------|
|
||||
| Anxiety | Fragmented grids, incomplete maps | Desaturated warm neutrals; avoid pure glitch |
|
||||
| Hope | Light breaking through window, seedling | Warm gradients on light background |
|
||||
| Melancholy | Single lit desk, soft dissolution | Muted blues on ivory; avoid black void |
|
||||
| Joy | Expansion patterns, open space | Bright, ascending on warm white |
|
||||
| Peace | Still water, open notebook | Soft neutrals, generous negative space |
|
||||
| Clarity | Clean geometry, clear window | Pure, minimal with one accent |
|
||||
|
||||
## Transformation & Process
|
||||
## Signature Motifs (Brand Consistency)
|
||||
|
||||
| Process | Visual Narrative | Symbolic Elements |
|
||||
|---------|------------------|------------------|
|
||||
| Growth | Seeds → trees | Fibonacci spirals |
|
||||
| Decay | Entropy patterns | Rust, dissolution |
|
||||
| Evolution | Branching forms | Darwin's tree reimagined |
|
||||
| Revolution | Circles breaking | Shattered patterns |
|
||||
| Innovation | Spark → flame | Lightning, fusion |
|
||||
| Tradition | Continuous thread | Inherited patterns |
|
||||
| Disruption | Broken grids | Glitch aesthetics |
|
||||
| Integration | Merging streams | Confluence points |
|
||||
| Motif | Description |
|
||||
|-------|-------------|
|
||||
| **Threshold Spaces** | Doorways, bridges, windows, paths, liminal rooms — transition |
|
||||
| **Network Organic** | Roots, threads, neurons, constellations as soft digital networks |
|
||||
| **Fragment Philosophy** | Folded paper, layered cards, gentle fragmentation and reassembly |
|
||||
| **Light Studies** | Knowledge/uncertainty via window light, lamps, soft gradients |
|
||||
| **Human Traces** | Hands, desks, notebooks, chairs, small figures within conceptual scenes |
|
||||
|
||||
## Korean-Western Fusion Elements
|
||||
## Recommended Color Palettes
|
||||
|
||||
| Korean Element | Western Parallel | Fusion Approach |
|
||||
|---------------|-----------------|-----------------|
|
||||
| 여백 (Empty space) | Negative space | Active emptiness |
|
||||
| 오방색 (Five colors) | Color theory | Symbolic palette |
|
||||
| 달항아리 (Moon jar) | Minimalism | Imperfect circles |
|
||||
| 한글 geometry | Typography | Structural letters |
|
||||
| 산수화 (Landscape) | Abstract landscape | Atmospheric depth |
|
||||
| 전통문양 (Patterns) | Geometric design | Cultural geometry |
|
||||
| Palette | Background | Accent | Mood |
|
||||
|---------|-----------|--------|------|
|
||||
| Morning Desk | `#F7F3EA` warm ivory | `#4A90A4` muted teal | calm, thoughtful |
|
||||
| Soft Technology | `#F6F8FA` cloud white | `#F2A65A` warm amber | clear, quietly optimistic |
|
||||
| Warm Data | `#FFF8EF` soft cream | `#5EAAA8` soft turquoise | human, analytical |
|
||||
| Clear Critique | `#F4F6F8` light gray | `#E07A5F` soft coral | critical but not aggressive |
|
||||
| Human Network | `#FAF9F6` off-white | `#8AB17D` muted green | organic, connected |
|
||||
|
||||
**Dark color limit:** max 15% of canvas; never full dark background by default.
|
||||
|
||||
## Korean Design Elements
|
||||
|
||||
| Korean Element | Western Parallel | Application |
|
||||
|----------------|-----------------|-------------|
|
||||
| 여백 (Empty space) | Negative space | Active emptiness — 30%+ default |
|
||||
| 달항아리 (Moon jar) | Minimalism | Roundness, asymmetry, soft white |
|
||||
| 한지 texture | Paper grain | Subtle background texture only |
|
||||
| 산수화 (Landscape) | Editorial landscape | Atmospheric depth without ornament |
|
||||
|
||||
**Avoid:** decorative oriental motifs, calligraphy clichés, 오방색 as obvious symbols — express Korean sensibility through spacing and restraint, not surface pattern.
|
||||
|
||||
## Usage Notes
|
||||
|
||||
1. **Layer metaphors**: Combine 2-3 for depth
|
||||
2. **Avoid clichés**: No obvious tech symbols
|
||||
3. **Cultural sensitivity**: Universal over specific
|
||||
4. **Abstraction levels**: Match essay tone
|
||||
5. **Emotional resonance**: Feel over literal
|
||||
1. **Layer metaphors**: Combine 2–3 elements; one clear focal object + one human-scale anchor
|
||||
2. **Avoid clichés**: No obvious tech icons (robot, glowing brain, magnifying glass alone)
|
||||
3. **Match essay tone**: bright → practical guides; balanced → analysis; warmer → personal reflections
|
||||
4. **30%+ negative space**: default; minimum 20% only when composition needs density
|
||||
5. **Human trace required**: include hand, desk, figure, window, or everyday object unless topic demands pure abstraction
|
||||
|
||||
## Quick Selection Guide
|
||||
|
||||
For **technology essays**: organic-digital hybrids
|
||||
For **social commentary**: human elements in systems
|
||||
For **philosophy pieces**: space and light
|
||||
For **cultural topics**: layered traditions
|
||||
For **future themes**: transformation states
|
||||
For **technology essays**: organic-digital hybrid forms in a light, human-scale environment
|
||||
For **social commentary**: network patterns with small human traces
|
||||
For **philosophy pieces**: calm symbolic scenes with Zen-like spacing
|
||||
For **practical guides**: clean editorial diagrams with warm accents
|
||||
For **personal reflections**: soft everyday scenes with metaphorical detail
|
||||
|
||||
@@ -1,101 +1,104 @@
|
||||
# Visual Metaphor Dictionary
|
||||
|
||||
Quick reference for translating abstract concepts into visual elements.
|
||||
<!-- Aligned to OurDigital_Visual_Style_Guide_v2.1 (2026-06-05) -->
|
||||
|
||||
Quick reference for translating abstract concepts into visual elements.
|
||||
Default direction: **bright editorial minimalism with conceptual depth** — light/warm-neutral backgrounds, 30%+ negative space, one clear focal metaphor, one human-scale anchor.
|
||||
|
||||
## Technology & Digital
|
||||
|
||||
| Concept | Primary Metaphor | Alternative Visuals |
|
||||
|---------|-----------------|-------------------|
|
||||
| Algorithm | Constellation patterns | Maze structures, flow charts as art |
|
||||
| AI | Crystalline growth | Mirror reflections, fractal patterns |
|
||||
| Data | Water flow, particles | Bird murmurations, sand grains |
|
||||
| Network | Root systems | Neural pathways, spider silk, web |
|
||||
| Code | Musical notation | DNA strands, city blueprints |
|
||||
| Cloud | Atmospheric forms | Floating islands, ethereal spaces |
|
||||
| Privacy | Veils, shadows | One-way mirrors, fog, barriers |
|
||||
| Security | Locks dissolving | Fortresses becoming permeable |
|
||||
| Automation | Clockwork organic | Self-assembling structures |
|
||||
| Virtual | Layers of reality | Parallel dimensions, glass planes |
|
||||
| Concept | Preferred Metaphor | Alternatives | Avoid |
|
||||
|---------|-------------------|--------------|-------|
|
||||
| AI | Co-writing desk, translucent companion shape, mirror | Soft geometric overlay, window reflection | Robot face, glowing brain, Terminator mood, crystalline growth |
|
||||
| Algorithm | Path map, constellation over paper, sorting trays | Gentle maze, flow chart as art | Black-box cube, surveillance grid |
|
||||
| Data | Flowing dots, paper charts, seed-like particles | Water stream, gentle scatter | Neon matrix, endless binary code |
|
||||
| SEO/Search | Compass, map, signpost, light through shelves | Library index, folded path | Magnifying glass cliché alone |
|
||||
| Automation | Clockwork garden, conveyor of paper, small helpful mechanism | Self-assembling organic structure | Industrial robot arm, factory dystopia |
|
||||
| Network | Roots, threads, bridges, neurons, community table | Constellation, river delta | Spider web trap, dark cables |
|
||||
| Privacy | Curtain, frosted glass, closed notebook, soft boundary | One-way window | Lock icon alone, heavy shadow |
|
||||
| Content | Notebook, seeds, layered paper, archive boxes, small lamp | Open shelves | Generic document icons |
|
||||
| Code | Musical notation, blueprint detail | DNA strands | Heavy terminal/green-code imagery |
|
||||
|
||||
## Social & Cultural
|
||||
|
||||
| Concept | Primary Metaphor | Alternative Visuals |
|
||||
|---------|-----------------|-------------------|
|
||||
| Identity | Layered masks | Fingerprints merging, mirrors |
|
||||
| Community | Overlapping circles | Shared spaces, woven threads |
|
||||
| Isolation | Islands in fog | Glass barriers, empty chairs |
|
||||
| Communication | Bridge structures | Echo patterns, light beams |
|
||||
| Conflict | Opposing forces | Tectonic plates, storm systems |
|
||||
| Harmony | Resonance patterns | Orchestra arrangements, balance |
|
||||
| Culture | Textile patterns | Layered sediments, palimpsest |
|
||||
| Tradition | Tree rings | Ancient stones, inherited objects |
|
||||
| Change | Metamorphosis | Phase transitions, seasonal cycles |
|
||||
| Power | Pyramids inverting | Current flows, gravity wells |
|
||||
| Concept | Preferred Metaphor | Alternatives | Avoid |
|
||||
|---------|-------------------|--------------|-------|
|
||||
| Identity | Layered paper portrait, reflection in window, fingerprint as landscape | Translucent profile | Faceless mask overload |
|
||||
| Community | Shared table, overlapping circles, lighted windows | Small bridges, woven threads | Crowd silhouettes in darkness |
|
||||
| Isolation | Single lit desk, island of paper, window distance | Empty chair in warm light | Lonely person in black void |
|
||||
| Communication | Threads, folded letters, bridge, echo rings | Light beams | Speech bubble clutter |
|
||||
| Trust | Clear water, open notebook, steady lamp | Transparent materials | Handshake stock image |
|
||||
| Reputation | Ripples, layered traces, visible footprints | Soft badges | Star-rating cliché |
|
||||
| Change | Folded paper becoming path, seed from circuit, gentle transition | Phase shift, seasonal cycle | Dramatic explosion, shattered pattern |
|
||||
|
||||
## Philosophical & Abstract
|
||||
|
||||
| Concept | Primary Metaphor | Alternative Visuals |
|
||||
|---------|-----------------|-------------------|
|
||||
| Time | Spirals, loops | Sediment layers, clock dissolution |
|
||||
| Knowledge | Light sources | Growing trees, opening books |
|
||||
| Wisdom | Mountain vistas | Deep waters, ancient libraries |
|
||||
| Truth | Clear water | Prisms splitting light, unveiled |
|
||||
| Illusion | Distorted mirrors | Smoke shapes, double images |
|
||||
| Choice | Diverging paths | Doors opening, quantum splits |
|
||||
| Balance | Tensegrity | Scales reimagined, equilibrium |
|
||||
| Paradox | Möbius strips | Impossible objects, Escher-like |
|
||||
| Existence | Breath patterns | Pulse rhythms, presence/absence |
|
||||
| Consciousness | Nested awareness | Recursive mirrors, awakening |
|
||||
| Concept | Preferred Metaphor | Alternatives | Avoid |
|
||||
|---------|-------------------|--------------|-------|
|
||||
| Time | Calendar pages, soft spiral, sediment-like paper layers | Loops, tide marks | Melting clock cliché |
|
||||
| Knowledge | Lamp, window light, open book, growing plant | Expanding shelves | Glowing brain |
|
||||
| Uncertainty | Foggy path, half-open door, incomplete map | Soft blur at edges | Storm clouds only |
|
||||
| Balance | Asymmetrical stones, mobile, table edge, quiet scale | Tensegrity | Literal scale icon only |
|
||||
| Paradox | Möbius paper strip, two paths meeting, shadow/light on same object | Nested frames | Escher-like complexity overload |
|
||||
| Wisdom | Window overlooking depth, layered book spines | Ancient stone in light | Heavy mystical imagery |
|
||||
| Choice | Diverging paths, open doors, fork in paper map | Branching thread | Dramatic split/explosion |
|
||||
|
||||
## Emotional States
|
||||
|
||||
| Emotion | Visual Translation | Color Association |
|
||||
|---------|-------------------|------------------|
|
||||
| Anxiety | Fragmented grids | Desaturated, glitch |
|
||||
| Hope | Light breaking through | Warm gradients |
|
||||
| Melancholy | Soft dissolution | Muted blues, grays |
|
||||
| Joy | Expansion patterns | Bright, ascending |
|
||||
| Fear | Contracting spaces | Sharp contrasts |
|
||||
| Peace | Still water | Soft neutrals |
|
||||
| Confusion | Tangled lines | Overlapping hues |
|
||||
| Clarity | Clean geometry | Pure, minimal |
|
||||
| Emotion | Visual Translation | Color Guidance |
|
||||
|---------|-------------------|----------------|
|
||||
| Anxiety | Fragmented grids, incomplete maps | Desaturated warm neutrals; avoid pure glitch |
|
||||
| Hope | Light breaking through window, seedling | Warm gradients on light background |
|
||||
| Melancholy | Single lit desk, soft dissolution | Muted blues on ivory; avoid black void |
|
||||
| Joy | Expansion patterns, open space | Bright, ascending on warm white |
|
||||
| Peace | Still water, open notebook | Soft neutrals, generous negative space |
|
||||
| Clarity | Clean geometry, clear window | Pure, minimal with one accent |
|
||||
|
||||
## Transformation & Process
|
||||
## Signature Motifs (Brand Consistency)
|
||||
|
||||
| Process | Visual Narrative | Symbolic Elements |
|
||||
|---------|------------------|------------------|
|
||||
| Growth | Seeds → trees | Fibonacci spirals |
|
||||
| Decay | Entropy patterns | Rust, dissolution |
|
||||
| Evolution | Branching forms | Darwin's tree reimagined |
|
||||
| Revolution | Circles breaking | Shattered patterns |
|
||||
| Innovation | Spark → flame | Lightning, fusion |
|
||||
| Tradition | Continuous thread | Inherited patterns |
|
||||
| Disruption | Broken grids | Glitch aesthetics |
|
||||
| Integration | Merging streams | Confluence points |
|
||||
| Motif | Description |
|
||||
|-------|-------------|
|
||||
| **Threshold Spaces** | Doorways, bridges, windows, paths, liminal rooms — transition |
|
||||
| **Network Organic** | Roots, threads, neurons, constellations as soft digital networks |
|
||||
| **Fragment Philosophy** | Folded paper, layered cards, gentle fragmentation and reassembly |
|
||||
| **Light Studies** | Knowledge/uncertainty via window light, lamps, soft gradients |
|
||||
| **Human Traces** | Hands, desks, notebooks, chairs, small figures within conceptual scenes |
|
||||
|
||||
## Korean-Western Fusion Elements
|
||||
## Recommended Color Palettes
|
||||
|
||||
| Korean Element | Western Parallel | Fusion Approach |
|
||||
|---------------|-----------------|-----------------|
|
||||
| 여백 (Empty space) | Negative space | Active emptiness |
|
||||
| 오방색 (Five colors) | Color theory | Symbolic palette |
|
||||
| 달항아리 (Moon jar) | Minimalism | Imperfect circles |
|
||||
| 한글 geometry | Typography | Structural letters |
|
||||
| 산수화 (Landscape) | Abstract landscape | Atmospheric depth |
|
||||
| 전통문양 (Patterns) | Geometric design | Cultural geometry |
|
||||
| Palette | Background | Accent | Mood |
|
||||
|---------|-----------|--------|------|
|
||||
| Morning Desk | `#F7F3EA` warm ivory | `#4A90A4` muted teal | calm, thoughtful |
|
||||
| Soft Technology | `#F6F8FA` cloud white | `#F2A65A` warm amber | clear, quietly optimistic |
|
||||
| Warm Data | `#FFF8EF` soft cream | `#5EAAA8` soft turquoise | human, analytical |
|
||||
| Clear Critique | `#F4F6F8` light gray | `#E07A5F` soft coral | critical but not aggressive |
|
||||
| Human Network | `#FAF9F6` off-white | `#8AB17D` muted green | organic, connected |
|
||||
|
||||
**Dark color limit:** max 15% of canvas; never full dark background by default.
|
||||
|
||||
## Korean Design Elements
|
||||
|
||||
| Korean Element | Western Parallel | Application |
|
||||
|----------------|-----------------|-------------|
|
||||
| 여백 (Empty space) | Negative space | Active emptiness — 30%+ default |
|
||||
| 달항아리 (Moon jar) | Minimalism | Roundness, asymmetry, soft white |
|
||||
| 한지 texture | Paper grain | Subtle background texture only |
|
||||
| 산수화 (Landscape) | Editorial landscape | Atmospheric depth without ornament |
|
||||
|
||||
**Avoid:** decorative oriental motifs, calligraphy clichés, 오방색 as obvious symbols — express Korean sensibility through spacing and restraint, not surface pattern.
|
||||
|
||||
## Usage Notes
|
||||
|
||||
1. **Layer metaphors**: Combine 2-3 for depth
|
||||
2. **Avoid clichés**: No obvious tech symbols
|
||||
3. **Cultural sensitivity**: Universal over specific
|
||||
4. **Abstraction levels**: Match essay tone
|
||||
5. **Emotional resonance**: Feel over literal
|
||||
1. **Layer metaphors**: Combine 2–3 elements; one clear focal object + one human-scale anchor
|
||||
2. **Avoid clichés**: No obvious tech icons (robot, glowing brain, magnifying glass alone)
|
||||
3. **Match essay tone**: bright → practical guides; balanced → analysis; warmer → personal reflections
|
||||
4. **30%+ negative space**: default; minimum 20% only when composition needs density
|
||||
5. **Human trace required**: include hand, desk, figure, window, or everyday object unless topic demands pure abstraction
|
||||
|
||||
## Quick Selection Guide
|
||||
|
||||
For **technology essays**: organic-digital hybrids
|
||||
For **social commentary**: human elements in systems
|
||||
For **philosophy pieces**: space and light
|
||||
For **cultural topics**: layered traditions
|
||||
For **future themes**: transformation states
|
||||
For **technology essays**: organic-digital hybrid forms in a light, human-scale environment
|
||||
For **social commentary**: network patterns with small human traces
|
||||
For **philosophy pieces**: calm symbolic scenes with Zen-like spacing
|
||||
For **practical guides**: clean editorial diagrams with warm accents
|
||||
For **personal reflections**: soft everyday scenes with metaphorical detail
|
||||
|
||||
173
custom-skills/07-ourdigital-ad-manager/SKILL.md
Normal file
173
custom-skills/07-ourdigital-ad-manager/SKILL.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: ourdigital-ad-manager
|
||||
description: |
|
||||
Ad copywriting and keyword research for OurDigital marketing.
|
||||
Activated with "ourdigital" keyword for advertising tasks.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital ad copy", "our ad copy"
|
||||
- "ourdigital 광고 카피", "our 광고 카피"
|
||||
- "ourdigital keyword", "our keyword"
|
||||
- "ourdigital 검색 광고", "our 검색 광고"
|
||||
|
||||
Features:
|
||||
- Search ad copywriting (Google, Naver)
|
||||
- Display ad copywriting
|
||||
- Branded content creation
|
||||
- Keyword volume research
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Ad Manager
|
||||
|
||||
Create compelling ad copy and research keywords for OurDigital marketing campaigns.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital ad copy" / "our ad copy"
|
||||
- "ourdigital 광고 카피" / "our 광고 카피"
|
||||
- "our keyword research [topic]"
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Campaign Brief
|
||||
|
||||
Gather information:
|
||||
- **Product/Service**: What are we advertising?
|
||||
- **Target audience**: Who are we reaching?
|
||||
- **Campaign goal**: Awareness, consideration, or conversion?
|
||||
- **Platform**: Google, Naver, Meta, Display?
|
||||
- **Budget tier**: Affects keyword competitiveness
|
||||
|
||||
### Phase 2: Keyword Research
|
||||
|
||||
For search campaigns:
|
||||
1. **Seed keywords**: Core terms from brief
|
||||
2. **Volume research**: Web search for search volume data
|
||||
3. **Intent mapping**: Informational → Transactional
|
||||
4. **Competitor analysis**: Top-ranking ad copy patterns
|
||||
|
||||
Tools to use:
|
||||
- `web_search`: Search volume and trends
|
||||
- `web_fetch`: Competitor ad copy analysis
|
||||
|
||||
### Phase 3: Ad Copy Creation
|
||||
|
||||
Generate platform-specific copy following character limits and best practices.
|
||||
|
||||
## Search Ad Copy
|
||||
|
||||
### Google Ads Format
|
||||
|
||||
```
|
||||
Headline 1: [30 chars] - Primary keyword + value prop
|
||||
Headline 2: [30 chars] - Benefit or CTA
|
||||
Headline 3: [30 chars] - Differentiator
|
||||
Description 1: [90 chars] - Expand on value
|
||||
Description 2: [90 chars] - CTA + urgency
|
||||
```
|
||||
|
||||
**Best Practices:**
|
||||
- Include keyword in Headline 1
|
||||
- Numbers and specifics increase CTR
|
||||
- Test emotional vs. rational appeals
|
||||
- Include pricing if competitive
|
||||
|
||||
### Naver Search Ad Format
|
||||
|
||||
```
|
||||
제목: [25자] - 핵심 키워드 + 가치
|
||||
설명: [45자] - 혜택 + 행동 유도
|
||||
```
|
||||
|
||||
**Korean Ad Copy Tips:**
|
||||
- 존댓말 일관성 유지
|
||||
- 숫자와 구체적 혜택 강조
|
||||
- 신뢰 요소 포함 (경력, 인증)
|
||||
|
||||
## Display Ad Copy
|
||||
|
||||
### Headlines by Format
|
||||
|
||||
| Format | Max Length | Focus |
|
||||
|--------|------------|-------|
|
||||
| Leaderboard | 25 chars | Brand + single benefit |
|
||||
| Medium Rectangle | 30 chars | Offer + CTA |
|
||||
| Responsive | 30 chars | Multiple variations |
|
||||
|
||||
### Copy Formula
|
||||
|
||||
```
|
||||
[Problem Recognition] + [Solution Hint] + [CTA]
|
||||
"여전히 [문제]? [해결책]으로 [결과]"
|
||||
```
|
||||
|
||||
## Branded Content
|
||||
|
||||
For native advertising and sponsored content:
|
||||
|
||||
### OurDigital Tone
|
||||
|
||||
- **Authority without arrogance**: Share expertise, invite questions
|
||||
- **Data-backed claims**: Statistics increase credibility
|
||||
- **Subtle CTAs**: Education first, promotion second
|
||||
|
||||
### Content Types
|
||||
|
||||
| Type | Length | CTA Style |
|
||||
|------|--------|-----------|
|
||||
| Sponsored Article | 800-1,200 words | Soft (learn more) |
|
||||
| Native Ad | 100-200 words | Medium (discover) |
|
||||
| Social Sponsored | 50-100 words | Direct (get started) |
|
||||
|
||||
## Keyword Research Output
|
||||
|
||||
### Research Report Structure
|
||||
|
||||
```
|
||||
## Keyword Analysis: [Topic]
|
||||
|
||||
### Primary Keywords
|
||||
| Keyword | Volume | Difficulty | Intent |
|
||||
|---------|--------|------------|--------|
|
||||
| [kw1] | 10K | Medium | Trans |
|
||||
|
||||
### Long-tail Opportunities
|
||||
- [keyword phrase 1]: Low competition, high intent
|
||||
- [keyword phrase 2]: Rising trend
|
||||
|
||||
### Negative Keywords
|
||||
- [irrelevant term 1]
|
||||
- [irrelevant term 2]
|
||||
|
||||
### Recommended Ad Groups
|
||||
1. [Group Name]: kw1, kw2, kw3
|
||||
2. [Group Name]: kw4, kw5, kw6
|
||||
```
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital ad copy [product]" | Full ad set |
|
||||
| "ourdigital 검색 광고 [키워드]" | Search ads |
|
||||
| "ourdigital display ad [campaign]" | Display copy |
|
||||
| "ourdigital keyword [topic]" | Volume research |
|
||||
|
||||
## Platform Guidelines
|
||||
|
||||
| Platform | Headline | Description | Key Focus |
|
||||
|----------|----------|-------------|-----------|
|
||||
| Google | 30×3 | 90×2 | Keyword match |
|
||||
| Naver | 25 | 45 | Trust signals |
|
||||
| Meta | 40 | 125 | Visual-copy sync |
|
||||
| LinkedIn | 150 | 70 | Professional tone |
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/ad-copy-formulas.md` - Proven copy templates
|
||||
- `shared/references/platform-specs.md` - Character limits
|
||||
- `01-ourdigital-brand-guide` - Brand voice
|
||||
186
custom-skills/08-ourdigital-trainer/SKILL.md
Normal file
186
custom-skills/08-ourdigital-trainer/SKILL.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: ourdigital-trainer
|
||||
description: |
|
||||
Training material creation and workshop planning for OurDigital.
|
||||
Activated with "ourdigital" keyword for education tasks.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital training", "our training"
|
||||
- "ourdigital 교육", "our 교육"
|
||||
- "ourdigital workshop", "our workshop"
|
||||
- "ourdigital 워크샵", "our 워크샵"
|
||||
|
||||
Features:
|
||||
- Training material design
|
||||
- Workshop agenda planning
|
||||
- Participant evaluation design
|
||||
- Exercise and activity creation
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Trainer
|
||||
|
||||
Design training materials, plan workshops, and create evaluation frameworks for OurDigital education programs.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital training" / "our training"
|
||||
- "ourdigital 워크샵" / "our 워크샵"
|
||||
- "our curriculum [subject]"
|
||||
|
||||
## Core Domains
|
||||
|
||||
OurDigital training expertise:
|
||||
|
||||
| Domain | Topics |
|
||||
|--------|--------|
|
||||
| **Data Literacy** | 데이터 리터러시, 분석 기초, 시각화 |
|
||||
| **AI Literacy** | AI 활용, 프롬프트 엔지니어링, AI 윤리 |
|
||||
| **Digital Marketing** | SEO, GTM, 마케팅 자동화 |
|
||||
| **Brand Marketing** | 브랜드 전략, 콘텐츠 마케팅 |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Training Needs Analysis
|
||||
|
||||
Gather requirements:
|
||||
- **Target audience**: 직급, 경험 수준, 사전 지식
|
||||
- **Learning objectives**: 교육 후 달성할 역량
|
||||
- **Duration**: 시간 제약 (2시간/반일/전일/다회차)
|
||||
- **Format**: 온라인/오프라인/하이브리드
|
||||
- **Group size**: 참여 인원
|
||||
|
||||
### Phase 2: Curriculum Design
|
||||
|
||||
Structure the learning journey:
|
||||
|
||||
```
|
||||
Module Structure:
|
||||
├── 도입 (10-15%)
|
||||
│ ├── Ice-breaker
|
||||
│ ├── 학습 목표 공유
|
||||
│ └── 사전 지식 확인
|
||||
├── 핵심 학습 (60-70%)
|
||||
│ ├── 개념 설명
|
||||
│ ├── 사례 분석
|
||||
│ ├── 실습 활동
|
||||
│ └── 토론/질의응답
|
||||
├── 심화/응용 (15-20%)
|
||||
│ ├── 응용 과제
|
||||
│ └── 그룹 활동
|
||||
└── 마무리 (5-10%)
|
||||
├── 핵심 정리
|
||||
├── 평가
|
||||
└── 후속 학습 안내
|
||||
```
|
||||
|
||||
### Phase 3: Material Development
|
||||
|
||||
Create supporting materials:
|
||||
|
||||
| Material Type | Purpose |
|
||||
|---------------|---------|
|
||||
| 슬라이드 | 핵심 개념 전달 |
|
||||
| 핸드아웃 | 참조 자료, 체크리스트 |
|
||||
| 워크시트 | 실습 활동용 |
|
||||
| 사례 연구 | 토론 및 분석용 |
|
||||
| 퀴즈/평가지 | 학습 확인용 |
|
||||
|
||||
### Phase 4: Activity Design
|
||||
|
||||
Engagement techniques:
|
||||
|
||||
| Activity Type | Duration | Purpose |
|
||||
|---------------|----------|---------|
|
||||
| Think-Pair-Share | 5-10분 | 개별 사고 → 협력 |
|
||||
| Case Study | 20-30분 | 실제 적용력 |
|
||||
| Role Play | 15-20분 | 경험적 학습 |
|
||||
| Gallery Walk | 15분 | 아이디어 공유 |
|
||||
| Fishbowl | 20-30분 | 심층 토론 |
|
||||
|
||||
### Phase 5: Evaluation Design
|
||||
|
||||
Assessment framework:
|
||||
|
||||
| Level | What to Measure | Method |
|
||||
|-------|-----------------|--------|
|
||||
| 반응 | 만족도, 참여도 | 설문조사 |
|
||||
| 학습 | 지식 습득 | 퀴즈, 테스트 |
|
||||
| 행동 | 현업 적용 | 관찰, 피드백 |
|
||||
| 결과 | 성과 개선 | KPI 측정 |
|
||||
|
||||
## Training Templates
|
||||
|
||||
### 2-Hour Workshop
|
||||
|
||||
```
|
||||
00:00-00:10 도입 및 Ice-breaker
|
||||
00:10-00:20 학습 목표 및 아젠다
|
||||
00:20-00:50 핵심 개념 1
|
||||
00:50-01:00 휴식
|
||||
01:00-01:30 핵심 개념 2 + 실습
|
||||
01:30-01:50 그룹 활동/토론
|
||||
01:50-02:00 정리 및 Q&A
|
||||
```
|
||||
|
||||
### Half-Day (4 Hours)
|
||||
|
||||
```
|
||||
09:00-09:20 도입 및 네트워킹
|
||||
09:20-10:20 모듈 1: 기초 개념
|
||||
10:20-10:30 휴식
|
||||
10:30-11:30 모듈 2: 심화 학습
|
||||
11:30-12:00 실습 세션
|
||||
12:00-12:30 사례 연구
|
||||
12:30-13:00 정리, 평가, Q&A
|
||||
```
|
||||
|
||||
### Full-Day (8 Hours)
|
||||
|
||||
```
|
||||
09:00-09:30 도입
|
||||
09:30-10:30 모듈 1
|
||||
10:30-10:45 휴식
|
||||
10:45-12:00 모듈 2 + 실습
|
||||
12:00-13:00 점심
|
||||
13:00-14:00 모듈 3
|
||||
14:00-15:00 그룹 프로젝트
|
||||
15:00-15:15 휴식
|
||||
15:15-16:30 프로젝트 발표
|
||||
16:30-17:00 종합 정리 및 평가
|
||||
```
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital training [topic]" | Design curriculum |
|
||||
| "ourdigital 워크샵 [주제]" | Workshop agenda |
|
||||
| "ourdigital evaluation for [training]" | Assessment design |
|
||||
| "ourdigital 교육자료 [주제]" | Material outline |
|
||||
|
||||
## Facilitation Tips
|
||||
|
||||
### Engagement Techniques
|
||||
|
||||
- **3의 법칙**: 핵심 메시지 3개 이하
|
||||
- **10분 규칙**: 10분마다 활동 전환
|
||||
- **참여 유도**: 질문 → 대기 → 지명
|
||||
- **시각화**: 텍스트보다 다이어그램
|
||||
|
||||
### Korean Training Context
|
||||
|
||||
- 존칭 일관성 유지
|
||||
- 실무 사례 강조
|
||||
- 명함 교환 시간 확보
|
||||
- 그룹 활동 시 리더 지정
|
||||
|
||||
## References
|
||||
|
||||
- `shared/references/training-frameworks.md` - 교수 설계 모델
|
||||
- `shared/references/activity-library.md` - 활동 아이디어
|
||||
- `shared/templates/workshop-template.md` - 워크샵 템플릿
|
||||
- `01-ourdigital-brand-guide` - 발표 스타일
|
||||
231
custom-skills/09-ourdigital-backoffice/SKILL.md
Normal file
231
custom-skills/09-ourdigital-backoffice/SKILL.md
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
name: ourdigital-backoffice
|
||||
description: |
|
||||
Business document creation for OurDigital consulting services.
|
||||
Activated with "ourdigital" keyword for business documents.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital quote", "our quote"
|
||||
- "ourdigital 견적서", "our 견적서"
|
||||
- "ourdigital proposal", "our proposal"
|
||||
- "ourdigital 비용 분석", "our 비용 분석"
|
||||
|
||||
Features:
|
||||
- Quote/estimate generation
|
||||
- Service proposal creation
|
||||
- Contract draft (requires legal review)
|
||||
- Cost-benefit analysis
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Backoffice
|
||||
|
||||
Create business documents for OurDigital consulting services.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital 견적서" / "our 견적서"
|
||||
- "ourdigital proposal" / "our proposal"
|
||||
- "our cost analysis [project]"
|
||||
|
||||
## Important Notice
|
||||
|
||||
⚠️ **Legal Disclaimer**: Contract drafts require professional legal review before use. This skill provides templates and structure only.
|
||||
|
||||
## Document Types
|
||||
|
||||
### 1. Quote/Estimate (견적서)
|
||||
|
||||
**Purpose**: Service pricing and scope summary
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
견적서 번호: OD-YYYY-NNN
|
||||
발행일: YYYY-MM-DD
|
||||
유효기간: 30일
|
||||
|
||||
1. 고객 정보
|
||||
- 회사명, 담당자, 연락처
|
||||
|
||||
2. 서비스 개요
|
||||
- 프로젝트명
|
||||
- 서비스 범위 요약
|
||||
|
||||
3. 세부 항목
|
||||
| 항목 | 상세 | 수량 | 단가 | 금액 |
|
||||
|------|------|------|------|------|
|
||||
|
||||
4. 합계
|
||||
- 소계, VAT, 총액
|
||||
|
||||
5. 결제 조건
|
||||
- 선금/잔금 비율
|
||||
- 결제 방법
|
||||
|
||||
6. 특이사항
|
||||
- 포함/미포함 사항
|
||||
```
|
||||
|
||||
### 2. Service Proposal (서비스 제안서)
|
||||
|
||||
**Purpose**: Detailed service offering and value proposition
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
1. Executive Summary
|
||||
- 핵심 제안 1-2문단
|
||||
|
||||
2. 고객 상황 이해
|
||||
- 현재 과제
|
||||
- 니즈 분석
|
||||
|
||||
3. 제안 서비스
|
||||
- 서비스 범위
|
||||
- 접근 방법
|
||||
- 예상 산출물
|
||||
|
||||
4. 프로젝트 계획
|
||||
- 일정표
|
||||
- 마일스톤
|
||||
- 체크포인트
|
||||
|
||||
5. 투입 리소스
|
||||
- 담당자 프로필
|
||||
- 역할 분담
|
||||
|
||||
6. 비용 및 조건
|
||||
- 비용 구조
|
||||
- 결제 조건
|
||||
|
||||
7. 기대 효과
|
||||
- 예상 성과
|
||||
- ROI 추정
|
||||
|
||||
8. 왜 OurDigital인가
|
||||
- 차별점
|
||||
- 관련 경험
|
||||
```
|
||||
|
||||
### 3. Contract Draft (계약서 초안)
|
||||
|
||||
**Purpose**: Service agreement framework
|
||||
|
||||
⚠️ **반드시 법률 전문가 검토 필요**
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
제1조 (목적)
|
||||
제2조 (용어의 정의)
|
||||
제3조 (계약 기간)
|
||||
제4조 (서비스 범위)
|
||||
제5조 (대금 및 지급 조건)
|
||||
제6조 (권리와 의무)
|
||||
제7조 (비밀유지)
|
||||
제8조 (지적재산권)
|
||||
제9조 (계약의 해지)
|
||||
제10조 (손해배상)
|
||||
제11조 (분쟁 해결)
|
||||
제12조 (일반 조항)
|
||||
```
|
||||
|
||||
### 4. Cost-Benefit Analysis (비용 분석)
|
||||
|
||||
**Purpose**: ROI and investment justification
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
1. 프로젝트 개요
|
||||
- 목적 및 범위
|
||||
|
||||
2. 비용 분석
|
||||
| 항목 | 초기비용 | 연간비용 | 3년 TCO |
|
||||
|
||||
3. 예상 효과
|
||||
| 효과 | 정량적 가치 | 연간 효과 |
|
||||
|
||||
4. ROI 계산
|
||||
- 투자회수기간
|
||||
- NPV, IRR
|
||||
|
||||
5. 리스크 분석
|
||||
- 잠재 리스크
|
||||
- 완화 방안
|
||||
|
||||
6. 권장 사항
|
||||
```
|
||||
|
||||
## Service Catalog
|
||||
|
||||
OurDigital standard service offerings:
|
||||
|
||||
### SEO Services
|
||||
|
||||
| Service | Description | Duration | Price Range |
|
||||
|---------|-------------|----------|-------------|
|
||||
| Technical Audit | 기술 SEO 진단 | 1-2주 | 300-500만원 |
|
||||
| On-Page Optimization | 콘텐츠 최적화 | 월간 | 150-300만원/월 |
|
||||
| Local SEO | 로컬 검색 최적화 | 월간 | 100-200만원/월 |
|
||||
|
||||
### Data & Analytics
|
||||
|
||||
| Service | Description | Duration | Price Range |
|
||||
|---------|-------------|----------|-------------|
|
||||
| GTM Setup | 태그 관리 구축 | 2-4주 | 200-400만원 |
|
||||
| GA4 Implementation | 분석 환경 구축 | 1-3주 | 150-300만원 |
|
||||
| Dashboard Development | 대시보드 개발 | 2-4주 | 300-600만원 |
|
||||
|
||||
### Consulting
|
||||
|
||||
| Service | Description | Duration | Price Range |
|
||||
|---------|-------------|----------|-------------|
|
||||
| Brand Consulting | 브랜드 전략 | 프로젝트 | 500-1000만원 |
|
||||
| Marketing Strategy | 마케팅 전략 | 프로젝트 | 300-700만원 |
|
||||
| Data Strategy | 데이터 전략 | 프로젝트 | 400-800만원 |
|
||||
|
||||
### Training
|
||||
|
||||
| Service | Description | Duration | Price Range |
|
||||
|---------|-------------|----------|-------------|
|
||||
| Workshop | 반일/전일 워크샵 | 4-8시간 | 100-200만원 |
|
||||
| Corporate Training | 기업 교육 | 다회차 | 50-100만원/회 |
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital 견적서 [서비스]" | Generate quote |
|
||||
| "ourdigital proposal [client]" | Create proposal |
|
||||
| "ourdigital 계약서 초안" | Contract template |
|
||||
| "ourdigital 비용 분석 [project]" | Cost-benefit analysis |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Requirement Gathering
|
||||
|
||||
- Client information
|
||||
- Service scope
|
||||
- Timeline requirements
|
||||
- Budget constraints
|
||||
|
||||
### Phase 2: Document Generation
|
||||
|
||||
- Select appropriate template
|
||||
- Fill with gathered information
|
||||
- Apply OurDigital branding
|
||||
|
||||
### Phase 3: Review & Finalize
|
||||
|
||||
- Internal review
|
||||
- Client discussion points highlight
|
||||
- Legal review (for contracts)
|
||||
|
||||
## References
|
||||
|
||||
- `shared/templates/quote-template.md` - 견적서 양식
|
||||
- `shared/templates/proposal-template.md` - 제안서 양식
|
||||
- `shared/templates/contract-template.md` - 계약서 양식
|
||||
- `shared/references/pricing-guide.md` - 가격 가이드
|
||||
- `01-ourdigital-brand-guide` - 문서 스타일
|
||||
167
custom-skills/10-ourdigital-skill-creator/SKILL.md
Normal file
167
custom-skills/10-ourdigital-skill-creator/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: ourdigital-skill-creator
|
||||
description: |
|
||||
Meta skill for creating and managing OurDigital Claude Skills.
|
||||
Activated when user includes "ourdigital" keyword with skill creation requests.
|
||||
|
||||
Triggers (ourdigital or our prefix):
|
||||
- "ourdigital skill create", "our skill create"
|
||||
- "ourdigital 스킬 만들기", "our 스킬 만들기"
|
||||
- "ourdigital skill creator", "our skill creator"
|
||||
|
||||
Features:
|
||||
- Skill suitability evaluation
|
||||
- Interactive Q&A for requirements gathering
|
||||
- Optimized skill generation (Desktop/Code)
|
||||
- Notion history tracking
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop
|
||||
---
|
||||
|
||||
# OurDigital Skill Creator
|
||||
|
||||
Meta skill for creating, validating, and managing OurDigital Claude Skills.
|
||||
|
||||
## Activation
|
||||
|
||||
Activate with "ourdigital" or "our" prefix:
|
||||
- "ourdigital 스킬 만들어줘" / "our 스킬 만들어줘"
|
||||
- "ourdigital skill creator" / "our skill creator"
|
||||
- "our skill create [name]"
|
||||
|
||||
Do NOT activate for generic "make a skill" requests (without our/ourdigital prefix).
|
||||
|
||||
## Interactive Workflow
|
||||
|
||||
### Phase 1: Need Assessment
|
||||
|
||||
When user requests a new skill:
|
||||
|
||||
1. **Acknowledge** the initial request
|
||||
2. **Ask clarifying questions** (max 3 per turn):
|
||||
- What is the core purpose?
|
||||
- What triggers this skill?
|
||||
- What outputs do you expect?
|
||||
|
||||
### Phase 2: Suitability Check
|
||||
|
||||
Evaluate against Claude Skill criteria:
|
||||
|
||||
| Criterion | Question to Ask |
|
||||
|-----------|-----------------|
|
||||
| Clear trigger | When exactly should this skill activate? |
|
||||
| Focused scope | Can you describe 1-3 core functions? |
|
||||
| Reusable resources | What scripts, templates, or references are needed? |
|
||||
| Domain knowledge | What specialized knowledge does Claude lack? |
|
||||
| Clear boundaries | How does this differ from existing skills? |
|
||||
|
||||
**Decision**: If ≥3 criteria pass → proceed. Otherwise, suggest alternatives.
|
||||
|
||||
### Phase 3: Requirements Definition
|
||||
|
||||
Guide user through structured Q&A:
|
||||
|
||||
```
|
||||
Q1. 스킬 목적과 핵심 기능은 무엇인가요?
|
||||
(What is the skill's purpose and core functions?)
|
||||
|
||||
Q2. 어떤 상황에서 이 스킬이 트리거되어야 하나요?
|
||||
(When should this skill be triggered?)
|
||||
|
||||
Q3. 필요한 외부 도구나 API가 있나요?
|
||||
(Any external tools or APIs needed?)
|
||||
|
||||
Q4. 기대하는 출력 형식은 무엇인가요?
|
||||
(What output format do you expect?)
|
||||
|
||||
Q5. Desktop, Code, 또는 Both 환경이 필요한가요?
|
||||
(Which environment: Desktop, Code, or Both?)
|
||||
```
|
||||
|
||||
### Phase 4: Skill Generation
|
||||
|
||||
Generate skill structure following OurDigital standards:
|
||||
|
||||
```
|
||||
XX-ourdigital-{skill-name}/
|
||||
├── desktop/
|
||||
│ └── SKILL.md # Desktop version
|
||||
├── code/
|
||||
│ └── SKILL.md # Code version (CLAUDE.md pattern)
|
||||
├── shared/
|
||||
│ ├── references/ # Common documentation
|
||||
│ ├── templates/ # Shared templates
|
||||
│ └── scripts/ # Utility scripts
|
||||
├── docs/
|
||||
│ ├── CHANGELOG.md # Version history
|
||||
│ └── logs/ # Update logs
|
||||
└── README.md # Overview
|
||||
```
|
||||
|
||||
### Phase 5: Validation
|
||||
|
||||
Before finalizing, verify:
|
||||
|
||||
- [ ] YAML frontmatter includes "ourdigital" trigger keywords
|
||||
- [ ] Description clearly states activation conditions
|
||||
- [ ] Body content is 800-1,200 words
|
||||
- [ ] shared/ resources are properly referenced
|
||||
- [ ] No overlap with existing ourdigital skills
|
||||
|
||||
### Phase 6: Notion Sync
|
||||
|
||||
Record to Working with AI database:
|
||||
|
||||
- **Database**: f8f19ede-32bd-43ac-9f60-0651f6f40afe
|
||||
- **Properties**:
|
||||
- Name: `ourdigital-{skill-name} v{version}`
|
||||
- Status: In progress → Done
|
||||
- AI used: Claude Desktop
|
||||
- AI summary: Brief skill description
|
||||
|
||||
## YAML Frontmatter Template
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: ourdigital-{skill-name}
|
||||
description: |
|
||||
[Purpose summary]
|
||||
Activated when user includes "ourdigital" keyword.
|
||||
|
||||
Triggers:
|
||||
- "ourdigital {keyword1}", "ourdigital {keyword2}"
|
||||
|
||||
Features:
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
version: "1.0"
|
||||
author: OurDigital
|
||||
environment: Desktop | Code | Both
|
||||
---
|
||||
```
|
||||
|
||||
## Skill Numbering
|
||||
|
||||
| Range | Category |
|
||||
|-------|----------|
|
||||
| 01-09 | OurDigital Core (brand, blog, journal, research, etc.) |
|
||||
| 10 | Meta (skill-creator) |
|
||||
| 11-19 | SEO Tools |
|
||||
| 20-29 | GTM/Analytics Tools |
|
||||
| 31-39 | Notion Tools |
|
||||
| 40-49 | Jamie Clinic Tools |
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `shared/references/suitability-criteria.md` - Skill evaluation criteria
|
||||
- `shared/references/skill-patterns.md` - Common patterns
|
||||
- `shared/templates/skill-template/` - Blank skill template
|
||||
|
||||
## Quick Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| "ourdigital 스킬 적합성" | Run suitability check only |
|
||||
| "ourdigital 스킬 생성" | Full creation workflow |
|
||||
| "ourdigital 스킬 검증" | Validate existing skill |
|
||||
109
custom-skills/11-seo-comprehensive-audit/SKILL.md
Normal file
109
custom-skills/11-seo-comprehensive-audit/SKILL.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: seo-comprehensive-audit
|
||||
description: |
|
||||
Comprehensive SEO audit orchestrator. Runs 6-stage audit pipeline (Technical, On-Page, CWV, Schema, Local, GSC) and produces a unified report with weighted health score.
|
||||
Triggers: comprehensive SEO, full SEO audit, 종합 SEO 감사, site audit, SEO health check.
|
||||
---
|
||||
|
||||
# Comprehensive SEO Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Orchestrate a full-spectrum SEO audit by running 6 specialized analyses and synthesizing results into a unified health score and actionable report.
|
||||
|
||||
## Pipeline Stages
|
||||
|
||||
| # | Stage | Source Skill | Default |
|
||||
|---|-------|-------------|---------|
|
||||
| 1 | Technical SEO | 12-seo-technical-audit | Always |
|
||||
| 2 | On-Page SEO | 13-seo-on-page-audit | Always |
|
||||
| 3 | Core Web Vitals | 14-seo-core-web-vitals | Always |
|
||||
| 4 | Schema Validation | 16-seo-schema-validator | Always |
|
||||
| 5 | Local SEO | 18-seo-local-audit | Skippable |
|
||||
| 6 | Search Console | 15-seo-search-console | Skippable |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Initialization
|
||||
1. Receive target URL from user
|
||||
2. Confirm which stages to run (all 6 by default)
|
||||
3. Set up audit tracking ID: `COMP-YYYYMMDD-NNN`
|
||||
|
||||
### 2. Execute Stages (Sequential)
|
||||
For each active stage:
|
||||
1. Run the sub-skill analysis
|
||||
2. Collect JSON results
|
||||
3. Extract score and issues
|
||||
|
||||
### 3. Synthesis
|
||||
1. Compute weighted health score (0-100)
|
||||
2. Assign grade (A/B+/B/C/D/F)
|
||||
3. Prioritize critical and high-severity findings
|
||||
4. Generate recommendations
|
||||
|
||||
### 4. Notion Report
|
||||
1. Create summary page: `종합 SEO 감사 보고서 - [domain] - YYYY-MM-DD`
|
||||
2. Create individual pages for Critical/High findings
|
||||
3. Database: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
|
||||
## Health Score Weights
|
||||
|
||||
| Category | Weight |
|
||||
|----------|--------|
|
||||
| Technical SEO | 20% |
|
||||
| On-Page SEO | 20% |
|
||||
| Core Web Vitals | 25% |
|
||||
| Schema | 15% |
|
||||
| Local SEO | 10% |
|
||||
| Search Console | 10% |
|
||||
|
||||
Skipped stages redistribute weight proportionally.
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## 종합 SEO 감사 보고서: [domain]
|
||||
|
||||
**Health Score**: [score]/100 ([grade])
|
||||
**Date**: YYYY-MM-DD
|
||||
**Audit ID**: COMP-YYYYMMDD-NNN
|
||||
|
||||
### Stage Results
|
||||
| Stage | Score | Issues |
|
||||
|-------|-------|--------|
|
||||
| Technical SEO | XX/100 | N issues |
|
||||
| On-Page SEO | XX/100 | N issues |
|
||||
| Core Web Vitals | XX/100 | N issues |
|
||||
| Schema | XX/100 | N issues |
|
||||
| Local SEO | XX/100 | N issues |
|
||||
| Search Console | XX/100 | N issues |
|
||||
|
||||
### Critical Findings
|
||||
1. [Finding with recommendation]
|
||||
|
||||
### Recommendations (Priority Order)
|
||||
1. [Action item]
|
||||
```
|
||||
|
||||
## MCP Tool Usage
|
||||
|
||||
### Firecrawl
|
||||
```
|
||||
mcp__firecrawl__scrape: Fetch page content for on-page and schema analysis
|
||||
```
|
||||
|
||||
### Notion
|
||||
```
|
||||
mcp__notion__*: Create audit report pages in SEO database
|
||||
```
|
||||
|
||||
### Perplexity
|
||||
```
|
||||
mcp__perplexity__search: Research best practices for recommendations
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Local SEO stage requires manual input for NAP/GBP data
|
||||
- Search Console stage requires GSC API credentials
|
||||
- Health score accuracy improves when all 6 stages are active
|
||||
103
custom-skills/12-seo-technical-audit/SKILL.md
Normal file
103
custom-skills/12-seo-technical-audit/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: seo-technical-audit
|
||||
description: |
|
||||
Technical SEO analyzer for robots.txt, sitemap, and crawlability fundamentals.
|
||||
Triggers: technical SEO, robots.txt, sitemap validation, crawlability, URL accessibility.
|
||||
---
|
||||
|
||||
# SEO Technical Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze crawlability fundamentals: robots.txt rules, XML sitemap structure, and URL accessibility. Identify issues blocking search engine crawlers.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Robots.txt Analysis** - Parse rules, check blocked resources
|
||||
2. **Sitemap Validation** - Verify XML structure, URL limits, dates
|
||||
3. **URL Accessibility** - Check HTTP status, redirects, broken links
|
||||
|
||||
## MCP Tool Usage
|
||||
|
||||
### Firecrawl for Page Data
|
||||
```
|
||||
mcp__firecrawl__scrape: Fetch robots.txt and sitemap content
|
||||
mcp__firecrawl__crawl: Check multiple URLs accessibility
|
||||
```
|
||||
|
||||
### Perplexity for Best Practices
|
||||
```
|
||||
mcp__perplexity__search: Research current SEO recommendations
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Robots.txt Check
|
||||
1. Fetch `[domain]/robots.txt` using Firecrawl
|
||||
2. Parse User-agent rules and Disallow patterns
|
||||
3. Identify blocked resources (CSS, JS, images)
|
||||
4. Check for Sitemap declarations
|
||||
5. Report critical issues
|
||||
|
||||
### 2. Sitemap Validation
|
||||
1. Locate sitemap (from robots.txt or `/sitemap.xml`)
|
||||
2. Validate XML syntax
|
||||
3. Check URL count (max 50,000)
|
||||
4. Verify lastmod date formats
|
||||
5. For sitemap index: parse child sitemaps
|
||||
|
||||
### 3. URL Accessibility Sampling
|
||||
1. Extract URLs from sitemap
|
||||
2. Sample 50-100 URLs for large sites
|
||||
3. Check HTTP status codes
|
||||
4. Identify redirects and broken links
|
||||
5. Report 4xx/5xx errors
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Technical SEO Audit: [domain]
|
||||
|
||||
### Robots.txt Analysis
|
||||
- Status: [Valid/Invalid/Missing]
|
||||
- Sitemap declared: [Yes/No]
|
||||
- Critical blocks: [List]
|
||||
|
||||
### Sitemap Validation
|
||||
- URLs found: [count]
|
||||
- Syntax: [Valid/Errors]
|
||||
- Issues: [List]
|
||||
|
||||
### URL Accessibility (sampled)
|
||||
- Checked: [count] URLs
|
||||
- Success (2xx): [count]
|
||||
- Redirects (3xx): [count]
|
||||
- Errors (4xx/5xx): [count]
|
||||
|
||||
### Recommendations
|
||||
1. [Priority fixes]
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
| Issue | Impact | Fix |
|
||||
|-------|--------|-----|
|
||||
| No sitemap in robots.txt | Medium | Add `Sitemap:` directive |
|
||||
| Blocking CSS/JS | High | Allow Googlebot access |
|
||||
| 404s in sitemap | High | Remove or fix URLs |
|
||||
| Missing lastmod | Low | Add dates for freshness signals |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Cannot access password-protected sitemaps
|
||||
- Large sitemaps (10,000+ URLs) require sampling
|
||||
- Does not check render-blocking issues (use Core Web Vitals skill)
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: [TYPE]-YYYYMMDD-NNN
|
||||
|
||||
103
custom-skills/13-seo-on-page-audit/SKILL.md
Normal file
103
custom-skills/13-seo-on-page-audit/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: seo-on-page-audit
|
||||
description: |
|
||||
On-page SEO analyzer for meta tags, headings, links, images, and Open Graph.
|
||||
Triggers: on-page SEO, meta tags, title tag, heading structure, alt text.
|
||||
---
|
||||
|
||||
# SEO On-Page Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze single-page SEO elements: meta tags, heading hierarchy, internal/external links, images, and social sharing tags.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Meta Tags** - Title, description, canonical, robots
|
||||
2. **Headings** - H1-H6 structure and hierarchy
|
||||
3. **Links** - Internal, external, broken detection
|
||||
4. **Images** - Alt text, sizing, lazy loading
|
||||
5. **Social** - Open Graph, Twitter Cards
|
||||
|
||||
## MCP Tool Usage
|
||||
|
||||
```
|
||||
mcp__firecrawl__scrape: Extract page HTML and metadata
|
||||
mcp__perplexity__search: Research SEO best practices
|
||||
mcp__notion__create-page: Save audit findings
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Scrape target URL with Firecrawl
|
||||
2. Extract and analyze meta tags
|
||||
3. Map heading hierarchy
|
||||
4. Count and categorize links
|
||||
5. Check image optimization
|
||||
6. Validate Open Graph tags
|
||||
7. Generate recommendations
|
||||
|
||||
## Checklist
|
||||
|
||||
### Meta Tags
|
||||
- [ ] Title present (50-60 characters)
|
||||
- [ ] Meta description present (150-160 characters)
|
||||
- [ ] Canonical URL set
|
||||
- [ ] Robots meta allows indexing
|
||||
|
||||
### Headings
|
||||
- [ ] Single H1 tag
|
||||
- [ ] Logical hierarchy (no skips)
|
||||
- [ ] Keywords in H1
|
||||
|
||||
### Links
|
||||
- [ ] No broken internal links
|
||||
- [ ] External links use rel attributes
|
||||
- [ ] Reasonable internal link count
|
||||
|
||||
### Images
|
||||
- [ ] All images have alt text
|
||||
- [ ] Images are appropriately sized
|
||||
- [ ] Lazy loading implemented
|
||||
|
||||
### Open Graph
|
||||
- [ ] og:title present
|
||||
- [ ] og:description present
|
||||
- [ ] og:image present (1200x630)
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## On-Page Audit: [URL]
|
||||
|
||||
### Meta Tags: X/5
|
||||
| Element | Status | Value |
|
||||
|---------|--------|-------|
|
||||
|
||||
### Headings: X/5
|
||||
- H1: [text]
|
||||
- Hierarchy: Valid/Invalid
|
||||
|
||||
### Links
|
||||
- Internal: X
|
||||
- External: X
|
||||
- Broken: X
|
||||
|
||||
### Recommendations
|
||||
1. [Priority fixes]
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Single page analysis only
|
||||
- Cannot detect JavaScript-rendered content issues
|
||||
- External link status requires additional crawl
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: [TYPE]-YYYYMMDD-NNN
|
||||
|
||||
117
custom-skills/14-seo-core-web-vitals/SKILL.md
Normal file
117
custom-skills/14-seo-core-web-vitals/SKILL.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: seo-core-web-vitals
|
||||
description: |
|
||||
Core Web Vitals analyzer for LCP, FID, CLS, and INP optimization recommendations.
|
||||
Triggers: Core Web Vitals, page speed, LCP optimization, CLS fix, INP analysis.
|
||||
---
|
||||
|
||||
# SEO Core Web Vitals
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze Core Web Vitals performance metrics and provide optimization recommendations.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **LCP** - Largest Contentful Paint measurement
|
||||
2. **FID/INP** - Interactivity metrics
|
||||
3. **CLS** - Cumulative Layout Shift
|
||||
4. **Recommendations** - Optimization guidance
|
||||
|
||||
## Metrics Thresholds
|
||||
|
||||
| Metric | Good | Needs Work | Poor |
|
||||
|--------|------|------------|------|
|
||||
| LCP | ≤2.5s | 2.5-4s | >4s |
|
||||
| FID | ≤100ms | 100-300ms | >300ms |
|
||||
| CLS | ≤0.1 | 0.1-0.25 | >0.25 |
|
||||
| INP | ≤200ms | 200-500ms | >500ms |
|
||||
|
||||
## Data Sources
|
||||
|
||||
### Option 1: PageSpeed Insights (Recommended)
|
||||
Use external tool and input results:
|
||||
- Visit: https://pagespeed.web.dev/
|
||||
- Enter URL, run test
|
||||
- Provide scores to skill
|
||||
|
||||
### Option 2: Research Best Practices
|
||||
```
|
||||
mcp__perplexity__search: "Core Web Vitals optimization [specific issue]"
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Request PageSpeed Insights data from user
|
||||
2. Analyze provided metrics
|
||||
3. Identify failing metrics
|
||||
4. Research optimization strategies
|
||||
5. Provide prioritized recommendations
|
||||
|
||||
## Common LCP Issues
|
||||
|
||||
| Cause | Fix |
|
||||
|-------|-----|
|
||||
| Slow server response | Improve TTFB, use CDN |
|
||||
| Render-blocking resources | Defer non-critical CSS/JS |
|
||||
| Slow resource load | Preload LCP image |
|
||||
| Client-side rendering | Use SSR/SSG |
|
||||
|
||||
## Common CLS Issues
|
||||
|
||||
| Cause | Fix |
|
||||
|-------|-----|
|
||||
| Images without dimensions | Add width/height attributes |
|
||||
| Ads/embeds without space | Reserve space with CSS |
|
||||
| Web fonts causing FOIT/FOUT | Use font-display: swap |
|
||||
| Dynamic content injection | Reserve space, use transforms |
|
||||
|
||||
## Common INP Issues
|
||||
|
||||
| Cause | Fix |
|
||||
|-------|-----|
|
||||
| Long JavaScript tasks | Break up tasks, use web workers |
|
||||
| Large DOM size | Reduce DOM nodes |
|
||||
| Heavy event handlers | Debounce, optimize listeners |
|
||||
| Third-party scripts | Defer, lazy load |
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Core Web Vitals: [URL]
|
||||
|
||||
### Scores
|
||||
| Metric | Mobile | Desktop | Status |
|
||||
|--------|--------|---------|--------|
|
||||
| LCP | Xs | Xs | Good/Poor |
|
||||
| FID | Xms | Xms | Good/Poor |
|
||||
| CLS | X.XX | X.XX | Good/Poor |
|
||||
| INP | Xms | Xms | Good/Poor |
|
||||
|
||||
### Overall Score
|
||||
- Mobile: X/100
|
||||
- Desktop: X/100
|
||||
|
||||
### Priority Fixes
|
||||
1. [Highest impact recommendation]
|
||||
2. [Second priority]
|
||||
|
||||
### Detailed Recommendations
|
||||
[Per-metric optimization steps]
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires external PageSpeed Insights data
|
||||
- Lab data may differ from field data
|
||||
- Some fixes require developer implementation
|
||||
- Third-party scripts may be difficult to optimize
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: [TYPE]-YYYYMMDD-NNN
|
||||
|
||||
126
custom-skills/15-seo-search-console/SKILL.md
Normal file
126
custom-skills/15-seo-search-console/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: seo-search-console
|
||||
description: |
|
||||
Google Search Console data analyzer for performance, queries, and index coverage.
|
||||
Triggers: Search Console, GSC analysis, search performance, rankings, CTR optimization.
|
||||
---
|
||||
|
||||
# SEO Search Console
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze Google Search Console data: search performance (queries, pages, CTR, position), sitemap status, and index coverage.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Performance Analysis** - Clicks, impressions, CTR, position
|
||||
2. **Query Analysis** - Top search queries
|
||||
3. **Page Performance** - Best/worst performing pages
|
||||
4. **Index Coverage** - Crawl and index issues
|
||||
5. **Sitemap Status** - Submission and processing
|
||||
|
||||
## Data Collection
|
||||
|
||||
### Option 1: User Provides Data
|
||||
Request GSC export from user:
|
||||
1. Go to Search Console > Performance
|
||||
2. Export data (CSV or Google Sheets)
|
||||
3. Share with assistant
|
||||
|
||||
### Option 2: User Describes Data
|
||||
User verbally provides:
|
||||
- Top queries and positions
|
||||
- CTR trends
|
||||
- Coverage issues
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
| Metric | What It Measures | Good Benchmark |
|
||||
|--------|------------------|----------------|
|
||||
| Clicks | User visits from search | Trending up |
|
||||
| Impressions | Search appearances | High for target keywords |
|
||||
| CTR | Click-through rate | 2-5% average |
|
||||
| Position | Average ranking | <10 for key terms |
|
||||
|
||||
### Query Analysis
|
||||
|
||||
Identify:
|
||||
- **Winners** - High position, high CTR
|
||||
- **Opportunities** - High impressions, low CTR
|
||||
- **Quick wins** - Position 8-20, low effort to improve
|
||||
|
||||
### Page Analysis
|
||||
|
||||
Categorize:
|
||||
- **Top performers** - High clicks, good CTR
|
||||
- **Underperformers** - High impressions, low CTR
|
||||
- **Declining** - Down vs previous period
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Collect GSC data from user
|
||||
2. Analyze performance trends
|
||||
3. Identify top queries and pages
|
||||
4. Find optimization opportunities
|
||||
5. Check for coverage issues
|
||||
6. Provide actionable recommendations
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Search Console Analysis: [Site]
|
||||
|
||||
### Overview (Last 28 Days)
|
||||
| Metric | Value | vs Previous |
|
||||
|--------|-------|-------------|
|
||||
| Clicks | X | +X% |
|
||||
| Impressions | X | +X% |
|
||||
| CTR | X% | +X% |
|
||||
| Position | X | +X |
|
||||
|
||||
### Top Queries
|
||||
| Query | Clicks | Position | Opportunity |
|
||||
|-------|--------|----------|-------------|
|
||||
|
||||
### Top Pages
|
||||
| Page | Clicks | CTR | Status |
|
||||
|------|--------|-----|--------|
|
||||
|
||||
### Opportunities
|
||||
1. [Query with high impressions, low CTR]
|
||||
2. [Page ranking 8-20 that can improve]
|
||||
|
||||
### Issues
|
||||
- [Coverage problems]
|
||||
- [Sitemap issues]
|
||||
|
||||
### Recommendations
|
||||
1. [Priority action]
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
| Issue | Impact | Fix |
|
||||
|-------|--------|-----|
|
||||
| Low CTR on high-impression query | Lost traffic | Improve title/description |
|
||||
| Declining positions | Traffic loss | Update content, build links |
|
||||
| Not indexed pages | No visibility | Fix crawl issues |
|
||||
| Sitemap errors | Discovery problems | Fix sitemap XML |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires user to provide GSC data
|
||||
- API access needs service account setup
|
||||
- Data has 2-3 day delay
|
||||
- Limited to verified properties
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: [TYPE]-YYYYMMDD-NNN
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: 16-seo-schema-validator
|
||||
name: seo-schema-validator
|
||||
description: |
|
||||
Validates an AUTHORED JSON-LD schema dataset (pre-deployment QA) and audits
|
||||
live structured data (post-deployment). Runs a 5-layer offline validation
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": "1.0",
|
||||
"scope": "Curated, hotel-focused subset of schema.org + Google rich-result requirements.",
|
||||
"intent": "Self-contained offline rules (the runtime cannot reach schema.org or Google). Unknown types/properties degrade to warnings, never hard errors, to avoid false positives. To support a new type or tighten a rule, edit THIS file only.",
|
||||
"sources": "schema.org/Hotel, schema.org/LocalBusiness, Google Search Central 'Structured data' rich-result docs (as of 2025)."
|
||||
},
|
||||
|
||||
"valid_contexts": [
|
||||
"https://schema.org",
|
||||
"http://schema.org",
|
||||
"https://schema.org/",
|
||||
"http://schema.org/",
|
||||
"https://www.schema.org",
|
||||
"http://www.schema.org"
|
||||
],
|
||||
|
||||
"global_properties": [
|
||||
"@context", "@type", "@id", "@graph", "@reverse",
|
||||
"name", "alternateName", "legalName", "description", "disambiguatingDescription",
|
||||
"url", "image", "logo", "sameAs", "identifier", "mainEntityOfPage",
|
||||
"additionalType", "subjectOf", "potentialAction", "inLanguage"
|
||||
],
|
||||
|
||||
"known_types": {
|
||||
"Organization": {
|
||||
"required": ["name", "url"],
|
||||
"recommended": ["logo", "sameAs", "contactPoint", "address"],
|
||||
"allowed": ["legalName", "foundingDate", "parentOrganization", "subOrganization", "brand", "telephone", "email", "founder", "numberOfEmployees", "memberOf", "hasMerchantReturnPolicy", "member"]
|
||||
},
|
||||
"Corporation": {
|
||||
"required": ["name", "url"],
|
||||
"recommended": ["logo", "sameAs", "address"],
|
||||
"allowed": ["legalName", "foundingDate", "parentOrganization", "tickerSymbol", "telephone", "email", "brand"]
|
||||
},
|
||||
"WebSite": {
|
||||
"required": ["name", "url"],
|
||||
"recommended": ["publisher", "potentialAction", "inLanguage"],
|
||||
"allowed": ["alternateName", "about", "copyrightHolder", "copyrightYear"]
|
||||
},
|
||||
"WebPage": {
|
||||
"required": ["name"],
|
||||
"recommended": ["url", "isPartOf", "primaryImageOfPage", "breadcrumb", "datePublished", "dateModified"],
|
||||
"allowed": ["about", "mentions", "speakable", "lastReviewed", "reviewedBy", "significantLink"]
|
||||
},
|
||||
"LocalBusiness": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["telephone", "openingHoursSpecification", "geo", "image", "url", "priceRange", "aggregateRating"],
|
||||
"allowed": ["email", "openingHours", "paymentAccepted", "currenciesAccepted", "areaServed", "hasMap", "department", "menu", "review", "containedInPlace", "containsPlace", "amenityFeature"]
|
||||
},
|
||||
"Hotel": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["telephone", "image", "priceRange", "geo", "url", "starRating", "aggregateRating", "checkinTime", "checkoutTime"],
|
||||
"allowed": ["email", "amenityFeature", "petsAllowed", "numberOfRooms", "availableLanguage", "containedInPlace", "containsPlace", "makesOffer", "brand", "currenciesAccepted", "smokingAllowed", "openingHoursSpecification", "audience", "review"]
|
||||
},
|
||||
"LodgingBusiness": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["telephone", "image", "priceRange", "geo", "url", "starRating", "aggregateRating", "checkinTime", "checkoutTime"],
|
||||
"allowed": ["email", "amenityFeature", "petsAllowed", "numberOfRooms", "availableLanguage", "containedInPlace", "containsPlace", "makesOffer", "currenciesAccepted", "smokingAllowed"]
|
||||
},
|
||||
"Resort": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["telephone", "image", "priceRange", "geo", "url", "starRating", "aggregateRating"],
|
||||
"allowed": ["email", "amenityFeature", "numberOfRooms", "containedInPlace", "containsPlace", "checkinTime", "checkoutTime"]
|
||||
},
|
||||
"Restaurant": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["servesCuisine", "priceRange", "telephone", "menu", "openingHoursSpecification", "image", "url", "geo", "acceptsReservations"],
|
||||
"allowed": ["email", "hasMenu", "starRating", "aggregateRating", "review", "containedInPlace", "smokingAllowed"]
|
||||
},
|
||||
"FoodEstablishment": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["servesCuisine", "priceRange", "telephone", "menu", "openingHoursSpecification"],
|
||||
"allowed": ["email", "hasMenu", "acceptsReservations", "containedInPlace"]
|
||||
},
|
||||
"BarOrPub": {
|
||||
"required": ["name", "address"],
|
||||
"recommended": ["telephone", "openingHoursSpecification", "priceRange", "servesCuisine"],
|
||||
"allowed": ["menu", "hasMenu", "image", "url"]
|
||||
},
|
||||
"FAQPage": {
|
||||
"required": ["mainEntity"],
|
||||
"recommended": [],
|
||||
"allowed": ["about", "headline", "datePublished", "dateModified"]
|
||||
},
|
||||
"Question": {
|
||||
"required": ["name", "acceptedAnswer"],
|
||||
"recommended": [],
|
||||
"allowed": ["text", "answerCount", "suggestedAnswer", "upvoteCount", "author"]
|
||||
},
|
||||
"Answer": {
|
||||
"required": ["text"],
|
||||
"recommended": [],
|
||||
"allowed": ["url", "upvoteCount", "author", "dateCreated"]
|
||||
},
|
||||
"BreadcrumbList": {
|
||||
"required": ["itemListElement"],
|
||||
"recommended": [],
|
||||
"allowed": ["numberOfItems", "itemListOrder"]
|
||||
},
|
||||
"ItemList": {
|
||||
"required": ["itemListElement"],
|
||||
"recommended": [],
|
||||
"allowed": ["numberOfItems", "itemListOrder"]
|
||||
},
|
||||
"ListItem": {
|
||||
"required": ["position"],
|
||||
"recommended": ["item", "name"],
|
||||
"allowed": ["url", "image", "nextItem", "previousItem"]
|
||||
},
|
||||
"Product": {
|
||||
"required": ["name"],
|
||||
"recommended": ["image", "offers", "brand", "aggregateRating", "review", "description", "sku"],
|
||||
"allowed": ["gtin", "gtin13", "gtin8", "gtin12", "mpn", "color", "material", "category", "audience", "isVariantOf", "additionalProperty", "hasMerchantReturnPolicy"]
|
||||
},
|
||||
"Offer": {
|
||||
"required": ["price", "priceCurrency"],
|
||||
"recommended": ["availability", "url", "validFrom", "priceValidUntil"],
|
||||
"allowed": ["itemCondition", "seller", "eligibleRegion", "priceSpecification", "shippingDetails", "availabilityStarts"]
|
||||
},
|
||||
"AggregateOffer": {
|
||||
"required": ["lowPrice", "priceCurrency"],
|
||||
"recommended": ["highPrice", "offerCount"],
|
||||
"allowed": ["offers", "availability"]
|
||||
},
|
||||
"Article": {
|
||||
"required": ["headline"],
|
||||
"recommended": ["author", "datePublished", "image", "dateModified", "publisher"],
|
||||
"allowed": ["articleBody", "articleSection", "wordCount", "keywords", "speakable"]
|
||||
},
|
||||
"NewsArticle": {
|
||||
"required": ["headline"],
|
||||
"recommended": ["author", "datePublished", "image", "dateModified", "publisher"],
|
||||
"allowed": ["articleBody", "dateline", "printSection"]
|
||||
},
|
||||
"BlogPosting": {
|
||||
"required": ["headline"],
|
||||
"recommended": ["author", "datePublished", "image", "dateModified", "publisher"],
|
||||
"allowed": ["articleBody", "keywords", "wordCount"]
|
||||
},
|
||||
"Event": {
|
||||
"required": ["name", "startDate", "location"],
|
||||
"recommended": ["endDate", "offers", "performer", "image", "eventStatus", "eventAttendanceMode", "organizer"],
|
||||
"allowed": ["doorTime", "previousStartDate", "typicalAgeRange", "maximumAttendeeCapacity"]
|
||||
},
|
||||
"Review": {
|
||||
"required": ["reviewRating", "author"],
|
||||
"recommended": ["datePublished", "reviewBody", "itemReviewed"],
|
||||
"allowed": ["publisher", "name"]
|
||||
},
|
||||
"AggregateRating": {
|
||||
"required": ["ratingValue"],
|
||||
"recommended": ["reviewCount", "ratingCount", "bestRating"],
|
||||
"allowed": ["worstRating", "itemReviewed"]
|
||||
},
|
||||
"MemberProgram": {
|
||||
"required": ["name"],
|
||||
"recommended": ["hasTiers", "hostingOrganization", "url"],
|
||||
"allowed": ["description", "membershipPointsEarned"]
|
||||
}
|
||||
},
|
||||
|
||||
"container_types": [
|
||||
"PostalAddress", "GeoCoordinates", "GeoShape", "ImageObject", "VideoObject",
|
||||
"ContactPoint", "OpeningHoursSpecification", "Rating", "QuantitativeValue",
|
||||
"MonetaryAmount", "PriceSpecification", "Brand", "EntryPoint", "Place",
|
||||
"OfferCatalog", "ReserveAction", "OrderAction", "SearchAction", "ViewAction",
|
||||
"MeetingRoom", "Room", "HotelRoom", "Suite", "LocationFeatureSpecification",
|
||||
"MemberProgramTier", "MobileApplication", "WebApplication", "SoftwareApplication",
|
||||
"Menu", "MenuItem", "MenuSection", "Country", "AdministrativeArea", "Duration",
|
||||
"PropertyValue", "Person", "Audience", "Language"
|
||||
],
|
||||
|
||||
"value_formats": {
|
||||
"url_props": ["url", "logo", "sameAs", "image", "contentUrl", "thumbnailUrl", "target", "urlTemplate", "installUrl", "menu", "hasMap", "downloadUrl", "embedUrl"],
|
||||
"date_props": ["datePublished", "dateModified", "dateCreated", "startDate", "endDate", "validFrom", "validThrough", "priceValidUntil", "foundingDate", "uploadDate", "availabilityStarts", "availabilityEnds", "lastReviewed", "previousStartDate"],
|
||||
"lang_props": ["inLanguage", "availableLanguage"],
|
||||
"currency_props": ["priceCurrency", "currenciesAccepted"],
|
||||
"number_props": ["price", "lowPrice", "highPrice", "ratingValue", "reviewCount", "ratingCount", "bestRating", "worstRating", "position", "numberOfRooms", "maxValue", "minValue", "offerCount"]
|
||||
},
|
||||
|
||||
"valid_currencies": ["KRW", "USD", "EUR", "JPY", "CNY", "GBP", "HKD", "SGD", "THB", "AUD", "CAD", "CHF", "TWD", "MYR", "PHP", "VND", "IDR", "INR"],
|
||||
|
||||
"valid_language_codes": ["ko", "en", "ja", "zh", "zh-CN", "zh-TW", "zh-Hans", "zh-Hant", "ko-KR", "en-US", "en-GB", "ja-JP", "fr", "de", "es", "ru", "th", "vi", "id", "ms"],
|
||||
|
||||
"placeholder_tokens": [
|
||||
"lorem ipsum", "lorem", "ipsum", "dolor sit", "todo", "tbd", "fixme",
|
||||
"xxx", "yyy", "zzz", "placeholder", "insert here", "insert text",
|
||||
"example.com", "your-domain", "yourdomain", "changeme", "sample text",
|
||||
"{{", "}}", "<insert", "[insert", "n/a", "샘플", "예시", "여기에",
|
||||
"변경필요", "수정필요", "입력필요", "내용입력", "테스트", "임시"
|
||||
],
|
||||
|
||||
"geo": {
|
||||
"lat_min": -90.0, "lat_max": 90.0,
|
||||
"lon_min": -180.0, "lon_max": 180.0,
|
||||
"kr_lat_range": [33.0, 39.0],
|
||||
"kr_lon_range": [124.0, 132.0]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": "1.1",
|
||||
"scope": "Curated, hotel-focused subset of schema.org + Google rich-result requirements.",
|
||||
"intent": "Self-contained offline rules (the runtime cannot reach schema.org or Google). Unknown types/properties degrade to warnings, never hard errors, to avoid false positives. To support a new type or tighten a rule, edit THIS file only. [v1.1 2026-05-29: +EventVenue/ExerciseGym/SportsActivityLocation/HealthAndBeautyBusiness/DaySpa/CafeOrCoffeeShop, +LodgingReservation container, Organization/Corporation url->recommended per Google, +slogan/founder/ceo/parentOrganization/award/hasOfferCatalog/openingDate/isPartOf allowances.]",
|
||||
"sources": "schema.org/Hotel, schema.org/LocalBusiness, Google Search Central 'Structured data' rich-result docs (as of 2025)."
|
||||
},
|
||||
"valid_contexts": [
|
||||
"https://schema.org",
|
||||
"http://schema.org",
|
||||
"https://schema.org/",
|
||||
"http://schema.org/",
|
||||
"https://www.schema.org",
|
||||
"http://www.schema.org"
|
||||
],
|
||||
"global_properties": [
|
||||
"@context",
|
||||
"@type",
|
||||
"@id",
|
||||
"@graph",
|
||||
"@reverse",
|
||||
"name",
|
||||
"alternateName",
|
||||
"legalName",
|
||||
"description",
|
||||
"disambiguatingDescription",
|
||||
"url",
|
||||
"image",
|
||||
"logo",
|
||||
"sameAs",
|
||||
"identifier",
|
||||
"mainEntityOfPage",
|
||||
"additionalType",
|
||||
"subjectOf",
|
||||
"potentialAction",
|
||||
"inLanguage",
|
||||
"isPartOf"
|
||||
],
|
||||
"known_types": {
|
||||
"Organization": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"logo",
|
||||
"sameAs",
|
||||
"contactPoint",
|
||||
"address",
|
||||
"url"
|
||||
],
|
||||
"allowed": [
|
||||
"legalName",
|
||||
"foundingDate",
|
||||
"parentOrganization",
|
||||
"subOrganization",
|
||||
"brand",
|
||||
"telephone",
|
||||
"email",
|
||||
"founder",
|
||||
"numberOfEmployees",
|
||||
"memberOf",
|
||||
"hasMerchantReturnPolicy",
|
||||
"member",
|
||||
"slogan",
|
||||
"hasOfferCatalog"
|
||||
]
|
||||
},
|
||||
"Corporation": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"logo",
|
||||
"sameAs",
|
||||
"address",
|
||||
"url"
|
||||
],
|
||||
"allowed": [
|
||||
"legalName",
|
||||
"foundingDate",
|
||||
"parentOrganization",
|
||||
"tickerSymbol",
|
||||
"telephone",
|
||||
"email",
|
||||
"brand",
|
||||
"founder",
|
||||
"ceo",
|
||||
"subOrganization",
|
||||
"memberOf",
|
||||
"slogan",
|
||||
"hasOfferCatalog"
|
||||
]
|
||||
},
|
||||
"WebSite": {
|
||||
"required": [
|
||||
"name",
|
||||
"url"
|
||||
],
|
||||
"recommended": [
|
||||
"publisher",
|
||||
"potentialAction",
|
||||
"inLanguage"
|
||||
],
|
||||
"allowed": [
|
||||
"alternateName",
|
||||
"about",
|
||||
"copyrightHolder",
|
||||
"copyrightYear"
|
||||
]
|
||||
},
|
||||
"WebPage": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"isPartOf",
|
||||
"primaryImageOfPage",
|
||||
"breadcrumb",
|
||||
"datePublished",
|
||||
"dateModified"
|
||||
],
|
||||
"allowed": [
|
||||
"about",
|
||||
"mentions",
|
||||
"speakable",
|
||||
"lastReviewed",
|
||||
"reviewedBy",
|
||||
"significantLink"
|
||||
]
|
||||
},
|
||||
"LocalBusiness": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"telephone",
|
||||
"openingHoursSpecification",
|
||||
"geo",
|
||||
"image",
|
||||
"url",
|
||||
"priceRange",
|
||||
"aggregateRating"
|
||||
],
|
||||
"allowed": [
|
||||
"email",
|
||||
"openingHours",
|
||||
"paymentAccepted",
|
||||
"currenciesAccepted",
|
||||
"areaServed",
|
||||
"hasMap",
|
||||
"department",
|
||||
"menu",
|
||||
"review",
|
||||
"containedInPlace",
|
||||
"containsPlace",
|
||||
"amenityFeature"
|
||||
]
|
||||
},
|
||||
"Hotel": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"telephone",
|
||||
"image",
|
||||
"priceRange",
|
||||
"geo",
|
||||
"url",
|
||||
"starRating",
|
||||
"aggregateRating",
|
||||
"checkinTime",
|
||||
"checkoutTime"
|
||||
],
|
||||
"allowed": [
|
||||
"email",
|
||||
"amenityFeature",
|
||||
"petsAllowed",
|
||||
"numberOfRooms",
|
||||
"availableLanguage",
|
||||
"containedInPlace",
|
||||
"containsPlace",
|
||||
"makesOffer",
|
||||
"brand",
|
||||
"currenciesAccepted",
|
||||
"smokingAllowed",
|
||||
"openingHoursSpecification",
|
||||
"audience",
|
||||
"review",
|
||||
"parentOrganization",
|
||||
"openingDate",
|
||||
"award"
|
||||
]
|
||||
},
|
||||
"LodgingBusiness": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"telephone",
|
||||
"image",
|
||||
"priceRange",
|
||||
"geo",
|
||||
"url",
|
||||
"starRating",
|
||||
"aggregateRating",
|
||||
"checkinTime",
|
||||
"checkoutTime"
|
||||
],
|
||||
"allowed": [
|
||||
"email",
|
||||
"amenityFeature",
|
||||
"petsAllowed",
|
||||
"numberOfRooms",
|
||||
"availableLanguage",
|
||||
"containedInPlace",
|
||||
"containsPlace",
|
||||
"makesOffer",
|
||||
"currenciesAccepted",
|
||||
"smokingAllowed"
|
||||
]
|
||||
},
|
||||
"Resort": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"telephone",
|
||||
"image",
|
||||
"priceRange",
|
||||
"geo",
|
||||
"url",
|
||||
"starRating",
|
||||
"aggregateRating"
|
||||
],
|
||||
"allowed": [
|
||||
"email",
|
||||
"amenityFeature",
|
||||
"numberOfRooms",
|
||||
"containedInPlace",
|
||||
"containsPlace",
|
||||
"checkinTime",
|
||||
"checkoutTime"
|
||||
]
|
||||
},
|
||||
"Restaurant": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"servesCuisine",
|
||||
"priceRange",
|
||||
"telephone",
|
||||
"menu",
|
||||
"openingHoursSpecification",
|
||||
"image",
|
||||
"url",
|
||||
"geo",
|
||||
"acceptsReservations"
|
||||
],
|
||||
"allowed": [
|
||||
"email",
|
||||
"hasMenu",
|
||||
"starRating",
|
||||
"aggregateRating",
|
||||
"review",
|
||||
"containedInPlace",
|
||||
"smokingAllowed",
|
||||
"award"
|
||||
]
|
||||
},
|
||||
"FoodEstablishment": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"servesCuisine",
|
||||
"priceRange",
|
||||
"telephone",
|
||||
"menu",
|
||||
"openingHoursSpecification"
|
||||
],
|
||||
"allowed": [
|
||||
"email",
|
||||
"hasMenu",
|
||||
"acceptsReservations",
|
||||
"containedInPlace"
|
||||
]
|
||||
},
|
||||
"BarOrPub": {
|
||||
"required": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"recommended": [
|
||||
"telephone",
|
||||
"openingHoursSpecification",
|
||||
"priceRange",
|
||||
"servesCuisine"
|
||||
],
|
||||
"allowed": [
|
||||
"menu",
|
||||
"hasMenu",
|
||||
"image",
|
||||
"url",
|
||||
"containedInPlace",
|
||||
"acceptsReservations",
|
||||
"award"
|
||||
]
|
||||
},
|
||||
"FAQPage": {
|
||||
"required": [
|
||||
"mainEntity"
|
||||
],
|
||||
"recommended": [],
|
||||
"allowed": [
|
||||
"about",
|
||||
"headline",
|
||||
"datePublished",
|
||||
"dateModified",
|
||||
"isPartOf"
|
||||
]
|
||||
},
|
||||
"Question": {
|
||||
"required": [
|
||||
"name",
|
||||
"acceptedAnswer"
|
||||
],
|
||||
"recommended": [],
|
||||
"allowed": [
|
||||
"text",
|
||||
"answerCount",
|
||||
"suggestedAnswer",
|
||||
"upvoteCount",
|
||||
"author"
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"recommended": [],
|
||||
"allowed": [
|
||||
"url",
|
||||
"upvoteCount",
|
||||
"author",
|
||||
"dateCreated"
|
||||
]
|
||||
},
|
||||
"BreadcrumbList": {
|
||||
"required": [
|
||||
"itemListElement"
|
||||
],
|
||||
"recommended": [],
|
||||
"allowed": [
|
||||
"numberOfItems",
|
||||
"itemListOrder"
|
||||
]
|
||||
},
|
||||
"ItemList": {
|
||||
"required": [
|
||||
"itemListElement"
|
||||
],
|
||||
"recommended": [],
|
||||
"allowed": [
|
||||
"numberOfItems",
|
||||
"itemListOrder"
|
||||
]
|
||||
},
|
||||
"ListItem": {
|
||||
"required": [
|
||||
"position"
|
||||
],
|
||||
"recommended": [
|
||||
"item",
|
||||
"name"
|
||||
],
|
||||
"allowed": [
|
||||
"url",
|
||||
"image",
|
||||
"nextItem",
|
||||
"previousItem"
|
||||
]
|
||||
},
|
||||
"Product": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"image",
|
||||
"offers",
|
||||
"brand",
|
||||
"aggregateRating",
|
||||
"review",
|
||||
"description",
|
||||
"sku"
|
||||
],
|
||||
"allowed": [
|
||||
"gtin",
|
||||
"gtin13",
|
||||
"gtin8",
|
||||
"gtin12",
|
||||
"mpn",
|
||||
"color",
|
||||
"material",
|
||||
"category",
|
||||
"audience",
|
||||
"isVariantOf",
|
||||
"additionalProperty",
|
||||
"hasMerchantReturnPolicy"
|
||||
]
|
||||
},
|
||||
"Offer": {
|
||||
"required": [
|
||||
"price",
|
||||
"priceCurrency"
|
||||
],
|
||||
"recommended": [
|
||||
"availability",
|
||||
"url",
|
||||
"validFrom",
|
||||
"priceValidUntil"
|
||||
],
|
||||
"allowed": [
|
||||
"itemCondition",
|
||||
"seller",
|
||||
"eligibleRegion",
|
||||
"priceSpecification",
|
||||
"shippingDetails",
|
||||
"availabilityStarts"
|
||||
]
|
||||
},
|
||||
"AggregateOffer": {
|
||||
"required": [
|
||||
"lowPrice",
|
||||
"priceCurrency"
|
||||
],
|
||||
"recommended": [
|
||||
"highPrice",
|
||||
"offerCount"
|
||||
],
|
||||
"allowed": [
|
||||
"offers",
|
||||
"availability"
|
||||
]
|
||||
},
|
||||
"Article": {
|
||||
"required": [
|
||||
"headline"
|
||||
],
|
||||
"recommended": [
|
||||
"author",
|
||||
"datePublished",
|
||||
"image",
|
||||
"dateModified",
|
||||
"publisher"
|
||||
],
|
||||
"allowed": [
|
||||
"articleBody",
|
||||
"articleSection",
|
||||
"wordCount",
|
||||
"keywords",
|
||||
"speakable"
|
||||
]
|
||||
},
|
||||
"NewsArticle": {
|
||||
"required": [
|
||||
"headline"
|
||||
],
|
||||
"recommended": [
|
||||
"author",
|
||||
"datePublished",
|
||||
"image",
|
||||
"dateModified",
|
||||
"publisher"
|
||||
],
|
||||
"allowed": [
|
||||
"articleBody",
|
||||
"dateline",
|
||||
"printSection"
|
||||
]
|
||||
},
|
||||
"BlogPosting": {
|
||||
"required": [
|
||||
"headline"
|
||||
],
|
||||
"recommended": [
|
||||
"author",
|
||||
"datePublished",
|
||||
"image",
|
||||
"dateModified",
|
||||
"publisher"
|
||||
],
|
||||
"allowed": [
|
||||
"articleBody",
|
||||
"keywords",
|
||||
"wordCount"
|
||||
]
|
||||
},
|
||||
"Event": {
|
||||
"required": [
|
||||
"name",
|
||||
"startDate",
|
||||
"location"
|
||||
],
|
||||
"recommended": [
|
||||
"endDate",
|
||||
"offers",
|
||||
"performer",
|
||||
"image",
|
||||
"eventStatus",
|
||||
"eventAttendanceMode",
|
||||
"organizer"
|
||||
],
|
||||
"allowed": [
|
||||
"doorTime",
|
||||
"previousStartDate",
|
||||
"typicalAgeRange",
|
||||
"maximumAttendeeCapacity"
|
||||
]
|
||||
},
|
||||
"Review": {
|
||||
"required": [
|
||||
"reviewRating",
|
||||
"author"
|
||||
],
|
||||
"recommended": [
|
||||
"datePublished",
|
||||
"reviewBody",
|
||||
"itemReviewed"
|
||||
],
|
||||
"allowed": [
|
||||
"publisher",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"AggregateRating": {
|
||||
"required": [
|
||||
"ratingValue"
|
||||
],
|
||||
"recommended": [
|
||||
"reviewCount",
|
||||
"ratingCount",
|
||||
"bestRating"
|
||||
],
|
||||
"allowed": [
|
||||
"worstRating",
|
||||
"itemReviewed"
|
||||
]
|
||||
},
|
||||
"MemberProgram": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"hasTiers",
|
||||
"hostingOrganization",
|
||||
"url"
|
||||
],
|
||||
"allowed": [
|
||||
"description",
|
||||
"membershipPointsEarned"
|
||||
]
|
||||
},
|
||||
"EventVenue": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"address",
|
||||
"maximumAttendeeCapacity",
|
||||
"image"
|
||||
],
|
||||
"allowed": [
|
||||
"containedInPlace",
|
||||
"amenityFeature",
|
||||
"openingHoursSpecification",
|
||||
"photo",
|
||||
"telephone",
|
||||
"alternateName",
|
||||
"geo"
|
||||
]
|
||||
},
|
||||
"ExerciseGym": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"address",
|
||||
"openingHoursSpecification",
|
||||
"image"
|
||||
],
|
||||
"allowed": [
|
||||
"containedInPlace",
|
||||
"amenityFeature",
|
||||
"telephone",
|
||||
"priceRange",
|
||||
"alternateName"
|
||||
]
|
||||
},
|
||||
"SportsActivityLocation": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"address"
|
||||
],
|
||||
"allowed": [
|
||||
"containedInPlace",
|
||||
"amenityFeature",
|
||||
"openingHoursSpecification",
|
||||
"telephone",
|
||||
"alternateName"
|
||||
]
|
||||
},
|
||||
"HealthAndBeautyBusiness": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"address",
|
||||
"telephone",
|
||||
"openingHoursSpecification",
|
||||
"priceRange",
|
||||
"image"
|
||||
],
|
||||
"allowed": [
|
||||
"containedInPlace",
|
||||
"amenityFeature",
|
||||
"potentialAction",
|
||||
"parentOrganization",
|
||||
"geo",
|
||||
"alternateName"
|
||||
]
|
||||
},
|
||||
"DaySpa": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"address",
|
||||
"telephone",
|
||||
"openingHoursSpecification",
|
||||
"priceRange",
|
||||
"image"
|
||||
],
|
||||
"allowed": [
|
||||
"containedInPlace",
|
||||
"amenityFeature",
|
||||
"potentialAction",
|
||||
"parentOrganization",
|
||||
"geo",
|
||||
"alternateName"
|
||||
]
|
||||
},
|
||||
"CafeOrCoffeeShop": {
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"recommended": [
|
||||
"url",
|
||||
"address",
|
||||
"servesCuisine",
|
||||
"priceRange",
|
||||
"telephone",
|
||||
"openingHoursSpecification",
|
||||
"image"
|
||||
],
|
||||
"allowed": [
|
||||
"menu",
|
||||
"hasMenu",
|
||||
"containedInPlace",
|
||||
"acceptsReservations",
|
||||
"alternateName"
|
||||
]
|
||||
}
|
||||
},
|
||||
"container_types": [
|
||||
"PostalAddress",
|
||||
"GeoCoordinates",
|
||||
"GeoShape",
|
||||
"ImageObject",
|
||||
"VideoObject",
|
||||
"ContactPoint",
|
||||
"OpeningHoursSpecification",
|
||||
"Rating",
|
||||
"QuantitativeValue",
|
||||
"MonetaryAmount",
|
||||
"PriceSpecification",
|
||||
"Brand",
|
||||
"EntryPoint",
|
||||
"Place",
|
||||
"OfferCatalog",
|
||||
"ReserveAction",
|
||||
"OrderAction",
|
||||
"SearchAction",
|
||||
"ViewAction",
|
||||
"MeetingRoom",
|
||||
"Room",
|
||||
"HotelRoom",
|
||||
"Suite",
|
||||
"LocationFeatureSpecification",
|
||||
"MemberProgramTier",
|
||||
"MobileApplication",
|
||||
"WebApplication",
|
||||
"SoftwareApplication",
|
||||
"Menu",
|
||||
"MenuItem",
|
||||
"MenuSection",
|
||||
"Country",
|
||||
"AdministrativeArea",
|
||||
"Duration",
|
||||
"PropertyValue",
|
||||
"Person",
|
||||
"Audience",
|
||||
"Language",
|
||||
"LodgingReservation"
|
||||
],
|
||||
"value_formats": {
|
||||
"url_props": [
|
||||
"url",
|
||||
"logo",
|
||||
"sameAs",
|
||||
"image",
|
||||
"contentUrl",
|
||||
"thumbnailUrl",
|
||||
"target",
|
||||
"urlTemplate",
|
||||
"installUrl",
|
||||
"menu",
|
||||
"hasMap",
|
||||
"downloadUrl",
|
||||
"embedUrl"
|
||||
],
|
||||
"date_props": [
|
||||
"datePublished",
|
||||
"dateModified",
|
||||
"dateCreated",
|
||||
"startDate",
|
||||
"endDate",
|
||||
"validFrom",
|
||||
"validThrough",
|
||||
"priceValidUntil",
|
||||
"foundingDate",
|
||||
"uploadDate",
|
||||
"availabilityStarts",
|
||||
"availabilityEnds",
|
||||
"lastReviewed",
|
||||
"previousStartDate"
|
||||
],
|
||||
"lang_props": [
|
||||
"inLanguage",
|
||||
"availableLanguage"
|
||||
],
|
||||
"currency_props": [
|
||||
"priceCurrency",
|
||||
"currenciesAccepted"
|
||||
],
|
||||
"number_props": [
|
||||
"price",
|
||||
"lowPrice",
|
||||
"highPrice",
|
||||
"ratingValue",
|
||||
"reviewCount",
|
||||
"ratingCount",
|
||||
"bestRating",
|
||||
"worstRating",
|
||||
"position",
|
||||
"numberOfRooms",
|
||||
"maxValue",
|
||||
"minValue",
|
||||
"offerCount"
|
||||
]
|
||||
},
|
||||
"valid_currencies": [
|
||||
"KRW",
|
||||
"USD",
|
||||
"EUR",
|
||||
"JPY",
|
||||
"CNY",
|
||||
"GBP",
|
||||
"HKD",
|
||||
"SGD",
|
||||
"THB",
|
||||
"AUD",
|
||||
"CAD",
|
||||
"CHF",
|
||||
"TWD",
|
||||
"MYR",
|
||||
"PHP",
|
||||
"VND",
|
||||
"IDR",
|
||||
"INR"
|
||||
],
|
||||
"valid_language_codes": [
|
||||
"ko",
|
||||
"en",
|
||||
"ja",
|
||||
"zh",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"ko-KR",
|
||||
"en-US",
|
||||
"en-GB",
|
||||
"ja-JP",
|
||||
"fr",
|
||||
"de",
|
||||
"es",
|
||||
"ru",
|
||||
"th",
|
||||
"vi",
|
||||
"id",
|
||||
"ms"
|
||||
],
|
||||
"placeholder_tokens": [
|
||||
"lorem ipsum",
|
||||
"lorem",
|
||||
"ipsum",
|
||||
"dolor sit",
|
||||
"todo",
|
||||
"tbd",
|
||||
"fixme",
|
||||
"xxx",
|
||||
"yyy",
|
||||
"zzz",
|
||||
"placeholder",
|
||||
"insert here",
|
||||
"insert text",
|
||||
"example.com",
|
||||
"your-domain",
|
||||
"yourdomain",
|
||||
"changeme",
|
||||
"sample text",
|
||||
"{{",
|
||||
"}}",
|
||||
"<insert",
|
||||
"[insert",
|
||||
"n/a",
|
||||
"샘플",
|
||||
"예시",
|
||||
"여기에",
|
||||
"변경필요",
|
||||
"수정필요",
|
||||
"입력필요",
|
||||
"내용입력",
|
||||
"테스트",
|
||||
"임시"
|
||||
],
|
||||
"geo": {
|
||||
"lat_min": -90.0,
|
||||
"lat_max": 90.0,
|
||||
"lon_min": -180.0,
|
||||
"lon_max": 180.0,
|
||||
"kr_lat_range": [
|
||||
33.0,
|
||||
39.0
|
||||
],
|
||||
"kr_lon_range": [
|
||||
124.0,
|
||||
132.0
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -607,6 +607,16 @@ def _address_street(node):
|
||||
return ""
|
||||
|
||||
|
||||
def _address_locality(node):
|
||||
"""City/region key, used to keep distinct same-name locations of a chain apart."""
|
||||
addr = node.get("address")
|
||||
if isinstance(addr, list) and addr and isinstance(addr[0], dict):
|
||||
addr = addr[0]
|
||||
if isinstance(addr, dict):
|
||||
return normalize_name(addr.get("addressLocality") or addr.get("addressRegion"))
|
||||
return ""
|
||||
|
||||
|
||||
def _walk_ids(obj, defined, referenced):
|
||||
"""Collect @id definitions vs pure references by walking the whole document.
|
||||
|
||||
@@ -645,21 +655,26 @@ def layer4_consistency(node_index, parsed_docs, rules, defects):
|
||||
break # one placeholder defect per node is enough signal
|
||||
|
||||
# ---- NAP consistency (P0) ----
|
||||
# Group by (name, locality): a multi-location chain legitimately shares a name
|
||||
# across cities (e.g. "더 파크뷰" in Seoul AND Jeju). A real NAP conflict is a
|
||||
# SINGLE location with contradictory phone/street, so scope the check per city.
|
||||
by_name = defaultdict(list)
|
||||
for entry, node in node_index:
|
||||
if type_of(node) in NAP_TYPES and node.get("name"):
|
||||
by_name[normalize_name(first_text(node.get("name")))].append((entry, node))
|
||||
for name, group in by_name.items():
|
||||
key = (normalize_name(first_text(node.get("name"))), _address_locality(node))
|
||||
by_name[key].append((entry, node))
|
||||
for (name, locality), group in by_name.items():
|
||||
loc = f" ({locality})" if locality else ""
|
||||
phones = {str(first_text(n.get("telephone"))).strip()
|
||||
for _, n in group if n.get("telephone")}
|
||||
streets = {_address_street(n) for _, n in group if _address_street(n)}
|
||||
if len(phones) > 1:
|
||||
defects.add("P0", "L4", "NAP_PHONE_MISMATCH",
|
||||
f"Business '{name}' has conflicting telephone values across "
|
||||
f"Business '{name}'{loc} has conflicting telephone values across "
|
||||
f"entries: {sorted(phones)}.", entry_id="(dataset)")
|
||||
if len(streets) > 1:
|
||||
defects.add("P0", "L4", "NAP_ADDRESS_MISMATCH",
|
||||
f"Business '{name}' has conflicting streetAddress values across "
|
||||
f"Business '{name}'{loc} has conflicting streetAddress values across "
|
||||
f"entries: {sorted(streets)}.", entry_id="(dataset)")
|
||||
|
||||
# ---- @id duplicates + dangling references (P1) ----
|
||||
@@ -719,10 +734,147 @@ def layer4_consistency(node_index, parsed_docs, rules, defects):
|
||||
f"(e.g. {sorted(eids)[:3]}): {desc[:50]!r}…", entry_id="(dataset)")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Layer R — Reference-URL integrity (sameAs / external identity links)
|
||||
# Hardened after the Shilla incident (2026-05-29): LLM-fabricated Wikidata IDs
|
||||
# (Q-numbers pointing to unrelated entities) and google.com/search reference
|
||||
# URLs shipped undetected. Offline: forbid search-result URLs (P0) and flag any
|
||||
# external identity ref as REFERENCE_UNVERIFIED (P1). Online (--verify-refs):
|
||||
# resolve every ref; for Wikidata, fetch the label and compare to the entity
|
||||
# name — a mismatch is a FALSE_REFERENCE (P0).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _http_get(url, timeout=12, accept=None):
|
||||
import urllib.request, urllib.parse
|
||||
# percent-encode non-ASCII path/query (e.g. ko.wikipedia.org/wiki/호텔신라)
|
||||
p = urllib.parse.urlsplit(url)
|
||||
url = urllib.parse.urlunsplit((p.scheme, p.netloc,
|
||||
urllib.parse.quote(p.path),
|
||||
urllib.parse.quote(p.query, safe="=&?"),
|
||||
p.fragment))
|
||||
req = urllib.request.Request(url, headers={
|
||||
"User-Agent": "schema-ref-validator/1.0 (+offline-qa)",
|
||||
**({"Accept": accept} if accept else {})})
|
||||
return urllib.request.urlopen(req, timeout=timeout)
|
||||
|
||||
|
||||
def _wikidata_labels(qid, timeout=12):
|
||||
import urllib.request, json as _json
|
||||
url = f"https://www.wikidata.org/w/api.php?action=wbgetentities&ids={qid}&props=labels&format=json"
|
||||
with _http_get(url, timeout=timeout, accept="application/json") as r:
|
||||
data = _json.loads(r.read().decode("utf-8"))
|
||||
ent = data.get("entities", {}).get(qid, {})
|
||||
if "missing" in ent:
|
||||
return None
|
||||
return {lang: v.get("value", "") for lang, v in ent.get("labels", {}).items()}
|
||||
|
||||
|
||||
def layer_references(node_index, rules, defects, verify_refs=False):
|
||||
policy = rules.get("reference_policy")
|
||||
if not policy:
|
||||
return
|
||||
forbidden = policy.get("forbidden_url_substrings", [])
|
||||
ref_props = set(policy.get("identity_ref_props", ["sameAs"]))
|
||||
import re as _re
|
||||
qid_re = _re.compile(r"wikidata\.org/(?:wiki|entity)/(Q\d+)")
|
||||
def every_string(obj):
|
||||
# Unlike all_strings(), this also yields strings nested inside lists
|
||||
# (e.g. each URL in a sameAs array) — the exact case missed before.
|
||||
if isinstance(obj, dict):
|
||||
for v in obj.values():
|
||||
yield from every_string(v)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
yield from every_string(v)
|
||||
elif isinstance(obj, str):
|
||||
yield obj
|
||||
|
||||
for entry, node in node_index:
|
||||
eid, url, ntype = entry["entry_id"], entry["url"], type_of(node)
|
||||
# (1) forbidden search-result URLs anywhere in the node (incl. list items) -> P0
|
||||
for val in every_string(node):
|
||||
low = val.lower()
|
||||
hit = next((s for s in forbidden if s in low), None)
|
||||
if hit:
|
||||
defects.add("P0", "LR", "FORBIDDEN_REFERENCE",
|
||||
f"Search-result URL used as reference (contains {hit!r}); "
|
||||
f"not a valid entity reference: {val[:80]!r}.", eid, url, ntype)
|
||||
# (2) external identity references (sameAs)
|
||||
refs = []
|
||||
for prop in ref_props:
|
||||
v = node.get(prop)
|
||||
if isinstance(v, str):
|
||||
refs.append(v)
|
||||
elif isinstance(v, list):
|
||||
refs += [x for x in v if isinstance(x, str)]
|
||||
if not refs:
|
||||
continue
|
||||
# (2a) discouraged reference sources (policy: prefer Wikipedia over Wikidata)
|
||||
for dd in policy.get("discouraged_ref_domains", []):
|
||||
for ref in refs:
|
||||
if dd in ref:
|
||||
defects.add("P1", "LR", "DISCOURAGED_REFERENCE",
|
||||
f"{ref} uses a discouraged source ({dd}). Policy: prefer "
|
||||
"Wikipedia; if none verified, omit — never fabricate.",
|
||||
eid, url, ntype)
|
||||
if not verify_refs:
|
||||
defects.add("P1", "LR", "REFERENCE_UNVERIFIED",
|
||||
f"{len(refs)} external reference(s) on '{ntype}' not machine-verified "
|
||||
f"(run with --verify-refs / confirm online): {refs}.", eid, url, ntype)
|
||||
continue
|
||||
# online verification
|
||||
name = normalize_name(first_text(node.get("name")))
|
||||
alts = node.get("alternateName") or []
|
||||
if isinstance(alts, str):
|
||||
alts = [alts]
|
||||
names = {name} | {normalize_name(a) for a in alts if isinstance(a, str)}
|
||||
for ref in refs:
|
||||
m = qid_re.search(ref)
|
||||
if m:
|
||||
try:
|
||||
labels = _wikidata_labels(m.group(1))
|
||||
except Exception as e:
|
||||
defects.add("P1", "LR", "REFERENCE_UNREACHABLE",
|
||||
f"Could not fetch Wikidata {m.group(1)} ({e}).", eid, url, ntype)
|
||||
continue
|
||||
if labels is None:
|
||||
defects.add("P0", "LR", "FALSE_REFERENCE",
|
||||
f"sameAs {ref} → Wikidata item is missing/deleted.", eid, url, ntype)
|
||||
continue
|
||||
lab = {normalize_name(v) for v in labels.values()}
|
||||
# match if any entity name appears in any label or vice-versa
|
||||
ok = any(n and (n in l or l in n) for n in names for l in lab)
|
||||
if not ok:
|
||||
defects.add("P0", "LR", "FALSE_REFERENCE",
|
||||
f"sameAs {ref} label {sorted(lab)[:3]} does NOT match entity "
|
||||
f"name {sorted(n for n in names if n)[:3]} — fabricated/incorrect ID.",
|
||||
eid, url, ntype)
|
||||
else:
|
||||
is_social = any(d in ref for d in policy.get("social_profile_domains", []))
|
||||
try:
|
||||
code = _http_get(ref).status
|
||||
except Exception as e:
|
||||
code = f"error: {e}"
|
||||
if is_social:
|
||||
# HTTP 200 does NOT prove official ownership, and platforms
|
||||
# often bot-block live pages (e.g. Facebook 400). Always hand
|
||||
# social/profile refs to a human. (Shilla: a 200 YouTube
|
||||
# channel was not the official one; FB page was closed.)
|
||||
defects.add("P1", "LR", "SOCIAL_UNVERIFIED",
|
||||
f"sameAs {ref} is a social/profile URL (HTTP {code}). "
|
||||
"Confirm official ownership AND active status manually — "
|
||||
"a 200 is not proof of ownership.", eid, url, ntype)
|
||||
elif isinstance(code, int) and code >= 400:
|
||||
defects.add("P0", "LR", "BROKEN_REFERENCE",
|
||||
f"sameAs {ref} returned HTTP {code}.", eid, url, ntype)
|
||||
elif not isinstance(code, int):
|
||||
defects.add("P1", "LR", "REFERENCE_UNREACHABLE",
|
||||
f"sameAs {ref} not reachable ({code}).", eid, url, ntype)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Orchestration + output
|
||||
# --------------------------------------------------------------------------- #
|
||||
def run(entries, rules, inventory, strict, no_recommended):
|
||||
def run(entries, rules, inventory, strict, no_recommended, verify_refs=False):
|
||||
defects = DefectLog()
|
||||
if inventory is not None:
|
||||
layer0_coverage(entries, inventory, defects)
|
||||
@@ -743,6 +895,7 @@ def run(entries, rules, inventory, strict, no_recommended):
|
||||
node_index.append((entry, node))
|
||||
|
||||
layer4_consistency(node_index, parsed_docs, rules, defects)
|
||||
layer_references(node_index, rules, defects, verify_refs=verify_refs)
|
||||
return defects, valid_entries, len(node_index)
|
||||
|
||||
|
||||
@@ -822,6 +975,9 @@ def main(argv=None):
|
||||
ap.add_argument("--live", nargs="+", metavar="URL",
|
||||
help="Mode B: validate live URLs (extract embedded JSON-LD)")
|
||||
ap.add_argument("--rules", default=str(RULES_DEFAULT), help="path to schema_rules.json")
|
||||
ap.add_argument("--verify-refs", action="store_true",
|
||||
help="online: resolve every sameAs and verify Wikidata labels match the "
|
||||
"entity name (catches fabricated/incorrect reference IDs). Needs network.")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if not args.dataset and not args.live:
|
||||
@@ -838,7 +994,8 @@ def main(argv=None):
|
||||
inventory = load_url_inventory(args.url_list) if args.url_list else None
|
||||
|
||||
defects, valid_entries, nodes = run(entries, rules, inventory,
|
||||
args.strict, args.no_recommended)
|
||||
args.strict, args.no_recommended,
|
||||
verify_refs=args.verify_refs)
|
||||
meta = {"entries": len(entries), "valid_entries": valid_entries, "nodes": nodes,
|
||||
"mode": "B-live" if args.live else "A-dataset", "strict": args.strict,
|
||||
"coverage": inventory is not None}
|
||||
|
||||
@@ -0,0 +1,854 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
validate_schema.py — 5-layer offline JSON-LD schema validator.
|
||||
|
||||
WHY THIS EXISTS
|
||||
---------------
|
||||
When a client reviews hundreds of authored schema entries and says "there are too
|
||||
many errors," the root cause is almost always that nobody ran a machine lint first.
|
||||
Humans end up eyeballing raw JSON in a meeting. This tool moves every cheap,
|
||||
machine-checkable error OUT of human review and INTO an automated gate that runs
|
||||
first — so the client only ever sees clean, P0-free entries plus a defect report.
|
||||
|
||||
It is OFFLINE by design (the runtime cannot reach schema.org or Google). All rules
|
||||
live in schema_rules.json; unknown types/properties degrade to warnings, never hard
|
||||
errors, so the gate does not invent false positives.
|
||||
|
||||
THE 5 LAYERS
|
||||
------------
|
||||
L0 Coverage — URLs with no entry; entries whose URL isn't in the inventory.
|
||||
L1 Syntax — invalid JSON, bad/missing @context, missing @type, encoding corruption.
|
||||
L2 Vocabulary — unknown type, value-format errors (URL/date/lang/currency/number),
|
||||
(strict only) unexpected properties on a known type.
|
||||
L3 Rich-result — Google REQUIRED property missing (blocks rich result); recommended absent.
|
||||
L4 Consistency — NAP mismatch, @id duplicates/dangling refs, swapped geo,
|
||||
placeholder text, duplicate descriptions across entries.
|
||||
|
||||
GATE: PASS iff zero P0. Process exits 1 when the gate fails (so CI/`&&` chains stop).
|
||||
|
||||
Usage:
|
||||
python validate_schema.py DATASET [--url-list URLLIST] [--out DIR]
|
||||
[--strict] [--no-recommended]
|
||||
[--live URL ...] [--rules schema_rules.json]
|
||||
DATASET may be .xlsx / .csv (one row per entry, a JSON-LD column) / .jsonl / .json
|
||||
/ a directory of .json|.jsonld files. With --live, validate live URLs instead.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
RULES_DEFAULT = Path(__file__).resolve().parent / "schema_rules.json"
|
||||
|
||||
SEVERITY_ORDER = {"P0": 0, "P1": 1, "P2": 2}
|
||||
|
||||
# Header aliases for tabular input. Keys are normalized (lowercased, spaces removed).
|
||||
COLUMN_ALIASES = {
|
||||
"jsonld": ["jsonld", "jsonld", "json-ld", "json_ld", "schema", "schemamarkup",
|
||||
"structureddata", "structured_data", "markup", "스키마", "구조화데이터",
|
||||
"구조화된데이터", "jsonldcode", "스키마코드"],
|
||||
"url": ["url", "메뉴url", "pageurl", "주소", "링크", "loc", "uri", "캐노니컬", "canonical"],
|
||||
"lang": ["lang", "language", "언어", "언어코드", "locale", "lng"],
|
||||
"device": ["device", "pc/mobile", "pcmobile", "pc_mobile", "platform", "디바이스", "기기"],
|
||||
"page_type": ["page_type", "pagetype", "type", "페이지유형", "페이지타입", "menulevel",
|
||||
"menu_level", "메뉴레벨", "template", "템플릿", "유형"],
|
||||
}
|
||||
|
||||
URL_RE = re.compile(r"^https?://[^\s]+$", re.IGNORECASE)
|
||||
# ISO-8601 date or datetime (date, date+time, optional tz). Loose but rejects free text.
|
||||
DATE_RE = re.compile(
|
||||
r"^\d{4}-\d{2}-\d{2}"
|
||||
r"(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$"
|
||||
)
|
||||
LANG_RE = re.compile(r"^[a-zA-Z]{2,3}(?:-[A-Za-z0-9]{2,4})?$")
|
||||
JSONLD_SCRIPT_RE = re.compile(
|
||||
r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Defect collection
|
||||
# --------------------------------------------------------------------------- #
|
||||
class DefectLog:
|
||||
"""Accumulates findings. One row per finding, ready for triage."""
|
||||
|
||||
def __init__(self):
|
||||
self.rows = []
|
||||
|
||||
def add(self, severity, layer, code, message, entry_id="", url="", node_type=""):
|
||||
self.rows.append({
|
||||
"entry_id": str(entry_id),
|
||||
"url": url or "",
|
||||
"node_type": node_type or "",
|
||||
"layer": layer,
|
||||
"code": code,
|
||||
"severity": severity,
|
||||
"message": message,
|
||||
"status": "open",
|
||||
"owner": "",
|
||||
"note": "",
|
||||
})
|
||||
|
||||
def counts(self):
|
||||
c = Counter(r["severity"] for r in self.rows)
|
||||
return {"P0": c.get("P0", 0), "P1": c.get("P1", 0), "P2": c.get("P2", 0)}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Input adapters (Mode A: authored dataset / Mode B: live URLs)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _norm_header(h):
|
||||
return re.sub(r"\s+", "", str(h or "").strip().lower())
|
||||
|
||||
|
||||
def _detect_columns(headers):
|
||||
"""Map normalized headers to canonical column roles. Returns {role: index}."""
|
||||
found = {}
|
||||
for idx, h in enumerate(headers):
|
||||
nh = _norm_header(h)
|
||||
for role, aliases in COLUMN_ALIASES.items():
|
||||
if role in found:
|
||||
continue
|
||||
if nh in aliases:
|
||||
found[role] = idx
|
||||
return found
|
||||
|
||||
|
||||
def _row_to_entry(row, cols, entry_id, source_ref):
|
||||
def cell(role):
|
||||
i = cols.get(role)
|
||||
if i is None or i >= len(row):
|
||||
return None
|
||||
v = row[i]
|
||||
return None if v is None else str(v).strip()
|
||||
raw = cell("jsonld")
|
||||
if not raw:
|
||||
return None # blank JSON-LD cell → no entry to validate
|
||||
return {
|
||||
"entry_id": entry_id,
|
||||
"url": cell("url") or "",
|
||||
"lang": cell("lang") or "",
|
||||
"device": cell("device") or "",
|
||||
"page_type": cell("page_type") or "",
|
||||
"raw": raw,
|
||||
"source_ref": source_ref,
|
||||
}
|
||||
|
||||
|
||||
def _load_csv(path):
|
||||
entries = []
|
||||
with open(path, newline="", encoding="utf-8-sig") as f:
|
||||
reader = csv.reader(f)
|
||||
rows = list(reader)
|
||||
if not rows:
|
||||
return entries
|
||||
cols = _detect_columns(rows[0])
|
||||
if "jsonld" not in cols:
|
||||
raise ValueError(
|
||||
f"No JSON-LD column found in {path}. Looked for: "
|
||||
f"{', '.join(COLUMN_ALIASES['jsonld'][:6])} … Headers were: {rows[0]}"
|
||||
)
|
||||
for n, row in enumerate(rows[1:], start=2):
|
||||
e = _row_to_entry(row, cols, f"{Path(path).stem}#r{n}", f"{path}:row{n}")
|
||||
if e:
|
||||
entries.append(e)
|
||||
return entries
|
||||
|
||||
|
||||
def _load_xlsx(path):
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
except ImportError:
|
||||
raise SystemExit(
|
||||
"Reading .xlsx needs openpyxl: pip install openpyxl\n"
|
||||
"(or export the sheet to .csv and pass that instead)."
|
||||
)
|
||||
entries = []
|
||||
wb = load_workbook(path, read_only=True, data_only=True)
|
||||
for sheet in wb.worksheets:
|
||||
rows = list(sheet.iter_rows(values_only=True))
|
||||
if not rows:
|
||||
continue
|
||||
cols = _detect_columns(rows[0])
|
||||
if "jsonld" not in cols:
|
||||
continue # a tab without a JSON-LD column (e.g. summary) — skip silently
|
||||
for n, row in enumerate(rows[1:], start=2):
|
||||
e = _row_to_entry(list(row), cols, f"{sheet.title}#r{n}",
|
||||
f"{path}:{sheet.title}:row{n}")
|
||||
if e:
|
||||
entries.append(e)
|
||||
if not entries:
|
||||
raise ValueError(
|
||||
f"No sheet in {path} had a recognizable JSON-LD column. "
|
||||
f"Looked for: {', '.join(COLUMN_ALIASES['jsonld'][:6])} …"
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def _looks_like_schema(obj):
|
||||
"""True if a parsed object is itself JSON-LD (vs a wrapper row)."""
|
||||
if isinstance(obj, list):
|
||||
return True
|
||||
if isinstance(obj, dict):
|
||||
return any(k in obj for k in ("@context", "@type", "@graph"))
|
||||
return False
|
||||
|
||||
|
||||
def _wrapper_to_entry(obj, entry_id, source_ref):
|
||||
"""A JSONL/JSON wrapper object that carries url/lang + a jsonld payload."""
|
||||
cols = {k: k for k in obj.keys()}
|
||||
norm = {_norm_header(k): k for k in obj.keys()}
|
||||
def pick(role):
|
||||
for alias in COLUMN_ALIASES[role]:
|
||||
if alias in norm:
|
||||
v = obj[norm[alias]]
|
||||
return v
|
||||
return None
|
||||
payload = pick("jsonld")
|
||||
raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False)
|
||||
url = pick("url")
|
||||
return {
|
||||
"entry_id": entry_id,
|
||||
"url": str(url).strip() if url else "",
|
||||
"lang": str(pick("lang") or "").strip(),
|
||||
"device": str(pick("device") or "").strip(),
|
||||
"page_type": str(pick("page_type") or "").strip(),
|
||||
"raw": raw,
|
||||
"source_ref": source_ref,
|
||||
}
|
||||
|
||||
|
||||
def _load_jsonl(path):
|
||||
entries = []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for n, line in enumerate(f, start=1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
sref = f"{path}:line{n}"
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
# Keep the bad line so L1 reports it as a syntax error.
|
||||
entries.append({"entry_id": f"{Path(path).stem}#l{n}", "url": "",
|
||||
"lang": "", "device": "", "page_type": "",
|
||||
"raw": line, "source_ref": sref})
|
||||
continue
|
||||
eid = f"{Path(path).stem}#l{n}"
|
||||
if _looks_like_schema(obj):
|
||||
entries.append({"entry_id": eid, "url": "", "lang": "", "device": "",
|
||||
"page_type": "", "raw": line, "source_ref": sref})
|
||||
else:
|
||||
entries.append(_wrapper_to_entry(obj, eid, sref))
|
||||
return entries
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
entries = []
|
||||
if isinstance(data, dict) and not _looks_like_schema(data) and all(
|
||||
isinstance(v, (dict, list, str)) for v in data.values()
|
||||
) and not any(k.startswith("@") for k in data):
|
||||
# url -> jsonld map
|
||||
for url, payload in data.items():
|
||||
raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False)
|
||||
entries.append({"entry_id": url, "url": url, "lang": "", "device": "",
|
||||
"page_type": "", "raw": raw, "source_ref": f"{path}:{url}"})
|
||||
elif isinstance(data, list):
|
||||
for n, item in enumerate(data, start=1):
|
||||
sref = f"{path}:[{n}]"
|
||||
eid = f"{Path(path).stem}#{n}"
|
||||
if _looks_like_schema(item) or not isinstance(item, dict):
|
||||
raw = item if isinstance(item, str) else json.dumps(item, ensure_ascii=False)
|
||||
entries.append({"entry_id": eid, "url": "", "lang": "", "device": "",
|
||||
"page_type": "", "raw": raw, "source_ref": sref})
|
||||
else:
|
||||
entries.append(_wrapper_to_entry(item, eid, sref))
|
||||
else:
|
||||
entries.append({"entry_id": Path(path).stem, "url": "", "lang": "", "device": "",
|
||||
"page_type": "", "raw": json.dumps(data, ensure_ascii=False),
|
||||
"source_ref": path})
|
||||
return entries
|
||||
|
||||
|
||||
def _load_dir(path):
|
||||
entries = []
|
||||
for p in sorted(Path(path).rglob("*")):
|
||||
if p.suffix.lower() in (".json", ".jsonld"):
|
||||
entries.append({"entry_id": p.stem, "url": "", "lang": "", "device": "",
|
||||
"page_type": "", "raw": p.read_text(encoding="utf-8"),
|
||||
"source_ref": str(p)})
|
||||
if not entries:
|
||||
raise ValueError(f"No .json/.jsonld files found under {path}")
|
||||
return entries
|
||||
|
||||
|
||||
def _load_live(urls):
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
raise SystemExit("Live mode (--live) needs requests: pip install requests")
|
||||
entries = []
|
||||
headers = {"User-Agent": "Mozilla/5.0 (compatible; SchemaValidator/1.0)"}
|
||||
for url in urls:
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=20)
|
||||
resp.raise_for_status()
|
||||
except Exception as exc: # noqa: BLE001 — best-effort live fetch
|
||||
entries.append({"entry_id": url, "url": url, "lang": "", "device": "",
|
||||
"page_type": "", "raw": "", "source_ref": url,
|
||||
"_fetch_error": str(exc)})
|
||||
continue
|
||||
scripts = JSONLD_SCRIPT_RE.findall(resp.text)
|
||||
if not scripts:
|
||||
entries.append({"entry_id": url, "url": url, "lang": "", "device": "",
|
||||
"page_type": "", "raw": "", "source_ref": url,
|
||||
"_no_schema": True})
|
||||
continue
|
||||
for i, block in enumerate(scripts, start=1):
|
||||
entries.append({"entry_id": f"{url}#{i}", "url": url, "lang": "",
|
||||
"device": "", "page_type": "", "raw": block.strip(),
|
||||
"source_ref": f"{url} (script {i})"})
|
||||
return entries
|
||||
|
||||
|
||||
def load_entries(input_path, live_urls):
|
||||
if live_urls:
|
||||
return _load_live(live_urls)
|
||||
p = Path(input_path)
|
||||
if p.is_dir():
|
||||
return _load_dir(p)
|
||||
suffix = p.suffix.lower()
|
||||
if suffix == ".csv":
|
||||
return _load_csv(p)
|
||||
if suffix in (".xlsx", ".xlsm"):
|
||||
return _load_xlsx(p)
|
||||
if suffix == ".jsonl":
|
||||
return _load_jsonl(p)
|
||||
if suffix in (".json", ".jsonld"):
|
||||
return _load_json(p)
|
||||
raise ValueError(f"Unsupported input: {input_path} (suffix {suffix!r})")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Node helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
def type_of(node):
|
||||
"""Return the primary @type as a string (first if it's a list)."""
|
||||
t = node.get("@type")
|
||||
if isinstance(t, list):
|
||||
return t[0] if t else ""
|
||||
return t or ""
|
||||
|
||||
|
||||
def iter_typed_nodes(parsed):
|
||||
"""Yield every dict that has an @type, top-level and nested (recursively)."""
|
||||
seen = []
|
||||
|
||||
def walk(obj):
|
||||
if isinstance(obj, dict):
|
||||
if "@type" in obj:
|
||||
seen.append(obj)
|
||||
for v in obj.values():
|
||||
walk(v)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
walk(v)
|
||||
|
||||
# @graph documents: walk the graph; otherwise walk the object/array directly.
|
||||
if isinstance(parsed, dict) and "@graph" in parsed:
|
||||
walk(parsed["@graph"])
|
||||
else:
|
||||
walk(parsed)
|
||||
return seen
|
||||
|
||||
|
||||
def all_strings(obj):
|
||||
"""Yield (key, value) for every string value anywhere in the structure."""
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, str):
|
||||
yield k, v
|
||||
else:
|
||||
yield from all_strings(v)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
yield from all_strings(v)
|
||||
|
||||
|
||||
def normalize_name(s):
|
||||
return re.sub(r"\s+", " ", str(s or "").strip().lower())
|
||||
|
||||
|
||||
def first_text(value):
|
||||
"""Coerce a property value to a comparable scalar (handles list/dict)."""
|
||||
if isinstance(value, list):
|
||||
return first_text(value[0]) if value else ""
|
||||
if isinstance(value, dict):
|
||||
return value.get("name") or value.get("@id") or value.get("streetAddress") or ""
|
||||
return value
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Layer 0 — Coverage
|
||||
# --------------------------------------------------------------------------- #
|
||||
def load_url_inventory(url_list_path):
|
||||
urls = set()
|
||||
p = Path(url_list_path)
|
||||
suffix = p.suffix.lower()
|
||||
if suffix in (".xlsx", ".xlsm"):
|
||||
from openpyxl import load_workbook
|
||||
wb = load_workbook(p, read_only=True, data_only=True)
|
||||
for sheet in wb.worksheets:
|
||||
for row in sheet.iter_rows(values_only=True):
|
||||
for cell in row:
|
||||
if isinstance(cell, str) and URL_RE.match(cell.strip()):
|
||||
urls.add(cell.strip())
|
||||
elif suffix == ".csv":
|
||||
with open(p, newline="", encoding="utf-8-sig") as f:
|
||||
for row in csv.reader(f):
|
||||
for cell in row:
|
||||
if isinstance(cell, str) and URL_RE.match(cell.strip()):
|
||||
urls.add(cell.strip())
|
||||
else: # plain text, one URL per line
|
||||
for line in p.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if URL_RE.match(line):
|
||||
urls.add(line)
|
||||
return urls
|
||||
|
||||
|
||||
def layer0_coverage(entries, inventory, defects):
|
||||
entry_urls = {e["url"] for e in entries if e.get("url")}
|
||||
missing = inventory - entry_urls
|
||||
for url in sorted(missing):
|
||||
defects.add("P1", "L0", "COVERAGE_MISSING",
|
||||
"Inventory URL has no authored schema entry.", url=url)
|
||||
orphans = entry_urls - inventory
|
||||
for url in sorted(orphans):
|
||||
defects.add("P2", "L0", "COVERAGE_ORPHAN",
|
||||
"Entry URL is not in the canonical URL inventory "
|
||||
"(typo, stale path, or missing from list).", url=url)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Layer 1 — Syntax
|
||||
# --------------------------------------------------------------------------- #
|
||||
def layer1_syntax(entry, rules, defects):
|
||||
"""Parse + structural checks. Returns parsed object or None (fatal)."""
|
||||
eid, url = entry["entry_id"], entry["url"]
|
||||
if entry.get("_fetch_error"):
|
||||
defects.add("P1", "L1", "FETCH_ERROR",
|
||||
f"Could not fetch live URL: {entry['_fetch_error']}", eid, url)
|
||||
return None
|
||||
if entry.get("_no_schema"):
|
||||
defects.add("P0", "L1", "NO_SCHEMA_IN_HTML",
|
||||
"Live page has no application/ld+json script block.", eid, url)
|
||||
return None
|
||||
raw = entry["raw"]
|
||||
if "<22>" in raw:
|
||||
defects.add("P1", "L1", "ENCODING_CORRUPTION",
|
||||
"Replacement character (\\ufffd) present — encoding corruption.",
|
||||
eid, url)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
defects.add("P0", "L1", "INVALID_JSON",
|
||||
f"JSON does not parse: {exc.msg} at line {exc.lineno} col {exc.colno}.",
|
||||
eid, url)
|
||||
return None
|
||||
|
||||
nodes = iter_typed_nodes(parsed)
|
||||
if not nodes:
|
||||
defects.add("P1", "L1", "NO_TYPE",
|
||||
"No @type found anywhere in the entry — not a usable schema object.",
|
||||
eid, url)
|
||||
|
||||
# @context lives at the top of the document; nested nodes inherit it.
|
||||
if isinstance(parsed, dict):
|
||||
ctx = parsed.get("@context")
|
||||
if ctx is None:
|
||||
defects.add("P1", "L1", "MISSING_CONTEXT",
|
||||
"Top-level @context is missing.", eid, url)
|
||||
else:
|
||||
ctx_urls = [ctx] if isinstance(ctx, str) else (
|
||||
[c for c in ctx if isinstance(c, str)] if isinstance(ctx, list) else []
|
||||
)
|
||||
valid = rules["valid_contexts"]
|
||||
if ctx_urls and not any(c.rstrip("/") in [v.rstrip("/") for v in valid]
|
||||
for c in ctx_urls):
|
||||
defects.add("P1", "L1", "WRONG_CONTEXT",
|
||||
f"@context is not schema.org: {ctx_urls}.", eid, url)
|
||||
return parsed
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Layer 2 — Vocabulary + value formats
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _check_value_formats(node, rules, defects, eid, url, ntype, severity):
|
||||
vf = rules["value_formats"]
|
||||
|
||||
def each(value):
|
||||
if isinstance(value, list):
|
||||
for v in value:
|
||||
yield from each(v)
|
||||
else:
|
||||
yield value
|
||||
|
||||
for prop, value in node.items():
|
||||
if prop.startswith("@"):
|
||||
continue
|
||||
if prop in vf["url_props"]:
|
||||
for v in each(value):
|
||||
if isinstance(v, str) and not URL_RE.match(v.strip()):
|
||||
defects.add(severity, "L2", "BAD_URL",
|
||||
f"'{prop}' is not an http(s) URL: {v!r}.", eid, url, ntype)
|
||||
if prop in vf["date_props"]:
|
||||
for v in each(value):
|
||||
if isinstance(v, str) and not DATE_RE.match(v.strip()):
|
||||
defects.add(severity, "L2", "BAD_DATE",
|
||||
f"'{prop}' is not ISO-8601: {v!r}.", eid, url, ntype)
|
||||
if prop in vf["lang_props"]:
|
||||
for v in each(value):
|
||||
if isinstance(v, str) and not LANG_RE.match(v.strip()):
|
||||
defects.add(severity, "L2", "BAD_LANG",
|
||||
f"'{prop}' is not a BCP-47 language code: {v!r}.",
|
||||
eid, url, ntype)
|
||||
if prop in vf["currency_props"]:
|
||||
for v in each(value):
|
||||
if isinstance(v, str) and not re.match(r"^[A-Z]{3}$", v.strip()):
|
||||
defects.add(severity, "L2", "BAD_CURRENCY",
|
||||
f"'{prop}' is not a 3-letter ISO-4217 code: {v!r}.",
|
||||
eid, url, ntype)
|
||||
if prop in vf["number_props"]:
|
||||
for v in each(value):
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
float(v.replace(",", ""))
|
||||
except ValueError:
|
||||
defects.add(severity, "L2", "BAD_NUMBER",
|
||||
f"'{prop}' is not numeric: {v!r}.", eid, url, ntype)
|
||||
|
||||
|
||||
def layer2_vocabulary(node, rules, defects, eid, url, strict):
|
||||
ntype = type_of(node)
|
||||
known = rules["known_types"]
|
||||
containers = set(rules["container_types"])
|
||||
minor = "P1" if strict else "P2"
|
||||
|
||||
if ntype and ntype not in known and ntype not in containers:
|
||||
defects.add(minor, "L2", "UNKNOWN_TYPE",
|
||||
f"@type '{ntype}' is not in the curated rule set "
|
||||
"(treated as a warning — add it to schema_rules.json if intended).",
|
||||
eid, url, ntype)
|
||||
|
||||
_check_value_formats(node, rules, defects, eid, url, ntype, minor)
|
||||
|
||||
# Unexpected-property check is OPT-IN (--strict). Off by default to avoid the
|
||||
# exact noise explosion that makes clients say "too many errors".
|
||||
if strict and ntype in known:
|
||||
spec = known[ntype]
|
||||
allowed = set(spec["required"]) | set(spec["recommended"]) | set(spec["allowed"])
|
||||
allowed |= set(rules["global_properties"])
|
||||
for prop in node:
|
||||
if prop.startswith("@"):
|
||||
continue
|
||||
if prop not in allowed:
|
||||
defects.add("P1", "L2", "UNEXPECTED_PROPERTY",
|
||||
f"'{prop}' is not a known property of {ntype} (strict mode).",
|
||||
eid, url, ntype)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Layer 3 — Rich-result (required / recommended)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def layer3_richresult(node, rules, defects, eid, url, no_recommended):
|
||||
ntype = type_of(node)
|
||||
known = rules["known_types"]
|
||||
if ntype not in known:
|
||||
return # containers + unknown types have no required-property contract
|
||||
spec = known[ntype]
|
||||
|
||||
for prop in spec["required"]:
|
||||
if not node.get(prop):
|
||||
defects.add("P0", "L3", "MISSING_REQUIRED",
|
||||
f"{ntype} is missing required property '{prop}' "
|
||||
"(blocks the rich result).", eid, url, ntype)
|
||||
|
||||
if not no_recommended:
|
||||
missing_rec = [p for p in spec["recommended"] if not node.get(p)]
|
||||
if missing_rec:
|
||||
# Aggregate to ONE line per node — never one defect per property.
|
||||
defects.add("P2", "L3", "MISSING_RECOMMENDED",
|
||||
f"{ntype} is missing recommended properties: "
|
||||
f"{', '.join(missing_rec)}.", eid, url, ntype)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Layer 4 — Consistency (cross-node / cross-entry)
|
||||
# --------------------------------------------------------------------------- #
|
||||
NAP_TYPES = {"Organization", "Corporation", "LocalBusiness", "Hotel",
|
||||
"LodgingBusiness", "Resort", "Restaurant", "FoodEstablishment", "BarOrPub"}
|
||||
|
||||
|
||||
def _address_street(node):
|
||||
addr = node.get("address")
|
||||
if isinstance(addr, dict):
|
||||
return normalize_name(addr.get("streetAddress"))
|
||||
if isinstance(addr, list) and addr and isinstance(addr[0], dict):
|
||||
return normalize_name(addr[0].get("streetAddress"))
|
||||
return ""
|
||||
|
||||
|
||||
def _walk_ids(obj, defined, referenced):
|
||||
"""Collect @id definitions vs pure references by walking the whole document.
|
||||
|
||||
A *reference* is an object whose only key is @id (e.g. {"@id": "...#org"}).
|
||||
A *definition* is any object carrying @id plus other content. References live
|
||||
in untyped wrapper dicts, so this must walk the raw doc — not just typed nodes.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
nid = obj.get("@id")
|
||||
if nid:
|
||||
if set(obj.keys()) == {"@id"}:
|
||||
referenced.add(nid)
|
||||
else:
|
||||
defined.setdefault(nid, []).append(obj)
|
||||
for v in obj.values():
|
||||
_walk_ids(v, defined, referenced)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
_walk_ids(v, defined, referenced)
|
||||
|
||||
|
||||
def layer4_consistency(node_index, parsed_docs, rules, defects):
|
||||
"""node_index: (entry, node) for every TYPED node.
|
||||
parsed_docs: (entry, parsed) for every entry that parsed — used for @id scan."""
|
||||
# ---- placeholder text (P0) ----
|
||||
tokens = [t.lower() for t in rules["placeholder_tokens"]]
|
||||
for entry, node in node_index:
|
||||
ntype = type_of(node)
|
||||
for key, val in all_strings(node):
|
||||
low = val.lower()
|
||||
hit = next((t for t in tokens if t in low), None)
|
||||
if hit:
|
||||
defects.add("P0", "L4", "PLACEHOLDER_TEXT",
|
||||
f"Placeholder/boilerplate token {hit!r} in '{key}': {val[:60]!r}.",
|
||||
entry["entry_id"], entry["url"], ntype)
|
||||
break # one placeholder defect per node is enough signal
|
||||
|
||||
# ---- NAP consistency (P0) ----
|
||||
by_name = defaultdict(list)
|
||||
for entry, node in node_index:
|
||||
if type_of(node) in NAP_TYPES and node.get("name"):
|
||||
by_name[normalize_name(first_text(node.get("name")))].append((entry, node))
|
||||
for name, group in by_name.items():
|
||||
phones = {str(first_text(n.get("telephone"))).strip()
|
||||
for _, n in group if n.get("telephone")}
|
||||
streets = {_address_street(n) for _, n in group if _address_street(n)}
|
||||
if len(phones) > 1:
|
||||
defects.add("P0", "L4", "NAP_PHONE_MISMATCH",
|
||||
f"Business '{name}' has conflicting telephone values across "
|
||||
f"entries: {sorted(phones)}.", entry_id="(dataset)")
|
||||
if len(streets) > 1:
|
||||
defects.add("P0", "L4", "NAP_ADDRESS_MISMATCH",
|
||||
f"Business '{name}' has conflicting streetAddress values across "
|
||||
f"entries: {sorted(streets)}.", entry_id="(dataset)")
|
||||
|
||||
# ---- @id duplicates + dangling references (P1) ----
|
||||
defined = {} # @id -> list of definition dicts (walked across all docs)
|
||||
referenced = set() # @id values used purely as references
|
||||
for _, parsed in parsed_docs:
|
||||
_walk_ids(parsed, defined, referenced)
|
||||
for nid, defs in defined.items():
|
||||
if len(defs) > 1:
|
||||
# duplicate only matters if the definitions actually differ
|
||||
shapes = {json.dumps(n, sort_keys=True, ensure_ascii=False) for n in defs}
|
||||
if len(shapes) > 1:
|
||||
defects.add("P1", "L4", "DUPLICATE_ID",
|
||||
f"@id {nid!r} is defined {len(defs)} times with differing content.",
|
||||
entry_id="(dataset)")
|
||||
for nid in sorted(referenced - set(defined)):
|
||||
defects.add("P1", "L4", "DANGLING_ID",
|
||||
f"@id reference {nid!r} points to a node that is never defined.",
|
||||
entry_id="(dataset)")
|
||||
|
||||
# ---- swapped / out-of-range geo (P1) ----
|
||||
g = rules["geo"]
|
||||
for entry, node in node_index:
|
||||
if type_of(node) != "GeoCoordinates":
|
||||
continue
|
||||
try:
|
||||
lat = float(first_text(node.get("latitude")))
|
||||
lon = float(first_text(node.get("longitude")))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
lat_ok = g["lat_min"] <= lat <= g["lat_max"]
|
||||
lon_ok = g["lon_min"] <= lon <= g["lon_max"]
|
||||
if lat_ok and lon_ok:
|
||||
continue # both in valid global range
|
||||
# Invalid — distinguish a clean transposition from plain garbage.
|
||||
swap_ok = (g["lat_min"] <= lon <= g["lat_max"]) and (g["lon_min"] <= lat <= g["lon_max"])
|
||||
if swap_ok:
|
||||
defects.add("P1", "L4", "GEO_SWAPPED",
|
||||
f"GeoCoordinates look transposed (latitude={lat}, longitude={lon}) "
|
||||
"— swapping them yields valid coordinates.",
|
||||
entry["entry_id"], entry["url"], "GeoCoordinates")
|
||||
else:
|
||||
defects.add("P1", "L4", "GEO_OUT_OF_RANGE",
|
||||
f"GeoCoordinates out of range (latitude={lat}, longitude={lon}).",
|
||||
entry["entry_id"], entry["url"], "GeoCoordinates")
|
||||
|
||||
# ---- duplicate descriptions across entries (P1) ----
|
||||
desc_groups = defaultdict(set)
|
||||
for entry, node in node_index:
|
||||
d = first_text(node.get("description"))
|
||||
if isinstance(d, str) and len(d.strip()) >= 30:
|
||||
desc_groups[d.strip()].add(entry["entry_id"])
|
||||
for desc, eids in desc_groups.items():
|
||||
if len(eids) >= 3:
|
||||
defects.add("P1", "L4", "DUPLICATE_DESCRIPTION",
|
||||
f"Identical description reused across {len(eids)} entries "
|
||||
f"(e.g. {sorted(eids)[:3]}): {desc[:50]!r}…", entry_id="(dataset)")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Orchestration + output
|
||||
# --------------------------------------------------------------------------- #
|
||||
def run(entries, rules, inventory, strict, no_recommended):
|
||||
defects = DefectLog()
|
||||
if inventory is not None:
|
||||
layer0_coverage(entries, inventory, defects)
|
||||
|
||||
node_index = []
|
||||
parsed_docs = []
|
||||
valid_entries = 0
|
||||
for entry in entries:
|
||||
parsed = layer1_syntax(entry, rules, defects)
|
||||
if parsed is None:
|
||||
continue
|
||||
valid_entries += 1
|
||||
parsed_docs.append((entry, parsed))
|
||||
for node in iter_typed_nodes(parsed):
|
||||
layer2_vocabulary(node, rules, defects, entry["entry_id"], entry["url"], strict)
|
||||
layer3_richresult(node, rules, defects, entry["entry_id"], entry["url"],
|
||||
no_recommended)
|
||||
node_index.append((entry, node))
|
||||
|
||||
layer4_consistency(node_index, parsed_docs, rules, defects)
|
||||
return defects, valid_entries, len(node_index)
|
||||
|
||||
|
||||
def write_outputs(defects, outdir, meta):
|
||||
outdir = Path(outdir)
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# defect_log.csv — the client-facing triage artifact
|
||||
fields = ["entry_id", "url", "node_type", "layer", "code", "severity",
|
||||
"message", "status", "owner", "note"]
|
||||
rows = sorted(defects.rows, key=lambda r: (SEVERITY_ORDER[r["severity"]],
|
||||
r["layer"], r["code"]))
|
||||
with open(outdir / "defect_log.csv", "w", newline="", encoding="utf-8-sig") as f:
|
||||
w = csv.DictWriter(f, fieldnames=fields)
|
||||
w.writeheader()
|
||||
w.writerows(rows)
|
||||
|
||||
counts = defects.counts()
|
||||
gate = "PASS" if counts["P0"] == 0 else "FAIL"
|
||||
by_code = Counter((r["severity"], r["code"]) for r in defects.rows)
|
||||
|
||||
# results.json — machine-readable
|
||||
results = {
|
||||
"summary": {**meta, **counts, "total": len(rows), "gate": gate},
|
||||
"by_code": [{"severity": s, "code": c, "count": n}
|
||||
for (s, c), n in by_code.most_common()],
|
||||
"defects": rows,
|
||||
}
|
||||
(outdir / "results.json").write_text(
|
||||
json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
# report.md — human summary
|
||||
lines = [
|
||||
"# Schema Validation Report", "",
|
||||
f"- Entries read: **{meta['entries']}** | parsed OK: **{meta['valid_entries']}** "
|
||||
f"| nodes checked: **{meta['nodes']}**",
|
||||
f"- Defects: **P0 {counts['P0']}** · **P1 {counts['P1']}** · **P2 {counts['P2']}** "
|
||||
f"(total {len(rows)})",
|
||||
"",
|
||||
f"## Gate: **{gate}**",
|
||||
("> ✅ Zero P0 — entries may advance to client review."
|
||||
if gate == "PASS" else
|
||||
"> ⛔ P0 present — these entries must NOT reach client review. Fix P0 first."),
|
||||
"",
|
||||
"## Defects by code", "",
|
||||
"| Severity | Code | Count |", "|---|---|---|",
|
||||
]
|
||||
for (sev, code), n in by_code.most_common():
|
||||
lines.append(f"| {sev} | {code} | {n} |")
|
||||
|
||||
p0 = [r for r in rows if r["severity"] == "P0"]
|
||||
if p0:
|
||||
lines += ["", "## P0 blockers (top 15)", "",
|
||||
"| Entry | Type | Code | Message |", "|---|---|---|---|"]
|
||||
for r in p0[:15]:
|
||||
msg = r["message"].replace("|", "\\|")
|
||||
lines.append(f"| {r['entry_id']} | {r['node_type']} | {r['code']} | {msg} |")
|
||||
|
||||
lines += ["", "## Next step",
|
||||
("Triage P1 in `defect_log.csv`; client reviews the clean entries against this report."
|
||||
if gate == "PASS" else
|
||||
"Assign and fix every P0, re-run the validator, and only then open client review."),
|
||||
""]
|
||||
(outdir / "report.md").write_text("\n".join(lines), encoding="utf-8")
|
||||
return gate, counts
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="5-layer offline JSON-LD schema validator.")
|
||||
ap.add_argument("dataset", nargs="?", help="xlsx/csv/jsonl/json file or a directory")
|
||||
ap.add_argument("--url-list", help="canonical URL inventory (xlsx/csv/txt) → enables Layer 0")
|
||||
ap.add_argument("--out", default="schema_qa_out", help="output directory")
|
||||
ap.add_argument("--strict", action="store_true",
|
||||
help="unexpected props on known types → P1; unknown types → P1")
|
||||
ap.add_argument("--no-recommended", action="store_true",
|
||||
help="drop L3 recommended (P2) findings — highest-signal gate")
|
||||
ap.add_argument("--live", nargs="+", metavar="URL",
|
||||
help="Mode B: validate live URLs (extract embedded JSON-LD)")
|
||||
ap.add_argument("--rules", default=str(RULES_DEFAULT), help="path to schema_rules.json")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if not args.dataset and not args.live:
|
||||
ap.error("provide a DATASET path or --live URL ...")
|
||||
|
||||
rules = json.loads(Path(args.rules).read_text(encoding="utf-8"))
|
||||
|
||||
try:
|
||||
entries = load_entries(args.dataset, args.live)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
print(f"ERROR loading input: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
inventory = load_url_inventory(args.url_list) if args.url_list else None
|
||||
|
||||
defects, valid_entries, nodes = run(entries, rules, inventory,
|
||||
args.strict, args.no_recommended)
|
||||
meta = {"entries": len(entries), "valid_entries": valid_entries, "nodes": nodes,
|
||||
"mode": "B-live" if args.live else "A-dataset", "strict": args.strict,
|
||||
"coverage": inventory is not None}
|
||||
gate, counts = write_outputs(defects, args.out, meta)
|
||||
|
||||
print(f"[{gate}] entries={len(entries)} nodes={nodes} "
|
||||
f"P0={counts['P0']} P1={counts['P1']} P2={counts['P2']} → {args.out}/")
|
||||
# Exit 1 when the gate fails so CI and `&&` chains stop on P0.
|
||||
return 0 if gate == "PASS" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: 17-seo-schema-generator
|
||||
name: seo-schema-generator
|
||||
description: |
|
||||
Generates validation-ready JSON-LD structured data for a site, covering BOTH
|
||||
scenarios: (1) from an existing website — extract facts from live pages; and
|
||||
(2) from collected sources for a not-yet-published site — reconcile conflicting
|
||||
facts into a provenance-tracked claims register. Both modes emit the same claims
|
||||
register, build pruned drafts from type templates (no placeholders shipped), and
|
||||
hand off to 16-seo-schema-validator (generate -> validate, gate = zero P0).
|
||||
hand off to 16-seo-schema-validator (generate then validate, gate = zero P0).
|
||||
Triggers: generate schema, create JSON-LD, schema markup, structured data generator,
|
||||
source-to-schema, pre-launch schema, claims register, 스키마 생성, 스키마 저작,
|
||||
구조화 데이터 생성, 미발행 사이트 스키마, 기존 사이트 스키마 추출.
|
||||
|
||||
239
custom-skills/18-seo-local-audit/SKILL.md
Normal file
239
custom-skills/18-seo-local-audit/SKILL.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
name: seo-local-audit
|
||||
description: |
|
||||
Local business SEO auditor for Korean-market businesses. Covers business identity extraction,
|
||||
NAP consistency, Google Business Profile, Naver Smart Place, Kakao Map, local citations,
|
||||
and LocalBusiness schema validation.
|
||||
Triggers: local SEO, NAP audit, Google Business Profile, GBP optimization, local citations,
|
||||
네이버 스마트플레이스, 카카오맵, 로컬 SEO.
|
||||
---
|
||||
|
||||
# SEO Local Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Audit local business SEO for Korean-market businesses: business identity extraction, NAP consistency, GBP optimization, Naver Smart Place, Kakao Map, local citations, and LocalBusiness schema markup.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Business Identity** - Extract official names, address, phone from website schema/content
|
||||
2. **NAP Consistency** - Cross-platform verification against canonical NAP
|
||||
3. **GBP Optimization** - Layered discovery + profile completeness audit
|
||||
4. **Naver Smart Place** - Layered discovery + listing completeness audit
|
||||
5. **Kakao Map** - Presence verification + NAP check
|
||||
6. **Citation Audit** - Korean-first directory presence
|
||||
7. **Schema Validation** - LocalBusiness JSON-LD markup
|
||||
|
||||
## MCP Tool Usage
|
||||
|
||||
```
|
||||
mcp__firecrawl__scrape: Extract NAP and schema from website
|
||||
mcp__perplexity__search: Find citations, GBP, Naver Place listings
|
||||
mcp__notion__create-page: Save audit findings
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 0: Business Identity (MANDATORY FIRST STEP)
|
||||
|
||||
Before any audit, establish the official business identity.
|
||||
|
||||
**Sources (in priority order):**
|
||||
1. Website schema markup (JSON-LD `Organization`, `Hospital`, `LocalBusiness`) — `name` field is authoritative
|
||||
2. Contact page / About page
|
||||
3. Footer (address, phone, social links)
|
||||
4. User-provided information
|
||||
|
||||
**Data to collect:**
|
||||
|
||||
| Field | Example |
|
||||
|-------|---------|
|
||||
| Official name (Korean) | 제이미성형외과의원 |
|
||||
| Official name (English) | Jamie Plastic Surgery Clinic |
|
||||
| Brand/display name | Jamie Clinic |
|
||||
| Website URL | https://www.jamie.clinic |
|
||||
| Address (Korean) | 서울특별시 강남구 ... |
|
||||
| Phone | 02-XXX-XXXX |
|
||||
| Known GBP URL | (if available) |
|
||||
| Known Naver Place URL | (if available) |
|
||||
| Known Kakao Map URL | (if available) |
|
||||
|
||||
Look for these URL patterns in `sameAs`, footer links, or embedded iframes:
|
||||
- GBP: `maps.app.goo.gl/*`, `google.com/maps/place/*`, `g.page/*`
|
||||
- Naver Place: `naver.me/*`, `map.naver.com/*/place/*`, `m.place.naver.com/*`
|
||||
- Kakao Map: `place.map.kakao.com/*`, `kko.to/*`
|
||||
|
||||
### Step 1: Website NAP Extraction
|
||||
|
||||
Scrape header, footer, contact page, about page. Cross-reference with schema markup. Establish the **canonical NAP** baseline.
|
||||
|
||||
### Step 2: GBP Verification & Audit
|
||||
|
||||
**Layered discovery (try in order, stop when found):**
|
||||
1. Use provided GBP URL (from Step 0 or user input)
|
||||
2. Check website for GBP link (footer, contact, schema `sameAs`, embedded Google Maps iframe)
|
||||
3. Search: `"[Korean Name]" "[City/District]" Google Maps`
|
||||
4. Search: `"[English Name]" Google Maps [City]`
|
||||
5. Search: `"[exact phone number]" site:google.com/maps`
|
||||
|
||||
**Important**: Google Maps is JS-rendered — scraping tools cannot extract business data. Use search for discovery, verify via search result snippets.
|
||||
|
||||
**If found — audit checklist (score /10):**
|
||||
- [ ] Business name matches canonical NAP
|
||||
- [ ] Address is complete and accurate
|
||||
- [ ] Phone number matches
|
||||
- [ ] Business hours are current
|
||||
- [ ] Primary + secondary categories appropriate
|
||||
- [ ] Business description complete
|
||||
- [ ] 10+ photos uploaded
|
||||
- [ ] Posts are recent (within 7 days)
|
||||
- [ ] Reviews are responded to
|
||||
- [ ] Q&A section is active
|
||||
|
||||
**If NOT found:** Report as **"not discoverable via web search"** (distinct from "does not exist").
|
||||
|
||||
### Step 3: Naver Smart Place Verification & Audit
|
||||
|
||||
**Layered discovery (try in order, stop when found):**
|
||||
1. Use provided Naver Place URL (from Step 0 or user input)
|
||||
2. Check website for Naver Place link (footer, contact, schema `sameAs`)
|
||||
3. Search: `"[Korean Name]" site:map.naver.com`
|
||||
4. Search: `"[Korean Name]" 네이버 지도 [district]`
|
||||
5. Search: `"[Korean Name]" 네이버 스마트플레이스`
|
||||
6. Search: `"[exact phone number]" site:map.naver.com`
|
||||
|
||||
**Important**: Naver Map is JS-rendered — scraping tools cannot extract data. Use search for discovery, verify via snippets.
|
||||
|
||||
**If found — audit checklist (score /10):**
|
||||
- [ ] Business name matches canonical NAP
|
||||
- [ ] Address is complete and accurate
|
||||
- [ ] Phone number matches
|
||||
- [ ] Business hours are current
|
||||
- [ ] Place is "claimed" (owner-managed / 업주 등록)
|
||||
- [ ] Keywords/tags are set
|
||||
- [ ] Booking/reservation link present
|
||||
- [ ] Recent blog reviews linked
|
||||
- [ ] Photos uploaded and current
|
||||
- [ ] Menu/service/price information present
|
||||
|
||||
**If NOT found:** Report as **"not discoverable via web search"** (not "does not exist" or "not registered").
|
||||
|
||||
### Step 4: Kakao Map Verification
|
||||
|
||||
**Discovery:**
|
||||
1. Use provided Kakao Map URL (from Step 0)
|
||||
2. Check website for Kakao Map link (`place.map.kakao.com/*`, `kko.to/*`)
|
||||
3. Search: `"[Korean Name]" site:place.map.kakao.com`
|
||||
4. Search: `"[Korean Name]" 카카오맵 [district]`
|
||||
|
||||
**If found:** Verify NAP consistency against canonical NAP.
|
||||
|
||||
### Step 5: Citation Discovery
|
||||
|
||||
**Korean market platform priorities:**
|
||||
|
||||
| Platform | Priority | Market |
|
||||
|----------|----------|--------|
|
||||
| Google Business Profile | Critical | Global |
|
||||
| Naver Smart Place (네이버 스마트플레이스) | Critical | Korea |
|
||||
| Kakao Map (카카오맵) | High | Korea |
|
||||
| Industry-specific directories | High | Varies |
|
||||
| Apple Maps | Medium | Global |
|
||||
| Bing Places | Low | Global |
|
||||
|
||||
**Korean medical/cosmetic industry directories:**
|
||||
- 강남언니 (Gangnam Unni)
|
||||
- 바비톡 (Babitalk)
|
||||
- 성예사 (Sungyesa)
|
||||
- 굿닥 (Goodoc)
|
||||
- 똑닥 (Ddocdoc)
|
||||
- 모두닥 (Modoodoc)
|
||||
- 하이닥 (HiDoc)
|
||||
|
||||
### Step 6: NAP Consistency Report
|
||||
|
||||
Cross-reference all sources against canonical NAP.
|
||||
|
||||
**Common inconsistency points:**
|
||||
- Building/landmark names — authoritative source is the **business registration certificate** (사업자등록증)
|
||||
- Phone format variations (02-XXX-XXXX vs +82-2-XXX-XXXX)
|
||||
- Address format (road-name vs lot-number / 도로명 vs 지번)
|
||||
- Korean vs English name spelling variations
|
||||
- Suite/floor number omissions
|
||||
|
||||
### Step 7: LocalBusiness Schema Validation
|
||||
|
||||
Validate JSON-LD completeness: @type, name, address, telephone, openingHours, geo (GeoCoordinates), sameAs (GBP, Naver, Kakao, social), url, image.
|
||||
|
||||
## Scoring
|
||||
|
||||
| Component | Weight | Max Score |
|
||||
|-----------|--------|-----------|
|
||||
| Business Identity completeness | 5% | /10 |
|
||||
| NAP Consistency | 20% | /10 |
|
||||
| GBP Optimization | 20% | /10 |
|
||||
| Naver Smart Place | 20% | /10 |
|
||||
| Kakao Map presence | 10% | /10 |
|
||||
| Citations (directories) | 10% | /10 |
|
||||
| LocalBusiness Schema | 15% | /10 |
|
||||
|
||||
**Overall Local SEO Score** = weighted average, normalized to /100.
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Local SEO Audit: [Business]
|
||||
|
||||
### Business Identity
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Korean Name | ... |
|
||||
| English Name | ... |
|
||||
| Address | ... |
|
||||
| Phone | ... |
|
||||
|
||||
### NAP Consistency: X/10
|
||||
| Source | Name | Address | Phone | Status |
|
||||
|--------|------|---------|-------|--------|
|
||||
| Website | OK/Issue | OK/Issue | OK/Issue | Match/Mismatch |
|
||||
| GBP | OK/Issue | OK/Issue | OK/Issue | Match/Mismatch |
|
||||
| Naver Place | OK/Issue | OK/Issue | OK/Issue | Match/Mismatch |
|
||||
| Kakao Map | OK/Issue | OK/Issue | OK/Issue | Match/Mismatch |
|
||||
|
||||
### GBP Score: X/10
|
||||
[Checklist results]
|
||||
|
||||
### Naver Smart Place: X/10
|
||||
[Checklist results]
|
||||
|
||||
### Kakao Map: X/10
|
||||
[Status + NAP check]
|
||||
|
||||
### Citations: X/10
|
||||
| Platform | Found | NAP Match |
|
||||
|----------|-------|-----------|
|
||||
| ... | | |
|
||||
|
||||
### LocalBusiness Schema: X/10
|
||||
- Present: Yes/No
|
||||
- Valid: Yes/No
|
||||
- Missing fields: [list]
|
||||
|
||||
### Overall Score: XX/100 (Grade)
|
||||
### Priority Actions
|
||||
1. [Recommendations]
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- GBP and Naver Map are JS-rendered — scraping tools cannot extract listing data. Always use search for discovery.
|
||||
- "Not discoverable via web search" != "does not exist." Always use this precise language.
|
||||
- For Korean businesses, Naver Smart Place is as important as GBP (often more so for domestic traffic).
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category (Local SEO), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: LOCAL-YYYYMMDD-NNN
|
||||
148
custom-skills/19-seo-keyword-strategy/SKILL.md
Normal file
148
custom-skills/19-seo-keyword-strategy/SKILL.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: seo-keyword-strategy
|
||||
description: |
|
||||
Keyword strategy and research for SEO campaigns.
|
||||
Triggers: keyword research, keyword analysis, keyword gap, search volume,
|
||||
keyword clustering, intent classification, 키워드 전략, 키워드 분석,
|
||||
키워드 리서치, 검색량 분석, 키워드 클러스터링.
|
||||
---
|
||||
|
||||
# SEO Keyword Strategy & Research
|
||||
|
||||
## Purpose
|
||||
|
||||
Expand seed keywords, classify search intent, cluster topics, and identify competitor keyword gaps for comprehensive keyword strategy development.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Keyword Expansion** - Matching terms, related terms, search suggestions
|
||||
2. **Korean Market** - Suffix expansion, Naver autocomplete, Korean intent patterns
|
||||
3. **Intent Classification** - Informational, navigational, commercial, transactional
|
||||
4. **Topic Clustering** - Group keywords into semantic clusters
|
||||
5. **Gap Analysis** - Find competitor keywords missing from target site
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill can pull keyword data from multiple backends. **Pick one per task** — don't fan out to every backend by default (cost + rate limits).
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Default for keyword volume, related/matching terms, organic competitor pulls | Call pattern: `keyword_research` → `get_report_schema` → `execute_report`. `database="us"` default; `"kr"` for Korean market. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Ahrefs DR/UR weighting; first-party `gsc-keywords` (only Ahrefs integrates GSC inside its MCP) | `keywords-explorer-overview`, `-matching-terms`, `-related-terms`, `-search-suggestions`, `-volume-by-country`, `gsc-keywords`. |
|
||||
| **OurSEO Agent CLI** (`our keywords *`) | DataForSEO under the hood — cheapest per call, batch-friendly, Korean-aware via `--location 2410` | Claude Code only (needs Bash). Wrap calls: `our keywords volume`, `ideas`, `for-site`, `intent`, `difficulty`. |
|
||||
| **OurSEO Agent MCP** (`mcp__ourseo__*`) | Claude Desktop equivalent for crawl-derived keywords + Knowledge Graph entity expansion | `search_knowledge_graph` for entity seeding; `crawl_website` to extract on-page keyword inventory from the target site itself. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Direct fallback when `our` CLI isn't available | Same data as `our keywords *`. |
|
||||
| **GSC** (via `our research search-console` or Ahrefs `gsc-*`) | First-party queries the site actually ranks for — ground truth, not estimates | Use to validate/prune Semrush or Ahrefs lists with real impressions/CTR. |
|
||||
|
||||
### How to pick
|
||||
|
||||
Apply these in order; stop at the first match:
|
||||
|
||||
1. **User named a backend explicitly** in the prompt → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default there.
|
||||
3. **Task needs a capability only one backend has** (e.g., `gsc-keywords` first-party data, or `mcp__ourseo__search_knowledge_graph` entity expansion) → use that backend.
|
||||
4. **Default by market**:
|
||||
- English-market or unspecified → **Semrush MCP** with `database="us"`.
|
||||
- Korean market → **OurSEO CLI** `our keywords <subcmd> --location 2410 --language ko` (Claude Code), or **Semrush MCP** with `database="kr"` (Claude Desktop).
|
||||
5. **Still ambiguous on a non-trivial task** → ask once via `AskUserQuestion` listing the top 2–3 candidates.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Semrush MCP (default):**
|
||||
```
|
||||
mcp__semrush__keyword_research(query="<seed>", database="us")
|
||||
mcp__semrush__get_report_schema(report_id="...")
|
||||
mcp__semrush__execute_report(report_id="...", params={...})
|
||||
```
|
||||
|
||||
**OurSEO CLI (Korean default, Claude Code):**
|
||||
```bash
|
||||
our keywords volume "<keyword>" --location 2410 --language ko
|
||||
our keywords ideas "<keyword>" --location 2410 --limit 50
|
||||
our keywords for-site <competitor.com> --location 2410 --limit 100
|
||||
our keywords intent "<kw1>" "<kw2>" "<kw3>"
|
||||
our keywords difficulty "<kw1>" "<kw2>"
|
||||
```
|
||||
|
||||
**Ahrefs MCP (when user requests, or for GSC first-party):**
|
||||
```
|
||||
mcp__ahrefs__keywords-explorer-overview(keyword="<seed>", country="us")
|
||||
mcp__ahrefs__keywords-explorer-matching-terms(keyword="<seed>", country="us")
|
||||
mcp__ahrefs__keywords-explorer-volume-by-country(keyword="<seed>")
|
||||
mcp__ahrefs__gsc-keywords(...)
|
||||
```
|
||||
|
||||
**OurSEO Agent MCP (Claude Desktop, KG/entity expansion):**
|
||||
```
|
||||
mcp__ourseo__search_knowledge_graph(query="<brand or entity>")
|
||||
mcp__ourseo__crawl_website(url="<target>", max_pages=50)
|
||||
```
|
||||
|
||||
### Common parameters across backends
|
||||
|
||||
| Concept | Semrush | Ahrefs | DataForSEO / `our` CLI |
|
||||
|---|---|---|---|
|
||||
| Korean market | `database="kr"` | `country="kr"` | `--location 2410` |
|
||||
| US market | `database="us"` | `country="us"` | `--location 2840` |
|
||||
| Japan | `database="jp"` | `country="jp"` | `--location 2392` |
|
||||
| Language | (database-bound) | (country-bound) | `--language ko`/`en`/`ja` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Seed Keyword Expansion
|
||||
1. Determine backend via **Data Source Selection** above.
|
||||
2. Fetch search volume for the seed.
|
||||
3. Expand via the chosen backend's "related" / "ideas" / "matching-terms" endpoint.
|
||||
4. Apply Korean suffix expansion if Korean market (regardless of backend).
|
||||
5. Deduplicate and merge.
|
||||
|
||||
### 2. Intent Classification & Clustering
|
||||
1. Classify each keyword by search intent (informational / navigational / commercial / transactional).
|
||||
2. Group keywords into topic clusters.
|
||||
3. Identify pillar topics and supporting terms.
|
||||
4. Calculate cluster-level metrics (total volume, avg KD).
|
||||
|
||||
### 3. Gap Analysis
|
||||
1. Pull organic keywords for target via chosen backend.
|
||||
2. Pull organic keywords for competitors (parallel).
|
||||
3. Identify keywords present in competitors but missing from target.
|
||||
4. Score opportunities by volume/difficulty ratio.
|
||||
5. Prioritize by intent alignment with business goals.
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Keyword Strategy Report: [seed keyword]
|
||||
|
||||
### Overview
|
||||
- Data source: [Semrush | Ahrefs | OurSEO CLI | OurSEO MCP | GSC]
|
||||
- Market: [database/location code]
|
||||
- Total keywords discovered: [count]
|
||||
- Topic clusters: [count]
|
||||
- Total search volume: [sum]
|
||||
|
||||
### Top Clusters
|
||||
| Cluster | Keywords | Total Volume | Avg KD |
|
||||
|---|---|---|---|
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
### Top Opportunities
|
||||
| Keyword | Volume | KD | Intent | Cluster |
|
||||
|---|---|---|---|---|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
### Keyword Gaps (vs competitors)
|
||||
| Keyword | Volume | Competitor Position | Opportunity Score |
|
||||
|---|---|---|---|
|
||||
| ... | ... | ... | ... |
|
||||
```
|
||||
|
||||
Always record the chosen data source in the **Overview** so future audits can compare apples to apples.
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: KW-YYYYMMDD-NNN
|
||||
165
custom-skills/20-seo-serp-analysis/SKILL.md
Normal file
165
custom-skills/20-seo-serp-analysis/SKILL.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: seo-serp-analysis
|
||||
description: |
|
||||
SERP analysis for Google and Naver search results.
|
||||
Triggers: SERP analysis, search results, featured snippet, SERP features, Naver SERP, 검색결과 분석, SERP 분석.
|
||||
---
|
||||
|
||||
# SEO SERP Analysis
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze search engine result page composition for Google and Naver. Detect SERP features (featured snippets, PAA, knowledge panels, local pack, video, ads), map competitor positions, score SERP feature opportunities, and analyze Naver section distribution.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Google SERP Feature Detection** - Identify featured snippets, PAA, knowledge panels, local pack, video carousel, ads, image pack, site links, shopping
|
||||
2. **Competitor Position Mapping** - Extract domains, positions, content types for top organic results
|
||||
3. **Opportunity Scoring** - Score SERP opportunity (0-100) based on feature landscape and competition
|
||||
4. **Search Intent Validation** - Infer intent (informational, navigational, commercial, transactional, local) from SERP composition
|
||||
5. **Naver SERP Composition** - Detect sections (blog, cafe, knowledge iN, Smart Store, brand zone, books, shortform, influencer), map section priority, analyze brand zone presence
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill can pull SERP data from multiple backends. **Pick one per task** — don't fan out by default (cost + rate limits).
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Default Google SERP overview, organic competitor positions, SERP-feature presence | `overview_research` / `organic_research` → `get_report_schema` → `execute_report`. `database="us"` default; `"kr"` for Korean. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | When user wants Ahrefs SERP overview or already has an Ahrefs project | `serp-overview` exposes top organic, SERP features, paid layout per keyword. |
|
||||
| **OurSEO MCP** (`mcp__ourseo__check_serp`) | Live position spot-check for a single keyword/domain pair | Cheap; good for rank-only confirmations without full SERP pull. |
|
||||
| **OurSEO CLI** (`our serp *`) | DataForSEO under the hood — full SERP JSON with all features, Korean-aware via `--location 2410` | Claude Code only (Bash). Commands: `our serp live`, `our serp competitors`, `our serp ranked-keywords`, `our serp domain-overview`. |
|
||||
| **OurSEO CLI — Naver** (`our research naver serp`) | Naver SERP composition (blog, cafe, knowledge iN, Smart Store, brand zone, shortform, influencer) | Naver-only; required for Korean-market analysis since Semrush/Ahrefs don't cover Naver SERP. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Fallback when `our` CLI isn't running | `serp_organic_live_advanced`, `dataforseo_labs_google_serp_competitors`. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task needs a capability only one backend has** (Naver SERP → `our research naver serp`; full SERP JSON → DataForSEO / OurSEO CLI) → use that backend.
|
||||
4. **Default**: Semrush MCP for Google SERP overview; **`our research naver serp`** for Naver.
|
||||
5. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Semrush MCP (default Google):**
|
||||
```
|
||||
mcp__semrush__overview_research(query="<keyword>", database="us")
|
||||
mcp__semrush__get_report_schema(report_id="...")
|
||||
mcp__semrush__execute_report(report_id="...", params={...})
|
||||
```
|
||||
|
||||
**OurSEO CLI — DataForSEO (full Google SERP JSON):**
|
||||
```bash
|
||||
our serp live "<keyword>" --location 2410 --language ko
|
||||
our serp competitors <domain> --location 2410
|
||||
our serp ranked-keywords <domain> --location 2410 --limit 50
|
||||
our serp domain-overview <domain> --location 2410
|
||||
```
|
||||
|
||||
**OurSEO CLI — Naver SERP (Korean market):**
|
||||
```bash
|
||||
our research naver serp "<keyword>"
|
||||
our research naver serp "<keyword>" --domain <target.com>
|
||||
```
|
||||
|
||||
**OurSEO MCP (single-keyword spot-check):**
|
||||
```
|
||||
mcp__ourseo__check_serp(keyword="<keyword>", domain="<target.com>", country="kr")
|
||||
```
|
||||
|
||||
**Ahrefs MCP:**
|
||||
```
|
||||
mcp__ahrefs__serp-overview(keyword="<keyword>", country="us")
|
||||
```
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Concept | Semrush | Ahrefs | DataForSEO / `our` CLI |
|
||||
|---|---|---|---|
|
||||
| Korean market | `database="kr"` | `country="kr"` | `--location 2410` |
|
||||
| US market | `database="us"` | `country="us"` | `--location 2840` |
|
||||
| Japan | `database="jp"` | `country="jp"` | `--location 2392` |
|
||||
| Language | (database-bound) | (country-bound) | `--language ko`/`en`/`ja` |
|
||||
|
||||
Always record the chosen data source in the report **Overview** so future analyses can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Google SERP Analysis
|
||||
1. Fetch SERP via `our serp live "<keyword>" --location 2410 --language ko --format json`
|
||||
2. Parse SERP features from response (featured_snippet, people_also_ask, local_pack, etc.)
|
||||
3. Map competitor positions from organic_results (domain, URL, title, position)
|
||||
4. Classify content type for each result (blog, product, service, news, video)
|
||||
5. Calculate opportunity score (0-100) based on feature landscape
|
||||
6. Validate search intent from SERP composition
|
||||
7. Get competitor domain overview via `our serp domain-overview <competitor> --location 2410`
|
||||
|
||||
### 2. Naver SERP Analysis
|
||||
1. Fetch Naver search page for the target keyword
|
||||
2. Detect SERP sections (blog, cafe, knowledge iN, Smart Store, brand zone, news, encyclopedia, books, shortform, influencer)
|
||||
3. Map section priority (above-fold order)
|
||||
4. Check brand zone presence and extract brand name
|
||||
5. Count items per section
|
||||
6. Identify dominant content section
|
||||
|
||||
### 3. Report Generation
|
||||
1. Compile results into structured JSON
|
||||
2. Generate Korean-language report
|
||||
3. Save to Notion SEO Audit Log database
|
||||
|
||||
## Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"keyword": "치과 임플란트",
|
||||
"country": "kr",
|
||||
"serp_features": {
|
||||
"featured_snippet": true,
|
||||
"people_also_ask": true,
|
||||
"local_pack": true,
|
||||
"knowledge_panel": false,
|
||||
"video_carousel": false,
|
||||
"ads_top": 3,
|
||||
"ads_bottom": 2
|
||||
},
|
||||
"competitors": [
|
||||
{
|
||||
"position": 1,
|
||||
"url": "https://example.com/page",
|
||||
"domain": "example.com",
|
||||
"title": "...",
|
||||
"content_type": "service_page"
|
||||
}
|
||||
],
|
||||
"opportunity_score": 72,
|
||||
"intent_signals": "commercial",
|
||||
"timestamp": "2025-01-01T00:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Common SERP Features
|
||||
|
||||
| Feature | Impact | Opportunity |
|
||||
|---------|--------|-------------|
|
||||
| Featured Snippet | High visibility above organic | Optimize content format for snippet capture |
|
||||
| People Also Ask | Related question visibility | Create FAQ content targeting PAA |
|
||||
| Local Pack | Dominates local intent SERPs | Optimize Google Business Profile |
|
||||
| Knowledge Panel | Reduces organic CTR | Focus on brand queries and schema |
|
||||
| Video Carousel | Visual SERP real estate | Create video content for keyword |
|
||||
| Shopping | Transactional intent signal | Product feed optimization |
|
||||
|
||||
## Limitations
|
||||
|
||||
- SERP data may have a delay depending on data source (not real-time)
|
||||
- Naver SERP HTML structure changes periodically
|
||||
- Brand zone detection depends on HTML class patterns
|
||||
- Cannot detect personalized SERP results
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: SERP-YYYYMMDD-NNN
|
||||
157
custom-skills/21-seo-position-tracking/SKILL.md
Normal file
157
custom-skills/21-seo-position-tracking/SKILL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: seo-position-tracking
|
||||
description: |
|
||||
Keyword position tracking for keyword ranking monitoring.
|
||||
Triggers: rank tracking, position monitoring, keyword rankings, visibility score, ranking report, 키워드 순위, 순위 추적.
|
||||
---
|
||||
|
||||
# SEO Position Tracking
|
||||
|
||||
## Purpose
|
||||
|
||||
Monitor keyword ranking positions, detect significant changes, calculate visibility scores, and compare against competitors using our-seo-agent CLI or pre-fetched ranking data. Provides actionable alerts for ranking drops and segment-level performance breakdown.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Position Monitoring** - Retrieve current keyword ranking positions from our-seo-agent CLI or pre-fetched data
|
||||
2. **Change Detection** - Detect significant position changes with configurable threshold alerts (severity: critical/high/medium/low)
|
||||
3. **Visibility Scoring** - Calculate weighted visibility scores using CTR-curve model (position 1 = 30%, position 2 = 15%, etc.)
|
||||
4. **Brand/Non-brand Segmentation** - Automatically classify keywords by brand relevance and search intent type
|
||||
5. **Competitor Comparison** - Compare keyword overlap, position gaps, and visibility scores against competitors
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill can pull rank data from multiple backends. **Pick one per task** — don't fan out by default (cost + rate limits).
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Default when an Ahrefs Rank Tracker project exists for the domain | `rank-tracker-overview`, `rank-tracker-serp-overview`, `rank-tracker-competitors-*`. Best historical view; data is what Ahrefs already polled. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Default when no Ahrefs project; English/major market position scans | `tracking_research`, `organic_research`. `database="us"` default; `"kr"` for Korean. |
|
||||
| **OurSEO CLI** (`our serp *`) | DataForSEO under the hood — full ranked-keywords pulls with volume, Korean-aware via `--location 2410` | Claude Code only (Bash). Commands: `our serp ranked-keywords`, `our serp domain-overview`, `our keywords volume`. |
|
||||
| **OurSEO MCP** (`mcp__ourseo__check_serp`) | One-off rank spot-check for a single keyword/domain pair | Cheap; no historical view — pair with prior runs in MySQL / SQLite if tracking over time. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Fallback when `our` CLI isn't running; historical rank overview | `dataforseo_labs_google_historical_rank_overview`, `dataforseo_labs_google_ranked_keywords`. |
|
||||
| **GSC** (via `our research search-console` or Ahrefs `gsc-*`) | **First-party position data** — what Google actually rendered for the verified site | Only first-party source — use to validate or replace estimated positions. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Site is verified in GSC** AND task is single-site tracking → prefer **GSC** for ground truth, supplement with Semrush/Ahrefs for competitor delta.
|
||||
4. **Ahrefs project exists for the domain** → prefer Ahrefs `rank-tracker-*`.
|
||||
5. **Default**: Semrush MCP for new tracking jobs; **`our serp ranked-keywords`** for Korean batch.
|
||||
6. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Ahrefs MCP (when project exists):**
|
||||
```
|
||||
mcp__ahrefs__rank-tracker-overview(project_id="<id>")
|
||||
mcp__ahrefs__rank-tracker-serp-overview(project_id="<id>")
|
||||
mcp__ahrefs__rank-tracker-competitors-overview(project_id="<id>")
|
||||
mcp__ahrefs__rank-tracker-competitors-stats(project_id="<id>")
|
||||
```
|
||||
|
||||
**Semrush MCP (no Ahrefs project):**
|
||||
```
|
||||
mcp__semrush__tracking_research(query="<keyword>", database="us")
|
||||
mcp__semrush__get_report_schema(report_id="...")
|
||||
mcp__semrush__execute_report(report_id="...", params={...})
|
||||
```
|
||||
|
||||
**OurSEO CLI (Korean batch):**
|
||||
```bash
|
||||
our serp ranked-keywords <domain> --location 2410 --limit 100 --format json
|
||||
our serp domain-overview <domain> --location 2410 --format json
|
||||
our keywords volume "<kw1>" "<kw2>" --location 2410 --language ko
|
||||
our serp competitors <domain> --location 2410
|
||||
```
|
||||
|
||||
**OurSEO MCP (spot-check):**
|
||||
```
|
||||
mcp__ourseo__check_serp(keyword="<keyword>", domain="<target.com>", country="kr")
|
||||
```
|
||||
|
||||
**GSC (first-party validation):**
|
||||
```bash
|
||||
our research search-console queries --site sc-domain:<domain> --days 28
|
||||
```
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Concept | Semrush | Ahrefs | DataForSEO / `our` CLI |
|
||||
|---|---|---|---|
|
||||
| Korean market | `database="kr"` | `country="kr"` | `--location 2410` |
|
||||
| US market | `database="us"` | `country="us"` | `--location 2840` |
|
||||
| Japan | `database="jp"` | `country="jp"` | `--location 2392` |
|
||||
| Language | (database-bound) | (country-bound) | `--language ko`/`en`/`ja` |
|
||||
|
||||
Always record the chosen data source in the report **Overview** so future tracking runs can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Data Collection
|
||||
1. Fetch current ranked keywords: `our serp ranked-keywords <domain> --location 2410 --limit 100 --format json`
|
||||
2. Get domain overview: `our serp domain-overview <domain> --location 2410 --format json`
|
||||
3. Get search volumes for tracked keywords: `our keywords volume "<kw1>" "<kw2>" --location 2410`
|
||||
4. Fetch competitor positions: `our serp ranked-keywords <competitor> --location 2410 --limit 100`
|
||||
5. For historical comparison, use MCP: `mcp__dfs-mcp__dataforseo_labs_google_historical_rank_overview`
|
||||
|
||||
### Phase 2: Analysis
|
||||
1. Detect position changes against previous period
|
||||
2. Generate alerts for changes exceeding threshold
|
||||
3. Calculate visibility score weighted by search volume and CTR curve
|
||||
4. Segment keywords into brand/non-brand and by intent type
|
||||
5. Compare positions against each competitor
|
||||
|
||||
### Phase 3: Reporting
|
||||
1. Compile position distribution (top3/top10/top20/top50/top100)
|
||||
2. Summarize changes (improved/declined/stable/new/lost)
|
||||
3. List alerts sorted by severity and search volume
|
||||
4. Generate segment-level breakdown
|
||||
5. Save report to Notion SEO Audit Log database
|
||||
|
||||
## Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"target": "https://example.com",
|
||||
"total_keywords": 250,
|
||||
"visibility_score": 68.5,
|
||||
"positions": {
|
||||
"top3": 15,
|
||||
"top10": 48,
|
||||
"top20": 92,
|
||||
"top50": 180,
|
||||
"top100": 230
|
||||
},
|
||||
"changes": {
|
||||
"improved": 45,
|
||||
"declined": 30,
|
||||
"stable": 155,
|
||||
"new": 12,
|
||||
"lost": 8
|
||||
},
|
||||
"alerts": [
|
||||
{
|
||||
"keyword": "example keyword",
|
||||
"old_position": 5,
|
||||
"new_position": 15,
|
||||
"change": -10,
|
||||
"volume": 5400,
|
||||
"severity": "high"
|
||||
}
|
||||
],
|
||||
"segments": {
|
||||
"brand": {"keywords": 30, "avg_position": 2.1},
|
||||
"non_brand": {"keywords": 220, "avg_position": 24.5}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All tracking reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category (Position Tracking), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: RANK-YYYYMMDD-NNN
|
||||
147
custom-skills/22-seo-link-building/SKILL.md
Normal file
147
custom-skills/22-seo-link-building/SKILL.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: seo-link-building
|
||||
description: |
|
||||
Link building diagnosis and backlink analysis tool.
|
||||
Triggers: backlink audit, link building, referring domains, toxic links, link gap, broken backlinks, 백링크 분석, 링크빌딩.
|
||||
---
|
||||
|
||||
# SEO Link Building Diagnosis
|
||||
|
||||
## Purpose
|
||||
|
||||
Analyze backlink profiles, detect toxic links, find competitor link gaps, track link velocity, and map Korean platform links. Provides actionable link building recommendations.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Backlink Profile Audit** - DR, referring domains, dofollow ratio, anchor distribution
|
||||
2. **Toxic Link Detection** - PBN patterns, spam domains, link farm identification
|
||||
3. **Competitor Link Gap Analysis** - Domains linking to competitors but not target
|
||||
4. **Link Velocity Tracking** - New/lost referring domains over time
|
||||
5. **Broken Backlink Recovery** - Find and reclaim broken high-DR backlinks
|
||||
6. **Korean Platform Mapping** - Naver Blog, Cafe, Tistory, Brunch, Korean news
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill can pull backlink data from multiple backends. **Pick one per task** — don't fan out by default (cost + rate limits). Backlink graphs are the most cost-sensitive of all SEO data.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | **Default** — Ahrefs' backlink graph remains the strongest in the industry. All `site-explorer-*` endpoints. | Primary capability: `site-explorer-domain-rating`, `-backlinks-stats`, `-referring-domains`, `-anchors`, `-broken-backlinks`, `-refdomains-history`. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Alternative when user is already in Semrush context or wants a second opinion | `backlink_research` covers similar ground; Authority Score replaces DR. Coverage is competitive but not identical to Ahrefs. |
|
||||
| **OurSEO** | **Not a backlink source.** No backlink graph in the OurSEO stack. | If asked, use OurSEO MCP / CLI only as a supplement for on-page link inventory (`crawl_website` extracts internal/outbound links from the target site itself). |
|
||||
| **WebSearch / WebFetch** | Manual spot-check of a specific referring URL | Useful for verifying anchor / context of a high-value referring page. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task is comparison across vendors** (e.g., "verify Ahrefs claims with Semrush") → run both, document the source per metric.
|
||||
4. **Default**: **Ahrefs MCP** — backlink work is the one SEO task where Ahrefs is the default, not Semrush.
|
||||
5. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Ahrefs MCP (default):**
|
||||
```
|
||||
mcp__ahrefs__site-explorer-domain-rating(target="<domain>")
|
||||
mcp__ahrefs__site-explorer-backlinks-stats(target="<domain>")
|
||||
mcp__ahrefs__site-explorer-referring-domains(target="<domain>", limit=100)
|
||||
mcp__ahrefs__site-explorer-anchors(target="<domain>")
|
||||
mcp__ahrefs__site-explorer-broken-backlinks(target="<domain>", limit=100)
|
||||
mcp__ahrefs__site-explorer-refdomains-history(target="<domain>", history="weekly")
|
||||
mcp__ahrefs__site-explorer-all-backlinks(target="<domain>", limit=500)
|
||||
mcp__ahrefs__site-explorer-linked-anchors-external(target="<competitor>")
|
||||
```
|
||||
|
||||
**Semrush MCP (alternative):**
|
||||
```
|
||||
mcp__semrush__backlink_research(query="<domain>", database="us")
|
||||
mcp__semrush__get_report_schema(report_id="...")
|
||||
mcp__semrush__execute_report(report_id="...", params={...})
|
||||
```
|
||||
|
||||
**OurSEO (on-page link inventory, supplement only):**
|
||||
```
|
||||
mcp__ourseo__crawl_website(url="<domain>", max_pages=100)
|
||||
# Extract internal/outbound link inventory from crawl output — NOT a backlink graph.
|
||||
```
|
||||
|
||||
### Korean platform mapping
|
||||
|
||||
Backlinks from Korean platforms (Naver Blog, Cafe, Tistory, Brunch, Korean news) are often underrepresented in both Ahrefs and Semrush. After pulling from the chosen backend, supplement with `WebSearch` for major Korean directories and brand-name queries on Naver to spot-check coverage gaps.
|
||||
|
||||
Always record the chosen data source in the report **Overview** so future audits can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Backlink Profile Audit
|
||||
1. Fetch Domain Rating via `site-explorer-domain-rating`
|
||||
2. Get backlink stats via `site-explorer-backlinks-stats`
|
||||
3. Retrieve referring domains via `site-explorer-referring-domains`
|
||||
4. Analyze anchor distribution via `site-explorer-anchors`
|
||||
5. Detect toxic links (PBN patterns, spam keywords, suspicious TLDs)
|
||||
6. Map Korean platform links from referring domains
|
||||
7. Report with issues and recommendations
|
||||
|
||||
### 2. Link Gap Analysis
|
||||
1. Fetch target referring domains
|
||||
2. Fetch competitor referring domains (parallel)
|
||||
3. Compute set difference (competitor - target)
|
||||
4. Score opportunities by DR, traffic, category
|
||||
5. Categorize sources (news, blog, forum, directory, Korean platform)
|
||||
6. Rank by feasibility and impact
|
||||
7. Report top opportunities with recommendations
|
||||
|
||||
### 3. Link Velocity Check
|
||||
1. Fetch refdomains-history for last 90 days
|
||||
2. Calculate new/lost referring domains per period
|
||||
3. Determine velocity trend (growing/stable/declining)
|
||||
4. Flag declining velocity as issue
|
||||
|
||||
### 4. Broken Backlink Recovery
|
||||
1. Fetch broken backlinks via `site-explorer-broken-backlinks`
|
||||
2. Sort by DR (highest value first)
|
||||
3. Recommend 301 redirects or content recreation
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Link Building Audit: [domain]
|
||||
|
||||
### Overview
|
||||
- Domain Rating: [DR]
|
||||
- Referring Domains: [count]
|
||||
- Dofollow Ratio: [ratio]
|
||||
- Toxic Links: [count] ([risk level])
|
||||
|
||||
### Anchor Distribution
|
||||
| Type | Count | % |
|
||||
|------|-------|---|
|
||||
| Branded | [n] | [%] |
|
||||
| Exact Match | [n] | [%] |
|
||||
| Generic | [n] | [%] |
|
||||
| Naked URL | [n] | [%] |
|
||||
|
||||
### Toxic Links (Top 10)
|
||||
| Domain | Risk Score | Reason |
|
||||
|--------|-----------|--------|
|
||||
|
||||
### Korean Platform Links
|
||||
| Platform | Count |
|
||||
|----------|-------|
|
||||
|
||||
### Link Velocity
|
||||
| Period | New | Lost |
|
||||
|--------|-----|------|
|
||||
|
||||
### Recommendations
|
||||
1. [Priority actions]
|
||||
```
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category (Link Building), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: LINK-YYYYMMDD-NNN
|
||||
181
custom-skills/23-seo-content-strategy/SKILL.md
Normal file
181
custom-skills/23-seo-content-strategy/SKILL.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: seo-content-strategy
|
||||
description: |
|
||||
Content strategy and planning for SEO. Triggers: content audit, content strategy, content gap, topic clusters, content brief, editorial calendar, content decay, 콘텐츠 전략, 콘텐츠 감사.
|
||||
---
|
||||
|
||||
# SEO Content Strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
Audit existing content performance, identify topic gaps vs competitors, map topic clusters, detect content decay, and generate SEO content briefs. Supports Korean content patterns (Naver Blog format, 후기/review content, 추천 listicles).
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Content Audit** - Inventory, performance scoring, decay detection
|
||||
2. **Content Gap Analysis** - Topic gaps vs competitors, cluster mapping
|
||||
3. **Content Brief Generation** - Outlines, keywords, word count targets
|
||||
4. **Editorial Calendar** - Prioritized content creation schedule
|
||||
5. **Korean Content Patterns** - Naver Blog style, 후기, 추천 format analysis
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill can pull content + keyword + traffic data from multiple backends. **Pick one backend per data type per task** — content strategy spans multiple data classes, so you'll often combine 2 backends (e.g., one for keyword discovery + one for on-page inventory).
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | **Default** for organic content discovery, keyword expansion, top pages by traffic | `organic_research`, `keyword_research`, `overview_research` → `get_report_schema` → `execute_report`. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Top-pages-by-traffic for competitor sites, content-decay candidates, Korean platform refdomains | `site-explorer-top-pages`, `site-explorer-pages-history`, `keywords-explorer-*`, `gsc-pages` (first-party). |
|
||||
| **OurSEO MCP** (`mcp__ourseo__*`) | **On-page content inventory** — what the target site itself publishes | `crawl_website` for content URLs + titles + H1s; `find_similar_pages` for topic clustering. Pair with a keyword backend for performance data. |
|
||||
| **OurSEO CLI** (`our keywords *`, `our serp *`) | DataForSEO under the hood for Korean batch keyword + competitor pulls | Claude Code only (Bash). Best for `--location 2410` Korean content gap work. |
|
||||
| **GSC** (via `our research search-console` or Ahrefs `gsc-*`) | **Cannibalization detection** — query × page where multiple URLs split impressions; first-party content performance | Only first-party source — required for accurate decay detection on the target site. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Fallback when `our` CLI isn't running | Same data as `our keywords *` / `our serp *`. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task is content decay / cannibalization on the target site** → use **GSC** (first-party impression data is required).
|
||||
4. **Task is on-page content inventory** → use **OurSEO crawl_website**.
|
||||
5. **Default for keyword + competitor pulls**: Semrush MCP (English markets); OurSEO CLI (`--location 2410`) for Korean markets.
|
||||
6. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Semrush MCP (default keyword/content discovery):**
|
||||
```
|
||||
mcp__semrush__organic_research(query="<domain>", database="us")
|
||||
mcp__semrush__keyword_research(query="<seed>", database="us")
|
||||
mcp__semrush__get_report_schema(report_id="...")
|
||||
mcp__semrush__execute_report(report_id="...", params={...})
|
||||
```
|
||||
|
||||
**Ahrefs MCP (top pages by traffic, decay candidates):**
|
||||
```
|
||||
mcp__ahrefs__site-explorer-top-pages(target="<domain>", country="us", limit=100)
|
||||
mcp__ahrefs__site-explorer-pages-history(target="<domain>", history="monthly")
|
||||
mcp__ahrefs__keywords-explorer-overview(keyword="<seed>", country="us")
|
||||
```
|
||||
|
||||
**OurSEO MCP (on-page content inventory):**
|
||||
```
|
||||
mcp__ourseo__crawl_website(url="<target>", max_pages=200)
|
||||
mcp__ourseo__find_similar_pages(crawl_path="<path/to/crawl.json>", query="<topic>")
|
||||
```
|
||||
|
||||
**OurSEO CLI (Korean batch):**
|
||||
```bash
|
||||
our keywords ideas "<seed>" --location 2410 --limit 50
|
||||
our keywords for-site <competitor.com> --location 2410 --limit 100
|
||||
our serp ranked-keywords <domain> --location 2410 --limit 100
|
||||
```
|
||||
|
||||
**GSC (cannibalization + decay):**
|
||||
```bash
|
||||
our research search-console combined --site sc-domain:<domain> --days 90
|
||||
# Group by query; flag queries where multiple pages share impressions.
|
||||
```
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Concept | Semrush | Ahrefs | DataForSEO / `our` CLI |
|
||||
|---|---|---|---|
|
||||
| Korean market | `database="kr"` | `country="kr"` | `--location 2410` |
|
||||
| US market | `database="us"` | `country="us"` | `--location 2840` |
|
||||
| Japan | `database="jp"` | `country="jp"` | `--location 2392` |
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future audits can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Content Audit
|
||||
1. Crawl sitemap to discover all content URLs
|
||||
2. Fetch top pages data via our-seo-agent CLI, pre-fetched JSON, or WebSearch
|
||||
3. Classify content types (blog, product, service, landing, resource)
|
||||
4. Score each page performance (0-100 composite)
|
||||
5. Detect decaying content (traffic decline patterns)
|
||||
6. Analyze freshness distribution (fresh/aging/stale)
|
||||
7. Identify Korean content patterns (후기, 추천, 방법 formats)
|
||||
8. Generate recommendations
|
||||
|
||||
### 2. Content Gap Analysis
|
||||
1. Gather target site keywords via our-seo-agent CLI or pre-fetched data
|
||||
2. Gather competitor top pages and keywords
|
||||
3. Identify topics present in competitors but missing from target
|
||||
4. Score gaps by priority (traffic potential + competition coverage)
|
||||
5. Build topic clusters using TF-IDF + hierarchical clustering
|
||||
6. Generate editorial calendar with priority and dates
|
||||
7. Detect Korean market content opportunities
|
||||
|
||||
### 3. Content Brief Generation
|
||||
1. Analyze top 5-10 ranking pages for target keyword
|
||||
2. Extract headings, word counts, content features (FAQ, images, video)
|
||||
3. Build recommended H2/H3 outline from competitor patterns
|
||||
4. Suggest primary, secondary, and LSI keywords
|
||||
5. Calculate target word count (avg of top 5 +/- 20%)
|
||||
6. Find internal linking opportunities on the target site
|
||||
7. Detect search intent (informational, commercial, transactional, navigational)
|
||||
8. Add Korean format recommendations based on intent
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Content Audit: [domain]
|
||||
|
||||
### Content Inventory
|
||||
- Total pages: [count]
|
||||
- By type: blog [n], product [n], service [n], other [n]
|
||||
- Average performance score: [score]/100
|
||||
|
||||
### Top Performers
|
||||
1. [score] [url] (traffic: [n])
|
||||
...
|
||||
|
||||
### Decaying Content
|
||||
1. [decay rate] [url] (traffic: [n])
|
||||
...
|
||||
|
||||
### Content Gaps vs Competitors
|
||||
1. [priority] [topic] (est. traffic: [n], difficulty: [level])
|
||||
...
|
||||
|
||||
### Topic Clusters
|
||||
1. **[Pillar Topic]** ([n] subtopics)
|
||||
- [subtopic 1]
|
||||
- [subtopic 2]
|
||||
|
||||
### Editorial Calendar
|
||||
- [date] [topic] ([type], [word count], priority: [level])
|
||||
...
|
||||
|
||||
### Recommendations
|
||||
1. [Priority actions]
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
| Issue | Impact | Fix |
|
||||
|-------|--------|-----|
|
||||
| No blog content | High | Build blog content strategy with topic clusters |
|
||||
| Content decay (traffic loss) | High | Refresh and update declining pages |
|
||||
| Missing competitor topics | Medium | Create content for high-priority gaps |
|
||||
| No 후기/review content | Medium | Add Korean review-style content for conversions |
|
||||
| Stale content (>12 months) | Medium | Update or consolidate outdated pages |
|
||||
| No topic clusters | Medium | Organize content into pillar/cluster structure |
|
||||
| Missing FAQ sections | Low | Add FAQ schema for featured snippet opportunities |
|
||||
|
||||
## Limitations
|
||||
|
||||
- our-seo-agent CLI or pre-fetched JSON required for traffic and keyword data
|
||||
- Competitor analysis limited to publicly available content
|
||||
- Content decay detection uses heuristic without historical data in standalone mode
|
||||
- Topic clustering requires minimum 3 topics per cluster
|
||||
- Word count analysis requires accessible competitor pages (no JS rendering)
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: CONTENT-YYYYMMDD-NNN
|
||||
206
custom-skills/24-seo-ecommerce/SKILL.md
Normal file
206
custom-skills/24-seo-ecommerce/SKILL.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: seo-ecommerce
|
||||
description: |
|
||||
E-commerce SEO audit and optimization for product pages, product schema, category taxonomy,
|
||||
and Korean marketplace presence.
|
||||
Triggers: product SEO, e-commerce audit, product schema, category SEO, Smart Store, marketplace SEO,
|
||||
상품 SEO, 이커머스 감사, 쇼핑몰 SEO.
|
||||
---
|
||||
|
||||
# E-Commerce SEO Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Audit e-commerce sites for product page optimization, structured data validation, category taxonomy health, duplicate content issues, and Korean marketplace presence (Naver Smart Store, Coupang, Gmarket, 11번가).
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Product Page SEO Audit** - Title, meta description, H1, image alt text, internal links, canonical tags
|
||||
2. **Product Schema Validation** - Product, Offer, AggregateRating, Review, BreadcrumbList structured data
|
||||
3. **Category Taxonomy Analysis** - Depth check, breadcrumbs, faceted navigation handling
|
||||
4. **Duplicate Content Detection** - Parameter variants, product variants, pagination issues
|
||||
5. **Korean Marketplace Presence** - Naver Smart Store, Coupang, Gmarket, 11번가
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill spans multiple data classes (crawl + schema + keyword + marketplace presence). **Pick one backend per data class** — typically you'll combine: one for crawl/schema, one for keyword/competitor.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **OurSEO** (CLI + MCP) | **Default** for product-page crawl, product schema validation, category taxonomy, canonical/duplicate detection | CLI: `our collect crawl`, `our audit tech`, `our build kg-schema`. MCP: `mcp__ourseo__crawl_website`, `mcp__ourseo__audit_page`. Owns the crawl/audit/schema-fix surface end-to-end. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | E-commerce keyword discovery, organic competitor research, third-party site audit | `keyword_research`, `organic_research`, `siteaudit_research` → `get_report_schema` → `execute_report`. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Product/category backlinks; third-party site audit | `site-explorer-top-pages` (which product/category pages earn links), `site-audit-issues`, `site-audit-page-explorer`. |
|
||||
| **OurSEO CLI — DataForSEO** (`our keywords *`, `our serp *`) | Korean-market keyword + SERP batch for product/category terms | Claude Code only. `--location 2410` for Korean. |
|
||||
| **WebSearch / WebFetch** | Korean marketplace presence check (Naver Smart Store, Coupang, Gmarket, 11번가) | Korean marketplaces aren't covered by Semrush/Ahrefs at the listing level — verify presence with direct queries. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Fallback when `our` CLI isn't running | Same data as `our keywords *` / `our serp *`. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task is product schema / on-page audit / duplicate detection** → use **OurSEO** (it owns the schema + canonical + fix engine).
|
||||
4. **Task is e-commerce keyword discovery** → Semrush MCP (English) or OurSEO CLI (`--location 2410` Korean).
|
||||
5. **Task is Korean marketplace presence** → WebSearch/WebFetch (no MCP backend covers this).
|
||||
6. **Default for general e-commerce audits**: **OurSEO** (crawl + schema + fix) **paired with** Semrush MCP (keyword + competitor signal).
|
||||
7. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**OurSEO CLI (product-page crawl + schema audit, default):**
|
||||
```bash
|
||||
our collect crawl https://<site>.com --max-pages 100
|
||||
our audit tech https://<site>.com
|
||||
our analyze mysql-batch --session <id> --export missing-schema --format excel
|
||||
our build kg-schema --type Product --name "<product>" --url <product-url>
|
||||
```
|
||||
|
||||
**OurSEO MCP (Claude Desktop equivalent):**
|
||||
```
|
||||
mcp__ourseo__crawl_website(url="<site>", max_pages=100)
|
||||
mcp__ourseo__audit_page(url="<product-url>", audit_type="schema")
|
||||
```
|
||||
|
||||
**Semrush MCP (e-commerce keyword + competitor):**
|
||||
```
|
||||
mcp__semrush__keyword_research(query="<product term>", database="us")
|
||||
mcp__semrush__organic_research(query="<competitor.com>", database="us")
|
||||
mcp__semrush__siteaudit_research(query="<site.com>", database="us")
|
||||
```
|
||||
|
||||
**Ahrefs MCP (top pages + site audit):**
|
||||
```
|
||||
mcp__ahrefs__site-explorer-top-pages(target="<site>", country="us", limit=100)
|
||||
mcp__ahrefs__site-audit-issues(project_id="<id>")
|
||||
mcp__ahrefs__site-audit-page-explorer(project_id="<id>")
|
||||
```
|
||||
|
||||
**OurSEO CLI — Korean batch:**
|
||||
```bash
|
||||
our keywords ideas "<product>" --location 2410 --limit 50
|
||||
our serp ranked-keywords <site.com> --location 2410 --limit 100
|
||||
```
|
||||
|
||||
**Korean marketplace presence (WebSearch):**
|
||||
```
|
||||
WebSearch: smartstore.naver.com "<brand>"
|
||||
WebSearch: coupang.com "<brand>"
|
||||
WebSearch: gmarket.co.kr "<brand>"
|
||||
WebSearch: 11st.co.kr "<brand>"
|
||||
```
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future audits can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Product Page Audit
|
||||
1. Discover product pages via our-seo-agent CLI, pre-fetched JSON, or sitemap crawl
|
||||
2. For each product page check:
|
||||
- Title tag: contains product name, under 60 chars
|
||||
- Meta description: includes price/feature info, under 155 chars
|
||||
- Single H1 with product name
|
||||
- All product images have descriptive alt text
|
||||
- Canonical tag present and correct
|
||||
- Sufficient internal links (related products, breadcrumbs)
|
||||
- Open Graph tags for social sharing
|
||||
3. Score severity: critical/high/medium/low
|
||||
|
||||
### 2. Product Schema Validation
|
||||
1. Extract JSON-LD and Microdata from product pages
|
||||
2. Validate Product type: name, image, description (required)
|
||||
3. Validate Offer: price, priceCurrency, availability (required)
|
||||
4. Validate AggregateRating: ratingValue, reviewCount (required)
|
||||
5. Validate Review: author, reviewRating (required)
|
||||
6. Check BreadcrumbList implementation
|
||||
7. Assess Google rich result eligibility
|
||||
8. Check Naver Shopping specific requirements (Korean name, KRW price, absolute image URLs)
|
||||
|
||||
### 3. Category Taxonomy Analysis
|
||||
1. Crawl category pages from sitemap or homepage navigation
|
||||
2. Measure taxonomy depth (warn if > 4 levels)
|
||||
3. Check breadcrumb presence on every category page
|
||||
4. Identify faceted navigation URLs that are indexable without proper canonicals
|
||||
5. Count child category links for structure assessment
|
||||
|
||||
### 4. Duplicate Content Detection
|
||||
1. Group URLs by base path (stripping query parameters)
|
||||
2. Identify parameter variants (?color=, ?size=, ?sort=)
|
||||
3. Detect product variant URL duplicates (e.g., /product-red vs /product-blue)
|
||||
4. Flag paginated pages missing self-referencing canonicals
|
||||
|
||||
### 5. Korean Marketplace Presence
|
||||
1. Extract brand name from site (og:site_name or title)
|
||||
2. Search each marketplace for brand products:
|
||||
- Naver Smart Store (smartstore.naver.com)
|
||||
- Coupang (coupang.com)
|
||||
- Gmarket (gmarket.co.kr)
|
||||
- 11번가 (11st.co.kr)
|
||||
3. Check Naver Smart Store-specific SEO elements
|
||||
4. Verify naver-site-verification meta tag
|
||||
5. Check Korean content ratio for Naver visibility
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## E-Commerce SEO Audit: [domain]
|
||||
|
||||
### Score: [0-100]/100
|
||||
|
||||
### Product Page Issues
|
||||
- **Critical**: [count] issues
|
||||
- **High**: [count] issues
|
||||
- **Medium**: [count] issues
|
||||
- **Low**: [count] issues
|
||||
|
||||
#### Top Issues
|
||||
1. [severity] [issue_type] - [message]
|
||||
Recommendation: [fix]
|
||||
|
||||
### Category Structure
|
||||
- Categories found: [count]
|
||||
- Max depth: [number]
|
||||
- Breadcrumbs present: [count]
|
||||
- Faceted navigation issues: [count]
|
||||
|
||||
### Schema Validation
|
||||
- Pages with schema: [count]/[total]
|
||||
- Valid schemas: [count]
|
||||
- Rich result eligible: [count]
|
||||
- Common errors: [list]
|
||||
|
||||
### Korean Marketplaces
|
||||
- Naver Smart Store: [Found/Not Found]
|
||||
- Coupang: [Found/Not Found]
|
||||
- Gmarket: [Found/Not Found]
|
||||
- 11번가: [Found/Not Found]
|
||||
|
||||
### Recommendations
|
||||
1. [Priority fixes ordered by impact]
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
| Issue | Impact | Fix |
|
||||
|-------|--------|-----|
|
||||
| Missing Product schema | High | Add JSON-LD Product with offers |
|
||||
| No canonical on product variants | High | Add self-referencing canonical |
|
||||
| Images without alt text | High | Add product name to alt text |
|
||||
| Category depth > 4 levels | Medium | Flatten taxonomy |
|
||||
| Missing breadcrumbs | Medium | Add BreadcrumbList schema and visible nav |
|
||||
| Faceted nav creating duplicates | High | Use canonical or noindex on filtered pages |
|
||||
| Missing Naver verification | Medium | Add naver-site-verification meta tag |
|
||||
| Price not in KRW for Korean market | Medium | Add KRW pricing to schema |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Cannot access logged-in areas (member-only products)
|
||||
- Marketplace search results may vary by region/IP
|
||||
- Large catalogs require sampling (default 50 pages)
|
||||
- Cannot validate JavaScript-rendered product content without headless browser
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category (E-Commerce SEO), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: ECOM-YYYYMMDD-NNN
|
||||
166
custom-skills/25-seo-kpi-framework/SKILL.md
Normal file
166
custom-skills/25-seo-kpi-framework/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: seo-kpi-framework
|
||||
description: |
|
||||
SEO KPI and performance framework for unified metrics, health scores, ROI, and period-over-period reporting.
|
||||
Triggers: SEO KPI, performance report, health score, SEO metrics, ROI,
|
||||
baseline, targets, SEO 성과 지표, KPI 대시보드, SEO 성과 보고서.
|
||||
---
|
||||
|
||||
# SEO KPI & Performance Framework
|
||||
|
||||
## Purpose
|
||||
|
||||
Aggregate SEO KPIs across all dimensions into a unified dashboard. Establish baselines, set targets (30/60/90-day), generate executive summaries with health scores, provide tactical breakdowns, estimate ROI using our-seo-agent traffic cost data, and support period-over-period comparison (MoM, QoQ, YoY).
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **KPI Aggregation** - Unified metrics across 7 dimensions (traffic, rankings, links, technical, content, engagement, local)
|
||||
2. **Health Scoring** - Weighted 0-100 score with trend direction
|
||||
3. **Baseline & Targets** - Establish baselines and set 30/60/90 day growth targets
|
||||
4. **ROI Estimation** - Traffic value from organic cost data
|
||||
5. **Performance Reporting** - Period-over-period comparison with executive summary
|
||||
6. **Tactical Breakdown** - Actionable next steps per dimension
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
KPI framework is an **aggregator** — it pulls one metric per dimension from the best backend for that metric, then composites them. **Pick per metric, not per skill invocation.** Don't pull every metric from every backend.
|
||||
|
||||
### Per-metric backend defaults
|
||||
|
||||
| KPI dimension | Default backend | Alternates | Notes |
|
||||
|---|---|---|---|
|
||||
| **Organic traffic + traffic value** | Semrush `overview_research` | Ahrefs `site-explorer-metrics` | Modelled estimates — pick one per audit and document it. |
|
||||
| **Visibility / ranking distribution** | Semrush `tracking_research` (when project exists), else Ahrefs `rank-tracker-*` | OurSEO `check_serp` for spot positions | First-party alternative: GSC position data. |
|
||||
| **Backlinks / DR** | Ahrefs `site-explorer-domain-rating` + `-backlinks-stats` | Semrush `backlink_research` | Ahrefs has the strongest backlink graph. |
|
||||
| **Technical health** | OurSEO `our audit tech` + `our analyze mysql-batch` | Ahrefs `site-audit-issues`, Semrush `siteaudit_research` | OurSEO is the deepest because it owns the crawl + fix engine. |
|
||||
| **Indexed pages** | OurSEO `our research google index` / `mcp__ourseo__check_index` | GSC index coverage report | First-party best. |
|
||||
| **Content freshness** | OurSEO `crawl_website` (extracts last-modified) | Ahrefs `site-explorer-pages-history` | |
|
||||
| **First-party clicks / impressions / CTR** | **GSC** via `our research search-console` | Ahrefs `gsc-*` if Ahrefs project connected | **Required for accurate KPI** — Google's own data, not modelled. |
|
||||
| **GA4 / on-site engagement** | OurSEO CLI: `our research ga4 *` | (none equivalent in Semrush/Ahrefs at the property level) | Sessions, bounce, conversion. |
|
||||
| **Local visibility (GBP)** | OurSEO `our collect gbp *` + `our audit local` | (Semrush local listings only in US/EU) | First-party Google Business Profile data. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend for a specific metric** → use it for that metric.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor per-task defaults.
|
||||
3. **Apply per-metric defaults from the table above.** Default to first-party (GSC, GA4, GBP) whenever it's available — every modelled estimate weakens the composite.
|
||||
4. **Be consistent across reporting periods.** If the prior baseline used Semrush traffic value, use Semrush traffic value this run too — switching mid-stream breaks the trend.
|
||||
5. **Document every metric's source in the report Overview.**
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Semrush MCP (default traffic + visibility):**
|
||||
```
|
||||
mcp__semrush__overview_research(query="<domain>", database="us")
|
||||
mcp__semrush__organic_research(query="<domain>", database="us")
|
||||
mcp__semrush__tracking_research(query="<keyword>", database="us")
|
||||
```
|
||||
|
||||
**Ahrefs MCP (backlinks + traffic-value alternate):**
|
||||
```
|
||||
mcp__ahrefs__site-explorer-metrics(target="<domain>")
|
||||
mcp__ahrefs__site-explorer-domain-rating(target="<domain>")
|
||||
mcp__ahrefs__site-explorer-domain-rating-history(target="<domain>", history="weekly")
|
||||
mcp__ahrefs__site-explorer-backlinks-stats(target="<domain>")
|
||||
```
|
||||
|
||||
**OurSEO (technical + indexation + first-party):**
|
||||
```bash
|
||||
our audit tech https://<domain>
|
||||
our analyze mysql-batch --session <id>
|
||||
our research google index --domain <domain>
|
||||
our research search-console queries --site sc-domain:<domain> --days 28
|
||||
our research ga4 traffic --property-id <id>
|
||||
our audit local https://<domain> --gbp-profile <client>
|
||||
```
|
||||
|
||||
**OurSEO MCP (Claude Desktop alternate):**
|
||||
```
|
||||
mcp__ourseo__check_index(domain="<domain>")
|
||||
mcp__ourseo__audit_page(url="<url>", audit_type="tech")
|
||||
```
|
||||
|
||||
### Reporting rule
|
||||
|
||||
Every KPI report's **Overview** section MUST include a "Sources" subsection listing the data source per metric. Example:
|
||||
|
||||
```markdown
|
||||
### Sources
|
||||
- Organic traffic: Semrush overview_research (database=us)
|
||||
- Backlinks / DR: Ahrefs site-explorer-domain-rating
|
||||
- Indexed pages: OurSEO check_index
|
||||
- Clicks / Impressions: GSC (28d)
|
||||
- GBP visibility: OurSEO collect gbp (profile=client)
|
||||
```
|
||||
|
||||
This is non-negotiable — period-over-period KPI comparisons are meaningless without per-metric source attribution.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. KPI Aggregation
|
||||
1. Fetch site-explorer-metrics for current organic data
|
||||
2. Extract traffic, ranking, link, technical, content metrics
|
||||
3. Calculate dimension scores with weights (traffic 25%, rankings 20%, technical 20%, content 15%, links 15%, local 5%)
|
||||
4. Compute overall health score (0-100)
|
||||
5. Set 30/60/90 day targets (5%/10%/20% improvement)
|
||||
6. Estimate ROI from traffic cost data (use our-seo-agent CLI or pre-fetched JSON)
|
||||
|
||||
### 2. Performance Reporting
|
||||
1. Determine date range from period (monthly/quarterly/yearly/custom)
|
||||
2. Fetch metrics-history for current and previous period
|
||||
3. Calculate period-over-period changes
|
||||
4. Identify wins (>5% improvement) and concerns (>5% decline)
|
||||
5. Generate executive summary with trend arrows
|
||||
6. Create tactical breakdown with actionable next steps
|
||||
7. Compare against targets if provided
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## SEO KPI Dashboard: [domain]
|
||||
|
||||
### Health Score: [score]/100 ([trend])
|
||||
|
||||
### KPI Summary
|
||||
| Dimension | Score | Key Metric | Trend |
|
||||
|-----------|-------|------------|-------|
|
||||
| Traffic | [score] | [organic_traffic] | [arrow] |
|
||||
| Rankings | [score] | [visibility] | [arrow] |
|
||||
| Links | [score] | [DR] | [arrow] |
|
||||
| Technical | [score] | [health] | [arrow] |
|
||||
| Content | [score] | [indexed_pages] | [arrow] |
|
||||
|
||||
### Executive Summary
|
||||
- Top Wins: [list]
|
||||
- Top Concerns: [list]
|
||||
- Recommendations: [list]
|
||||
|
||||
### Targets (30/60/90 day)
|
||||
[Target table with progress bars]
|
||||
```
|
||||
|
||||
## Key Metrics
|
||||
|
||||
| Dimension | Metrics | Source |
|
||||
|-----------|---------|--------|
|
||||
| Traffic | Organic traffic, traffic value (USD) | site-explorer-metrics |
|
||||
| Rankings | Visibility score, top10 keywords | site-explorer-metrics |
|
||||
| Links | Domain rating, referring domains | domain-rating, metrics |
|
||||
| Technical | Pages crawled, technical health | site-explorer-metrics |
|
||||
| Content | Indexed pages, freshness score | site-explorer-metrics |
|
||||
| Local | GBP visibility, review score | External data |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Local KPIs require external GBP data (not available via our-seo-agent)
|
||||
- Engagement KPIs (bounce rate, session duration) require Google Analytics
|
||||
- Technical health is estimated heuristically from available data
|
||||
- ROI is estimated from organic traffic cost data, not actual revenue
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: KPI-YYYYMMDD-NNN
|
||||
168
custom-skills/26-seo-international/SKILL.md
Normal file
168
custom-skills/26-seo-international/SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: seo-international
|
||||
description: |
|
||||
International SEO audit and hreflang validation for multi-language and multi-region websites.
|
||||
Triggers: hreflang, international SEO, multi-language, multi-region, content parity, x-default, ccTLD, 다국어 SEO.
|
||||
---
|
||||
|
||||
# International SEO Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Audit international SEO implementation: hreflang tags, URL structure patterns, content parity across language versions, redirect logic, and Korean expansion strategies. Identify issues preventing proper multi-language indexing.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Hreflang Validation** - Bidirectional links, self-reference, x-default, language code validation
|
||||
2. **URL Structure Analysis** - ccTLD vs subdomain vs subdirectory pattern detection
|
||||
3. **Content Parity Audit** - Page count comparison, key page availability across languages
|
||||
4. **Redirect Logic Audit** - IP-based, Accept-Language redirects, forced redirect detection
|
||||
5. **Korean Expansion** - Priority markets (ja, zh, en), CJK URL issues, regional search engines
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
International SEO spans **hreflang validation** (crawl-derived), **content parity** (crawl-derived), and **per-country traffic** (modelled). Pick one backend per data class.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **OurSEO** (CLI + MCP) | **Default** — hreflang validation, content parity, redirect logic, fix engine | CLI: `our collect crawl`, `our audit tech`, `our fix hreflang`. MCP: `mcp__ourseo__crawl_website`, `mcp__ourseo__audit_page`. Only backend that owns the hreflang FIX path. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Per-country traffic distribution; hreflang issues from Ahrefs site audit | `web-analytics-countries`, `site-explorer-metrics-by-country`, `site-audit-issues`. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Per-database organic visibility (compare `kr`, `jp`, `us`, etc.) | `organic_research` with different `database` params; `siteaudit_research` for site-audit issues. |
|
||||
| **OurSEO CLI — Naver / per-region SERP** | Korean expansion + regional search engine SERP checks | `our research naver serp`, `our research keywords compare --engines naver`. |
|
||||
| **WebSearch / WebFetch** | Test geo/Accept-Language redirect behaviour, verify ccTLD handling | Required for redirect-logic audit — can't be done via SEO MCPs. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Per-country SERP fallback when `our` CLI isn't running | `serp_organic_live_advanced` with `location_code` per country. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task is hreflang validation / parity / fix** → use **OurSEO** (CLI primary, MCP for Desktop). No alternative owns the fix engine.
|
||||
4. **Task is per-country traffic distribution** → Ahrefs `web-analytics-countries` or Semrush `organic_research` across multiple `database` values.
|
||||
5. **Task is redirect-logic audit** (IP-based / Accept-Language) → WebFetch with explicit headers.
|
||||
6. **Default**: **OurSEO crawl + hreflang audit** for the validation work; Semrush MCP for cross-market visibility.
|
||||
7. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**OurSEO CLI (default — hreflang validation + fix):**
|
||||
```bash
|
||||
our collect crawl https://<site> --max-pages 200
|
||||
our audit tech https://<site>
|
||||
our fix hreflang --site https://<site>
|
||||
```
|
||||
|
||||
**OurSEO MCP (Claude Desktop):**
|
||||
```
|
||||
mcp__ourseo__crawl_website(url="<site>", max_pages=200)
|
||||
mcp__ourseo__audit_page(url="<url>", audit_type="tech")
|
||||
```
|
||||
|
||||
**Ahrefs MCP (per-country traffic):**
|
||||
```
|
||||
mcp__ahrefs__web-analytics-countries(domain="<site>")
|
||||
mcp__ahrefs__site-explorer-metrics-by-country(target="<site>")
|
||||
mcp__ahrefs__site-audit-issues(project_id="<id>")
|
||||
```
|
||||
|
||||
**Semrush MCP (cross-market visibility):**
|
||||
```
|
||||
mcp__semrush__organic_research(query="<site>", database="kr")
|
||||
mcp__semrush__organic_research(query="<site>", database="jp")
|
||||
mcp__semrush__siteaudit_research(query="<site>", database="us")
|
||||
```
|
||||
|
||||
**Redirect-logic audit (manual):**
|
||||
```
|
||||
WebFetch: https://<site>/ with Accept-Language: ko-KR
|
||||
WebFetch: https://<site>/ with Accept-Language: ja-JP
|
||||
WebFetch: https://<site>/ with Accept-Language: en-US
|
||||
# Compare redirect chains; flag forced geo/language redirects.
|
||||
```
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future audits can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Hreflang Validation
|
||||
1. Fetch target URL and extract hreflang tags (HTML head, HTTP headers)
|
||||
2. If sitemap provided, also extract xhtml:link hreflang from XML sitemap
|
||||
3. Validate language codes (ISO 639-1) and region codes (ISO 3166-1)
|
||||
4. Check bidirectional links (if A references B, B must reference A)
|
||||
5. Verify self-referencing tags on each page
|
||||
6. Check x-default tag presence and validity
|
||||
7. Detect conflicting hreflang for same language-region
|
||||
8. Report all errors with severity levels
|
||||
|
||||
### 2. URL Structure Analysis
|
||||
1. Crawl known language versions of the site
|
||||
2. Classify pattern: ccTLD (example.kr), subdomain (ko.example.com), subdirectory (example.com/ko/)
|
||||
3. Check consistency across all language versions
|
||||
4. Provide recommendation based on business context
|
||||
|
||||
### 3. Content Parity Audit
|
||||
1. Discover all language versions from hreflang tags
|
||||
2. Count pages per language version
|
||||
3. Check availability of key pages (home, about, contact, products/services)
|
||||
4. Compare content freshness (last modified dates) across versions
|
||||
5. Flag significant gaps in content availability
|
||||
|
||||
### 4. Redirect Logic Audit
|
||||
1. Test URL with different Accept-Language headers (ko, en, ja, zh)
|
||||
2. Check if redirects are forced (no way to override) vs suggested (banner/popup)
|
||||
3. Flag forced geo/language redirects as anti-pattern
|
||||
4. Recommend proper implementation (suggest, do not force)
|
||||
|
||||
### 5. Korean Expansion Analysis (Optional)
|
||||
1. Analyze current traffic by country via our-seo-agent CLI or pre-fetched data
|
||||
2. Recommend priority target markets for Korean businesses
|
||||
3. Check CJK-specific URL encoding issues
|
||||
4. Advise on regional search engines (Naver, Baidu, Yahoo Japan)
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## 다국어 SEO 감사: [domain]
|
||||
|
||||
### Hreflang 검증
|
||||
- 검사 페이지 수: [count]
|
||||
- 오류: [count] (심각 [count], 경고 [count])
|
||||
- 양방향 링크 누락: [list]
|
||||
- 자기참조 누락: [list]
|
||||
- x-default: [있음/없음]
|
||||
|
||||
### URL 구조
|
||||
- 패턴: [ccTLD/subdomain/subdirectory]
|
||||
- 일관성: [양호/비일관]
|
||||
- 권장사항: [recommendation]
|
||||
|
||||
### 콘텐츠 동등성
|
||||
| 언어 | 페이지 수 | 핵심 페이지 | 최신성 점수 |
|
||||
|------|----------|------------|-----------|
|
||||
| ko | 150 | 5/5 | 90 |
|
||||
| en | 120 | 4/5 | 75 |
|
||||
|
||||
### 리다이렉트 로직
|
||||
- IP 기반 리다이렉트: [있음/없음]
|
||||
- 언어 기반 리다이렉트: [있음/없음]
|
||||
- 강제 리다이렉트: [있음/없음] (없어야 정상)
|
||||
|
||||
### 종합 점수: [score]/100
|
||||
|
||||
### 권장 조치사항
|
||||
1. [Priority fixes in Korean]
|
||||
```
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category (International SEO), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: INTL-YYYYMMDD-NNN
|
||||
|
||||
## Limitations
|
||||
|
||||
- Cannot detect server-side IP-based redirects without proxy testing
|
||||
- Content language detection requires sufficient text content
|
||||
- Large sites (10,000+ pages) require sampling approach
|
||||
- Sitemap-based hreflang requires XML sitemap access
|
||||
158
custom-skills/27-seo-ai-visibility/SKILL.md
Normal file
158
custom-skills/27-seo-ai-visibility/SKILL.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: seo-ai-visibility
|
||||
description: |
|
||||
AI search visibility and brand radar monitoring. Tracks how a brand appears
|
||||
in AI-generated search answers using our-seo-agent CLI or pre-fetched data.
|
||||
Triggers: AI search, AI visibility, brand radar, AI citations,
|
||||
share of voice, AI answers, AI mentions.
|
||||
---
|
||||
|
||||
# SEO AI Visibility & Brand Radar
|
||||
|
||||
Monitor and analyze brand visibility in AI-generated search results. This skill uses our-seo-agent CLI or pre-fetched data to track impressions, mentions, share of voice, cited domains, cited pages, and AI response content.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### AI Visibility Tracking
|
||||
- **Impressions Overview** - How often the brand appears in AI answers
|
||||
- **Mentions Overview** - Brand mention frequency across AI engines
|
||||
- **Share of Voice (SOV)** - Brand's share vs competitors in AI search
|
||||
- **Historical Trends** - Impressions, mentions, and SOV over time
|
||||
- **Competitor Comparison** - Side-by-side AI visibility metrics
|
||||
|
||||
### AI Citation Analysis
|
||||
- **AI Response Analysis** - Content and sentiment of AI mentions
|
||||
- **Cited Domains** - Which source domains AI engines reference
|
||||
- **Cited Pages** - Specific URLs that get cited in AI answers
|
||||
- **Citation Ranking** - Frequency-based ranking of citations
|
||||
- **Sentiment Analysis** - Positive/neutral/negative distribution
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Input**: User provides target domain and optional competitors
|
||||
2. **Data Collection**: Fetch metrics from our-seo-agent CLI or pre-fetched JSON
|
||||
3. **Analysis**: Calculate trends, compare competitors, analyze sentiment
|
||||
4. **Recommendations**: Generate actionable Korean-language recommendations
|
||||
5. **Output**: JSON report and Notion database entry
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
**Single-vendor constraint.** AI search visibility / Brand Radar / share-of-voice in AI answers is currently **Ahrefs-only** among connected SEO MCPs. Neither Semrush nor DataForSEO exposes equivalent endpoints today. This is the one SEO skill where the "let the user choose" framing collapses to a one-option menu for the core capability.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Ahrefs MCP — Brand Radar** (`mcp__ahrefs__brand-radar-*`) | **Default** (and currently the only viable source) for AI mentions, impressions, share of voice, cited domains/pages, AI response history | Tools: `brand-radar-ai-responses`, `brand-radar-cited-domains`, `brand-radar-cited-pages`, `brand-radar-impressions-overview`, `brand-radar-mentions-overview`, `brand-radar-sov-overview`, `brand-radar-sov-history`, plus `*-entities` variants. |
|
||||
| **Ahrefs MCP — Management** | Configure Brand Radar prompts and reports | `management-brand-radar-prompts`, `management-brand-radar-reports`. |
|
||||
| **OurSEO brand monitoring** (`mcp__ourseo__monitor_brand` / `our research google brand`) | Web-wide brand mention scanning — **not** AI-search specific, but useful as a supplement to compare with AI citations | Catches brand mentions in standard Google search; complements Brand Radar's AI-specific view. |
|
||||
| **WebSearch** | Manual spot-check of AI engines (ChatGPT, Gemini, Perplexity) by querying directly | Not automated — useful for one-off "did the brand appear?" checks. |
|
||||
| **Semrush MCP** | **No equivalent.** Listed in `allowed-tools` only for future compatibility if Semrush adds an AI-visibility product. | Do not attempt to substitute Semrush for Brand Radar today. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named Ahrefs explicitly** → use Brand Radar.
|
||||
2. **Task is AI mentions / impressions / share of voice / cited pages** → Brand Radar is the only option; use it.
|
||||
3. **Task is general brand monitoring across the web (not AI-specific)** → OurSEO `monitor_brand` is sufficient and cheaper.
|
||||
4. **Task explicitly compares AI vs traditional brand visibility** → run **both** Brand Radar AND OurSEO `monitor_brand`, document the source per data slice.
|
||||
5. **No alternative needed** for the core AI-visibility capability — skip the AskUserQuestion fallback unless the user expresses preference uncertainty.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Ahrefs Brand Radar (default):**
|
||||
```
|
||||
mcp__ahrefs__brand-radar-impressions-overview(brand="<brand>")
|
||||
mcp__ahrefs__brand-radar-mentions-overview(brand="<brand>")
|
||||
mcp__ahrefs__brand-radar-sov-overview(brand="<brand>", competitors=["<comp1>","<comp2>"])
|
||||
mcp__ahrefs__brand-radar-sov-history(brand="<brand>", history="weekly")
|
||||
mcp__ahrefs__brand-radar-cited-domains(brand="<brand>")
|
||||
mcp__ahrefs__brand-radar-cited-pages(brand="<brand>")
|
||||
mcp__ahrefs__brand-radar-ai-responses(brand="<brand>")
|
||||
mcp__ahrefs__brand-radar-impressions-history(brand="<brand>", history="weekly")
|
||||
mcp__ahrefs__brand-radar-mentions-history(brand="<brand>", history="weekly")
|
||||
```
|
||||
|
||||
**Configure prompts / reports:**
|
||||
```
|
||||
mcp__ahrefs__management-brand-radar-prompts()
|
||||
mcp__ahrefs__management-brand-radar-reports()
|
||||
```
|
||||
|
||||
**OurSEO brand monitoring (supplement):**
|
||||
```
|
||||
mcp__ourseo__monitor_brand(brand="<brand>", exclude_domains=["<own.com>"])
|
||||
```
|
||||
|
||||
Or CLI:
|
||||
```bash
|
||||
our research google brand "<brand>" --exclude <own.com>
|
||||
```
|
||||
|
||||
### Tracking note
|
||||
|
||||
Brand Radar requires the brand to be configured in the user's Ahrefs workspace via `management-brand-radar-prompts`. If a query returns empty, first check that the brand is set up — don't conclude "no AI visibility" from an empty Brand Radar response without verifying configuration.
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future audits can compare like-for-like.
|
||||
|
||||
## Notion Output
|
||||
|
||||
All reports are saved to the OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Category**: AI Search Visibility
|
||||
- **Audit ID Format**: AI-YYYYMMDD-NNN
|
||||
- **Language**: Korean (technical terms in English)
|
||||
|
||||
## Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"domain": "example.com",
|
||||
"impressions": {
|
||||
"total": 15000,
|
||||
"trend": "increasing",
|
||||
"period": "30d"
|
||||
},
|
||||
"mentions": {
|
||||
"total": 450,
|
||||
"positive": 320,
|
||||
"neutral": 100,
|
||||
"negative": 30,
|
||||
"sentiment_score": 0.72
|
||||
},
|
||||
"share_of_voice": {
|
||||
"domain_sov": 12.5,
|
||||
"competitors": {
|
||||
"competitor1.com": 18.3,
|
||||
"competitor2.com": 15.1
|
||||
}
|
||||
},
|
||||
"cited_pages": [
|
||||
{"url": "https://example.com/guide", "citations": 45},
|
||||
{"url": "https://example.com/faq", "citations": 28}
|
||||
],
|
||||
"cited_domains": [
|
||||
{"domain": "example.com", "citations": 120},
|
||||
{"domain": "competitor1.com", "citations": 95}
|
||||
],
|
||||
"recommendations": [
|
||||
"Create more FAQ-style content for AI citation capture",
|
||||
"Add structured data to improve AI answer extraction"
|
||||
],
|
||||
"audit_id": "AI-20250115-001",
|
||||
"timestamp": "2025-01-15T14:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires our-seo-agent CLI or pre-fetched AI visibility data
|
||||
- AI search landscape changes rapidly; data may not reflect real-time state
|
||||
- Share of Voice metrics are relative to tracked competitor set only
|
||||
- Sentiment analysis based on AI-generated text, not user perception
|
||||
- Cannot distinguish between different AI engines (ChatGPT, Gemini, Perplexity) without Brand Radar
|
||||
|
||||
## Example Queries
|
||||
|
||||
- "example.com의 AI 검색 가시성을 분석해줘"
|
||||
- "AI search visibility for example.com with competitors"
|
||||
- "브랜드 레이더 분석: example.com vs competitor.com"
|
||||
- "AI 인용 분석 - 어떤 페이지가 AI 답변에서 인용되나요?"
|
||||
- "Share of Voice in AI search for our domain"
|
||||
177
custom-skills/28-seo-knowledge-graph/SKILL.md
Normal file
177
custom-skills/28-seo-knowledge-graph/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: seo-knowledge-graph
|
||||
description: |
|
||||
Knowledge Graph and entity SEO analysis.
|
||||
Triggers: knowledge panel, entity SEO, knowledge graph, PAA, FAQ schema,
|
||||
Wikipedia, Wikidata, brand entity, 지식 그래프, 엔티티 SEO,
|
||||
지식 패널, 브랜드 엔티티, 위키데이터.
|
||||
---
|
||||
|
||||
# Knowledge Graph & Entity SEO
|
||||
|
||||
Analyze brand entity presence in Google Knowledge Graph, Knowledge Panels, People Also Ask (PAA), and FAQ rich results. Check entity attribute completeness, Wikipedia/Wikidata presence, and Korean equivalents (Naver knowledge iN, Naver encyclopedia).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Knowledge Graph Analysis
|
||||
- Knowledge Panel detection and attribute extraction
|
||||
- Entity attribute completeness scoring (name, description, logo, type, social profiles, website, founded, CEO)
|
||||
- Wikipedia article presence check
|
||||
- Wikidata entity presence check (QID lookup)
|
||||
- Naver encyclopedia (네이버 백과사전) presence
|
||||
- Naver knowledge iN (지식iN) presence
|
||||
|
||||
### Entity SEO Audit
|
||||
- People Also Ask (PAA) monitoring for brand-related queries
|
||||
- FAQ schema presence tracking (FAQPage schema -> SERP appearance)
|
||||
- Entity markup audit (Organization, Person, LocalBusiness schema on website)
|
||||
- Social profile linking validation (sameAs in schema)
|
||||
- Brand SERP analysis (what appears when you search the brand name)
|
||||
- Entity consistency across web properties
|
||||
|
||||
## Workflow
|
||||
|
||||
### Knowledge Graph Analysis
|
||||
|
||||
1. Use **WebSearch** to search for the entity name on Google
|
||||
2. Analyze search results for Knowledge Panel indicators
|
||||
3. Use **WebFetch** to check Wikipedia article existence
|
||||
4. Use **WebFetch** to check Wikidata QID existence
|
||||
5. Use **WebFetch** to check Naver encyclopedia and 지식iN
|
||||
6. Score entity attribute completeness
|
||||
7. Save report to **Notion** SEO Audit Log
|
||||
|
||||
### Entity SEO Audit
|
||||
|
||||
1. Use **WebFetch** to fetch the website and extract JSON-LD schemas
|
||||
2. Validate Organization/Person/LocalBusiness schema completeness
|
||||
3. Check sameAs links accessibility
|
||||
4. Use **WebSearch** to search brand name and analyze SERP features
|
||||
5. Monitor PAA questions for brand keywords
|
||||
6. Use **WebSearch** for SERP feature detection
|
||||
7. Save report to **Notion** SEO Audit Log
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
Entity / Knowledge Graph work spans **KG API lookups**, **on-page schema audit**, **third-party presence checks** (Wikipedia, Wikidata, Naver), and **brand SERP analysis**. Different backends own different slices.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **OurSEO** (CLI + MCP) | **Default** for KG lookups, entity audit, schema generation + fix | CLI: `our research kg lookup`, `our research kg resolve`, `our audit entity`, `our audit sameas`, `our build kg-schema`, `our fix entity-schema`. MCP: `mcp__ourseo__search_knowledge_graph`. Only backend with an integrated KG + entity-fix path. |
|
||||
| **WebSearch / WebFetch** | Knowledge Panel detection, PAA monitoring, Wikipedia / Wikidata / Naver encyclopedia presence | KG Panel detection via direct Google search is heuristic but unavoidable — no MCP exposes Knowledge Panel structure directly. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | SERP-level entity queries — PAA, brand SERP, FAQ schema appearance | `serp-overview`, `gsc-keywords` (first-party brand query data), `site-audit-issues` for missing schema. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Brand SERP overview, organic positions for entity-related queries | No dedicated KG endpoints — supplementary only. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task is KG lookup / entity resolution / sameAs validation / schema fix** → use **OurSEO** (CLI primary, MCP for Desktop). No alternative covers this end-to-end.
|
||||
4. **Task is Wikipedia / Wikidata / Naver encyclopedia presence** → WebSearch + WebFetch (no MCP backend covers third-party encyclopedias).
|
||||
5. **Task is brand SERP analysis** → Ahrefs `serp-overview` or Semrush `overview_research`, paired with WebSearch for Knowledge Panel detection.
|
||||
6. **Default**: **OurSEO `our research kg`** + WebSearch for third-party presence.
|
||||
7. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**OurSEO CLI (default — KG lookup + entity audit + fix):**
|
||||
```bash
|
||||
our research kg lookup "<entity>" --language ko
|
||||
our research kg resolve "<entity>" --domain <site> --language ko
|
||||
our audit entity https://<site> --entity "<entity>" --language ko
|
||||
our audit sameas https://<site> --brand "<brand>"
|
||||
our build kg-schema --type Organization --name "<entity>" --url https://<site> --auto-sameas
|
||||
our fix entity-schema --site https://<site> --type Organization --entity-name "<entity>" --auto-sameas --cms ghost --deploy
|
||||
```
|
||||
|
||||
**OurSEO MCP (Claude Desktop):**
|
||||
```
|
||||
mcp__ourseo__search_knowledge_graph(query="<entity>", language="ko")
|
||||
```
|
||||
|
||||
**Third-party presence (WebSearch / WebFetch):**
|
||||
```
|
||||
WebSearch: "<entity>" site:wikipedia.org
|
||||
WebFetch: https://www.wikidata.org/wiki/Special:Search?search=<entity>
|
||||
WebSearch: "<entity>" site:terms.naver.com # Naver encyclopedia
|
||||
WebSearch: "<entity>" site:kin.naver.com # Naver 지식iN
|
||||
```
|
||||
|
||||
**Brand SERP analysis (Ahrefs / Semrush):**
|
||||
```
|
||||
mcp__ahrefs__serp-overview(keyword="<brand>", country="us")
|
||||
mcp__ahrefs__gsc-keywords(project_id="<id>") # First-party brand query data
|
||||
mcp__semrush__overview_research(query="<brand>", database="us")
|
||||
```
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future audits can compare like-for-like.
|
||||
|
||||
## Notion Output
|
||||
|
||||
All reports must be saved to the OurDigital SEO Audit Log database.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Database ID | `2c8581e5-8a1e-8035-880b-e38cefc2f3ef` |
|
||||
| Category | Knowledge Graph & Entity SEO |
|
||||
| Audit ID | KG-YYYYMMDD-NNN |
|
||||
|
||||
Report content should be written in Korean (한국어), keeping technical English terms as-is.
|
||||
|
||||
## Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"entity_name": "OurDigital",
|
||||
"knowledge_panel": {
|
||||
"present": false,
|
||||
"attributes": {}
|
||||
},
|
||||
"entity_presence": {
|
||||
"wikipedia": false,
|
||||
"wikidata": false,
|
||||
"wikidata_qid": null,
|
||||
"naver_encyclopedia": false,
|
||||
"naver_knowledge_in": false,
|
||||
"google_knowledge_panel": false
|
||||
},
|
||||
"entity_schema": {
|
||||
"organization_count": 2,
|
||||
"person_count": 1,
|
||||
"same_as_links": ["https://linkedin.com/...", "https://facebook.com/..."],
|
||||
"same_as_count": 2,
|
||||
"issues": [
|
||||
"Duplicate Organization schemas with inconsistent names",
|
||||
"Placeholder image in Organization schema",
|
||||
"Only 2 sameAs links (recommend 6+)"
|
||||
]
|
||||
},
|
||||
"paa_questions": [],
|
||||
"faq_schema_present": false,
|
||||
"entity_completeness_score": 12,
|
||||
"recommendations": [
|
||||
"Create Wikidata entity for brand recognition",
|
||||
"Add 4-6 more sameAs social profile links",
|
||||
"Replace placeholder image with actual brand logo",
|
||||
"Consolidate duplicate Organization schemas",
|
||||
"Add FAQPage schema to relevant pages"
|
||||
],
|
||||
"audit_id": "KG-20250115-001",
|
||||
"timestamp": "2025-01-15T14:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Google Knowledge Panel detection via search results is not guaranteed (personalization, location-based)
|
||||
- Direct Google scraping may be blocked (403/429); prefer WebSearch tool
|
||||
- Wikipedia/Wikidata creation requires meeting notability guidelines
|
||||
- PAA questions vary by location and device
|
||||
- Entity completeness scoring is heuristic-based
|
||||
|
||||
## Reference Scripts
|
||||
|
||||
Located in `code/scripts/`:
|
||||
- `knowledge_graph_analyzer.py` — Knowledge Panel and entity presence analysis
|
||||
- `entity_auditor.py` — Entity SEO signals and PAA/FAQ audit
|
||||
- `base_client.py` — Shared async client utilities
|
||||
168
custom-skills/29-seo-gateway-architect/SKILL.md
Normal file
168
custom-skills/29-seo-gateway-architect/SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: seo-gateway-architect
|
||||
description: |
|
||||
Gateway page strategy planner for keyword research, content architecture, and SEO KPIs.
|
||||
Triggers: SEO strategy, gateway pages, keyword research, content architecture.
|
||||
---
|
||||
|
||||
# SEO Gateway Page Strategist
|
||||
|
||||
This skill helps you create comprehensive SEO-focused gateway page strategies for Korean medical/service websites, optimized for both Naver and Google.
|
||||
|
||||
## Core Competencies
|
||||
|
||||
1. **Keyword Research & Analysis**: Identifies primary and LSI keywords with search intent mapping
|
||||
2. **Content Architecture**: Creates hierarchical page structure optimized for SEO
|
||||
3. **Technical SEO Planning**: Defines specific technical requirements and meta optimizations
|
||||
4. **Performance Targeting**: Sets measurable KPIs and tracking methodologies
|
||||
5. **Competitor Analysis**: Analyzes top-ranking competitors for gap identification
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Planning a new gateway page for any service/procedure category
|
||||
- Restructuring existing pages for better SEO performance
|
||||
- Conducting keyword research for content planning
|
||||
- Setting SEO performance targets and KPIs
|
||||
- Analyzing competitor strategies
|
||||
|
||||
## Instructions
|
||||
|
||||
When using this skill, provide:
|
||||
1. **Service/Procedure Name**: The main topic for the gateway page (e.g., "눈 성형", "이마 성형")
|
||||
2. **Target Market**: Location and demographic information
|
||||
3. **Current Performance** (optional): Existing rankings, traffic data if available
|
||||
4. **Competitor URLs** (optional): Known competitors to analyze
|
||||
|
||||
## Process Workflow
|
||||
|
||||
### Step 1: Keyword & Intent Analysis
|
||||
```python
|
||||
# The skill will generate:
|
||||
- Primary keyword with monthly search volume
|
||||
- 7-10 LSI (Latent Semantic Indexing) keywords
|
||||
- User intent distribution (Informational/Comparative/Transactional)
|
||||
- Top 3 competitor analysis
|
||||
```
|
||||
|
||||
### Step 2: Content Architecture
|
||||
The skill creates a complete H1-H3 structure with keyword placement strategy:
|
||||
```
|
||||
H1: [Primary keyword-optimized headline]
|
||||
├── Hero Section
|
||||
├── Problem/Solution Framework
|
||||
├── Service Categories
|
||||
├── Trust & Authority
|
||||
├── FAQ Section
|
||||
└── Consultation Guide
|
||||
```
|
||||
|
||||
### Step 3: Technical SEO Requirements
|
||||
Generates specific technical specifications:
|
||||
- Meta tags formulas and character limits
|
||||
- Schema markup recommendations
|
||||
- Internal linking strategy
|
||||
- Image optimization guidelines
|
||||
- Core Web Vitals targets
|
||||
|
||||
### Step 4: Performance Metrics
|
||||
Sets 30/60/90-day KPIs with tracking methodology
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Basic Request:
|
||||
```
|
||||
"Create an SEO gateway page strategy for 눈 성형"
|
||||
```
|
||||
|
||||
### Detailed Request:
|
||||
```
|
||||
"Create an SEO gateway page strategy for 눈 성형 targeting women aged 25-45 in Gangnam.
|
||||
Current ranking: page 2 for main keyword.
|
||||
Competitor: www.example-clinic.com/eye-surgery"
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
The skill delivers a structured report containing:
|
||||
|
||||
1. **Keyword Strategy Table**
|
||||
- Primary and LSI keywords with search volumes
|
||||
- User intent percentages
|
||||
- Competitor gap analysis
|
||||
|
||||
2. **Content Architecture Document**
|
||||
- Complete page hierarchy (H1-H3)
|
||||
- Word count targets per section
|
||||
- Keyword placement map
|
||||
|
||||
3. **Technical SEO Checklist**
|
||||
- Meta tag templates
|
||||
- Schema markup code
|
||||
- Performance requirements
|
||||
|
||||
4. **Performance Dashboard**
|
||||
- Current baseline metrics
|
||||
- Target KPIs with timeline
|
||||
- Tracking methodology
|
||||
|
||||
## Templates Included
|
||||
|
||||
- `keyword-research-template.md`: Keyword analysis worksheet
|
||||
- `content-architecture-template.md`: Page structure template
|
||||
- `seo-checklist-template.md`: Technical SEO requirements
|
||||
- `performance-tracking-template.md`: KPI tracking sheet
|
||||
|
||||
## Scripts Included
|
||||
|
||||
- `keyword_analyzer.py`: Automates keyword research and intent analysis
|
||||
- `competitor_analyzer.py`: Scrapes and analyzes competitor pages
|
||||
- `seo_scorer.py`: Calculates SEO optimization score
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Mobile-First Approach**: Always optimize for mobile (70%+ traffic in Korea)
|
||||
2. **Naver vs Google**: Consider platform-specific optimization differences
|
||||
3. **Local SEO**: Include location modifiers for local intent
|
||||
4. **Medical Compliance**: Ensure content meets Korean medical advertising regulations
|
||||
5. **User Intent Matching**: Align content with search intent distribution
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### For Medical Services:
|
||||
```
|
||||
Primary: [시술명]
|
||||
LSI: [시술명 비용], [시술명 부작용], [시술명 회복기간], [시술명 전후]
|
||||
Intent: 60% Informational, 30% Comparative, 10% Transactional
|
||||
```
|
||||
|
||||
### For Local Services:
|
||||
```
|
||||
Primary: [지역] [서비스명]
|
||||
LSI: [지역] [서비스명] 추천, [지역] [서비스명] 잘하는곳, [지역] [서비스명] 가격
|
||||
Intent: 40% Informational, 40% Comparative, 20% Transactional
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
This skill integrates with:
|
||||
- Google Search Console for current performance data
|
||||
- Naver Webmaster Tools for Naver-specific metrics
|
||||
- Analytics platforms for user behavior data
|
||||
- Keyword research tools APIs
|
||||
|
||||
## Notes
|
||||
|
||||
- Always validate keyword search volumes with actual tools
|
||||
- Consider seasonal trends in search behavior
|
||||
- Update strategy based on algorithm changes
|
||||
- Monitor competitor movements regularly
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: [TYPE]-YYYYMMDD-NNN
|
||||
|
||||
386
custom-skills/30-seo-gateway-builder/SKILL.md
Normal file
386
custom-skills/30-seo-gateway-builder/SKILL.md
Normal file
@@ -0,0 +1,386 @@
|
||||
---
|
||||
name: seo-gateway-builder
|
||||
description: |
|
||||
Gateway page content builder with templates, schema markup, and local SEO optimization.
|
||||
Triggers: build gateway page, create landing page, local service page, location pages.
|
||||
---
|
||||
|
||||
# Gateway Page Content Builder
|
||||
|
||||
A comprehensive skill for building high-quality, SEO-optimized gateway page content for local services, medical practices, and business locations.
|
||||
|
||||
## Core Purpose
|
||||
|
||||
This skill provides a systematic framework for creating gateway pages that:
|
||||
- Target specific location + service keyword combinations
|
||||
- Follow SEO best practices for local search optimization
|
||||
- Maintain content quality and uniqueness at scale
|
||||
- Include structured data and technical SEO elements
|
||||
|
||||
## Content Generation Framework
|
||||
|
||||
### 1. Page Structure Template
|
||||
|
||||
Every gateway page should follow this optimized structure:
|
||||
|
||||
```markdown
|
||||
# [Service Name] in [Location] - [Brand Name]
|
||||
|
||||
## Hero Section
|
||||
- Primary headline with target keywords
|
||||
- Value proposition statement
|
||||
- Quick contact CTA
|
||||
|
||||
## Service Overview
|
||||
- What is [service]?
|
||||
- Why choose our [service] in [location]
|
||||
- Key benefits for [location] residents
|
||||
|
||||
## Local Service Details
|
||||
- Service availability in [location]
|
||||
- Local team/facility information
|
||||
- Location-specific offerings
|
||||
|
||||
## Process & Procedure
|
||||
- Step-by-step service flow
|
||||
- Duration and frequency
|
||||
- What to expect
|
||||
|
||||
## Benefits & Results
|
||||
- Evidence-based outcomes
|
||||
- Patient/customer testimonials
|
||||
- Before/after scenarios
|
||||
|
||||
## Pricing & Insurance
|
||||
- Transparent pricing structure
|
||||
- Insurance coverage details
|
||||
- Payment options
|
||||
|
||||
## FAQ Section
|
||||
- Location-specific questions
|
||||
- Service-specific concerns
|
||||
- Booking and preparation
|
||||
|
||||
## Contact & Booking
|
||||
- Clear CTA sections
|
||||
- Multiple contact methods
|
||||
- Online booking integration
|
||||
```
|
||||
|
||||
### 2. Content Variables System
|
||||
|
||||
Define reusable content variables for efficient scaling:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
service_types:
|
||||
- name: "laser_hair_removal"
|
||||
korean: "레이저 제모"
|
||||
description: "Advanced laser technology for permanent hair reduction"
|
||||
keywords: ["laser hair removal", "permanent hair removal", "IPL treatment"]
|
||||
|
||||
locations:
|
||||
- name: "gangnam"
|
||||
korean: "강남"
|
||||
full_address: "서울특별시 강남구"
|
||||
landmarks: ["COEX", "Samsung Station", "Gangnam Station"]
|
||||
demographics: "Young professionals, high income"
|
||||
|
||||
brand_info:
|
||||
name: "Your Clinic"
|
||||
korean: "클리닉명"
|
||||
usp: "15+ years of experience with latest technology"
|
||||
```
|
||||
|
||||
### 3. Content Generation Rules
|
||||
|
||||
#### Title Tag Formula
|
||||
```
|
||||
[Service] in [Location] | [Unique Modifier] | [Brand]
|
||||
Examples:
|
||||
- "Laser Hair Removal in Gangnam | Same-Day Appointments | Jamie Clinic"
|
||||
- "강남 레이저 제모 | 당일 예약 가능 | 제이미 클리닉"
|
||||
```
|
||||
|
||||
#### Meta Description Template
|
||||
```
|
||||
Looking for [service] in [location]? [Brand] offers [USP] with [benefit].
|
||||
Book your consultation today. ✓ [Feature 1] ✓ [Feature 2] ✓ [Feature 3]
|
||||
```
|
||||
|
||||
#### H1 Optimization
|
||||
```
|
||||
Primary: [Service] in [Location]
|
||||
Alternative: [Location] [Service] - [Brand Modifier]
|
||||
Korean: [지역] [서비스] 전문 [브랜드]
|
||||
```
|
||||
|
||||
### 4. Local SEO Elements
|
||||
|
||||
#### Schema Markup Requirements
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "MedicalBusiness",
|
||||
"name": "Clinic Name",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "",
|
||||
"addressLocality": "",
|
||||
"addressRegion": "",
|
||||
"postalCode": ""
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "",
|
||||
"longitude": ""
|
||||
},
|
||||
"areaServed": {
|
||||
"@type": "City",
|
||||
"name": "Location Name"
|
||||
},
|
||||
"medicalSpecialty": "Service Type",
|
||||
"availableService": {
|
||||
"@type": "MedicalProcedure",
|
||||
"name": "Service Name",
|
||||
"description": "Service Description"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Content Uniqueness Strategy
|
||||
|
||||
#### Localization Techniques
|
||||
1. **Local landmarks**: "Just 5 minutes from [Landmark]"
|
||||
2. **Transportation**: "Accessible via [Subway Line] at [Station]"
|
||||
3. **Local statistics**: "Serving [X] residents in [Area] since [Year]"
|
||||
4. **Community involvement**: "Proud partner of [Local Organization]"
|
||||
5. **Regional preferences**: "Tailored to [Location] residents' needs"
|
||||
|
||||
#### Content Variation Patterns
|
||||
```python
|
||||
variations = {
|
||||
"intro_patterns": [
|
||||
"Discover professional [service] in [location]",
|
||||
"[Location] residents trust us for [service]",
|
||||
"Your local [service] experts in [location]",
|
||||
"Premium [service] now available in [location]"
|
||||
],
|
||||
"cta_patterns": [
|
||||
"Book your [location] appointment today",
|
||||
"Schedule a consultation at our [location] clinic",
|
||||
"Visit us in [location] for [service]",
|
||||
"Get started with [service] in [location]"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Content Quality Checklist
|
||||
|
||||
Before publishing any gateway page, verify:
|
||||
|
||||
- [ ] **Keyword optimization**: Target keyword appears in title, H1, first 100 words
|
||||
- [ ] **Content length**: Minimum 800 words of unique content
|
||||
- [ ] **Local signals**: At least 5 location mentions naturally integrated
|
||||
- [ ] **Structured data**: Schema markup properly implemented
|
||||
- [ ] **Internal linking**: Links to main service page and location page
|
||||
- [ ] **Images**: Alt text includes location + service keywords
|
||||
- [ ] **Mobile optimization**: Content readable on mobile devices
|
||||
- [ ] **Load speed**: Page loads under 3 seconds
|
||||
- [ ] **CTAs**: Clear calls-to-action above and below fold
|
||||
- [ ] **Trust signals**: Reviews, certifications, testimonials included
|
||||
|
||||
### 7. Scaling Framework
|
||||
|
||||
#### Batch Generation Process
|
||||
1. Create master template with variable placeholders
|
||||
2. Define location and service matrices
|
||||
3. Generate unique content blocks for each combination
|
||||
4. Review and customize top 20% traffic potential pages
|
||||
5. Implement progressive enhancement based on performance
|
||||
|
||||
#### Priority Matrix
|
||||
```
|
||||
High Priority (Manual Optimization):
|
||||
- High search volume + High commercial intent
|
||||
- Major city centers + Premium services
|
||||
- Competitive keywords requiring unique angle
|
||||
|
||||
Medium Priority (Template + Customization):
|
||||
- Moderate search volume + Standard services
|
||||
- Secondary locations + Common procedures
|
||||
|
||||
Low Priority (Automated Generation):
|
||||
- Long-tail keywords + Suburban areas
|
||||
- Informational intent + Low competition
|
||||
```
|
||||
|
||||
### 8. Performance Tracking
|
||||
|
||||
#### KPIs to Monitor
|
||||
```yaml
|
||||
metrics:
|
||||
organic_traffic:
|
||||
- Pageviews from organic search
|
||||
- Unique visitors by location
|
||||
- Average session duration
|
||||
|
||||
conversions:
|
||||
- Form submissions by page
|
||||
- Phone calls tracked
|
||||
- Online bookings completed
|
||||
|
||||
engagement:
|
||||
- Bounce rate below 40%
|
||||
- Pages per session above 2.0
|
||||
- Scroll depth above 75%
|
||||
|
||||
rankings:
|
||||
- Position tracking for target keywords
|
||||
- Local pack appearances
|
||||
- Featured snippet captures
|
||||
```
|
||||
|
||||
## Implementation Instructions
|
||||
|
||||
### Step 1: Keyword Research
|
||||
```python
|
||||
# Generate keyword combinations
|
||||
locations = ["gangnam", "sinsa", "apgujeong"]
|
||||
services = ["laser_hair_removal", "botox", "filler"]
|
||||
|
||||
keywords = []
|
||||
for location in locations:
|
||||
for service in services:
|
||||
keywords.append({
|
||||
"primary": f"{service} {location}",
|
||||
"secondary": f"{location} {service} clinic",
|
||||
"long_tail": f"best {service} clinic in {location}"
|
||||
})
|
||||
```
|
||||
|
||||
### Step 2: Content Creation
|
||||
1. Use the template structure above
|
||||
2. Fill in variables for location and service
|
||||
3. Add unique local content (minimum 30% unique per page)
|
||||
4. Include relevant images with local landmarks
|
||||
5. Add schema markup and meta tags
|
||||
|
||||
### Step 3: Technical Implementation
|
||||
1. Create URL structure: `/location/service/`
|
||||
2. Implement breadcrumbs with proper schema
|
||||
3. Add internal linking to related pages
|
||||
4. Set up canonical tags to avoid duplication
|
||||
5. Create XML sitemap for gateway pages
|
||||
|
||||
### Step 4: Quality Assurance
|
||||
- Run content through plagiarism checker
|
||||
- Verify all technical SEO elements
|
||||
- Test page speed and mobile responsiveness
|
||||
- Review content for local relevance
|
||||
- Check all CTAs and contact information
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Dynamic Content Insertion
|
||||
```javascript
|
||||
// Example of dynamic content based on user location
|
||||
const userLocation = getUserLocation();
|
||||
const nearestClinic = findNearestClinic(userLocation);
|
||||
|
||||
// Update content dynamically
|
||||
document.querySelector('.hero-location').textContent =
|
||||
`Serving ${userLocation.district} and surrounding areas`;
|
||||
|
||||
document.querySelector('.distance-info').textContent =
|
||||
`Only ${nearestClinic.distance} from your location`;
|
||||
```
|
||||
|
||||
### A/B Testing Framework
|
||||
```yaml
|
||||
test_variations:
|
||||
headlines:
|
||||
- control: "[Service] in [Location]"
|
||||
- variant_a: "#1 [Service] Provider in [Location]"
|
||||
- variant_b: "[Location]'s Trusted [Service] Clinic"
|
||||
|
||||
cta_buttons:
|
||||
- control: "Book Now"
|
||||
- variant_a: "Get Free Consultation"
|
||||
- variant_b: "Check Availability"
|
||||
```
|
||||
|
||||
### Content Refresh Strategy
|
||||
- Monthly: Update testimonials and reviews
|
||||
- Quarterly: Refresh statistics and data points
|
||||
- Semi-annually: Add new FAQs based on search queries
|
||||
- Annually: Complete content audit and refresh
|
||||
|
||||
## Prompts for Content Generation
|
||||
|
||||
### Initial Content Brief
|
||||
```
|
||||
Create gateway page content for [SERVICE] in [LOCATION]:
|
||||
- Target keyword: [PRIMARY KEYWORD]
|
||||
- Secondary keywords: [LIST]
|
||||
- Local landmarks: [LIST]
|
||||
- Unique selling points: [LIST]
|
||||
- Competitor differentiation: [POINTS]
|
||||
```
|
||||
|
||||
### Content Expansion
|
||||
```
|
||||
Expand the following gateway page section:
|
||||
Current content: [PASTE]
|
||||
Add: Local statistics, transportation info, 2 testimonials
|
||||
Maintain: Professional tone, keyword density 2-3%
|
||||
Length: 200-300 words
|
||||
```
|
||||
|
||||
### FAQ Generation
|
||||
```
|
||||
Generate 8 FAQs for [SERVICE] in [LOCATION]:
|
||||
- 3 service-specific questions
|
||||
- 2 location/accessibility questions
|
||||
- 2 pricing/insurance questions
|
||||
- 1 preparation/aftercare question
|
||||
Include question schema markup format
|
||||
```
|
||||
|
||||
## Resources and Tools
|
||||
|
||||
### Recommended Tools
|
||||
- **Keyword Research**: Ahrefs, SEMrush, Google Keyword Planner
|
||||
- **Content Optimization**: Surfer SEO, Clearscope, MarketMuse
|
||||
- **Schema Generation**: Schema.org, Google's Structured Data Tool
|
||||
- **Performance Tracking**: Google Analytics, Search Console
|
||||
- **A/B Testing**: Google Optimize, Optimizely
|
||||
|
||||
### Templates Directory
|
||||
- `templates/gateway-page-medical.md`
|
||||
- `templates/gateway-page-beauty.md`
|
||||
- `templates/gateway-page-dental.md`
|
||||
- `templates/schema-medical-business.json`
|
||||
- `templates/meta-tags-local.html`
|
||||
|
||||
## Version History
|
||||
|
||||
### v1.0.0 (Current)
|
||||
- Initial framework for gateway page content generation
|
||||
- Medical and beauty service focus
|
||||
- Korean market optimization
|
||||
- Local SEO best practices
|
||||
- Content scaling methodology
|
||||
|
||||
---
|
||||
|
||||
*This skill is optimized for Korean medical and beauty service markets but can be adapted for any local service business requiring location-based gateway pages.*
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category, Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: [TYPE]-YYYYMMDD-NNN
|
||||
|
||||
95
custom-skills/31-notion-organizer/SKILL.md
Normal file
95
custom-skills/31-notion-organizer/SKILL.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: notion-organizer
|
||||
description: |
|
||||
Notion workspace manager for database optimization, property cleanup, and bulk operations.
|
||||
Triggers: organize Notion, workspace cleanup, database schema, property standardization.
|
||||
---
|
||||
|
||||
# Notion Organizer Skill
|
||||
|
||||
## Purpose
|
||||
|
||||
Specialized Notion workspace management capability for:
|
||||
- Database schema analysis and optimization
|
||||
- Property standardization and cleanup
|
||||
- Content restructuring and hierarchy optimization
|
||||
- Database merging and migration
|
||||
- Bulk operations with rate-limit compliance
|
||||
|
||||
## Execution Strategy: Three-Tier Approach
|
||||
|
||||
Always follow this priority order:
|
||||
|
||||
### Tier 1: Notion MCP Tools (Primary)
|
||||
|
||||
Use built-in MCP tools first. Available tools:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `mcp__notion__search` | Find pages/databases by keyword |
|
||||
| `mcp__notion__get-page` | Retrieve page content |
|
||||
| `mcp__notion__get-database` | Retrieve database schema |
|
||||
| `mcp__notion__create-page` | Create new pages |
|
||||
| `mcp__notion__update-page` | Modify page properties |
|
||||
| `mcp__notion__query-database` | Query database with filters |
|
||||
|
||||
### Tier 2: Alternative Approaches (Fallback)
|
||||
|
||||
If MCP tools insufficient:
|
||||
- Export/import via filesystem (user action required)
|
||||
- Memory tools for tracking state across sessions
|
||||
- Sequential thinking for complex planning
|
||||
|
||||
### Tier 3: Python Scripts (Advanced)
|
||||
|
||||
For bulk operations (50+ items):
|
||||
- Generate async Python scripts
|
||||
- Include rate limiting (3 req/sec max)
|
||||
- Provide requirements.txt
|
||||
- Always include dry-run option
|
||||
|
||||
See `scripts/` directory for templates.
|
||||
|
||||
## Operational Guidelines
|
||||
|
||||
### Before Any Modification
|
||||
1. **Fetch first**: Always examine current structure before changes
|
||||
2. **Confirm destructive actions**: Get user approval for deletes/major restructures
|
||||
3. **Estimate impact**: For large operations, provide time/API call estimates
|
||||
4. **Backup reminder**: Remind about Notion version history
|
||||
|
||||
### Rate Limits (Critical)
|
||||
- Maximum: 3 requests/second average
|
||||
- Use pagination (100 items max per request)
|
||||
- Implement exponential backoff on 429 errors
|
||||
|
||||
### Communication
|
||||
- Korean for explanations (한국어로 설명)
|
||||
- English for code and technical terms
|
||||
- Structured before/after summaries
|
||||
|
||||
## Quick Commands
|
||||
|
||||
### Database Audit
|
||||
"Analyze [database name] structure and recommend optimizations"
|
||||
|
||||
### Property Cleanup
|
||||
"Standardize property names in [database] to [convention]"
|
||||
|
||||
### Bulk Move
|
||||
"Move all pages tagged [X] from [source] to [target]"
|
||||
|
||||
### Schema Migration
|
||||
"Migrate data from [source database] to [target database]"
|
||||
|
||||
## Workflow Patterns
|
||||
|
||||
See `reference.md` for detailed workflow documentation.
|
||||
See `scripts/` for Python templates.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Cannot access unshared databases/pages
|
||||
- Cannot modify workspace settings
|
||||
- Cannot recover permanently deleted content
|
||||
- Large operations (1000+ pages) require Python scripts
|
||||
189
custom-skills/31-seo-competitor-intel/SKILL.md
Normal file
189
custom-skills/31-seo-competitor-intel/SKILL.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
name: seo-competitor-intel
|
||||
description: |
|
||||
Competitor intelligence and SEO benchmarking.
|
||||
Triggers: competitor analysis, competitive intelligence, competitor comparison,
|
||||
threat assessment, market position, benchmarking, 경쟁사 분석,
|
||||
경쟁 인텔리전스, 벤치마킹, 경쟁사 비교.
|
||||
---
|
||||
|
||||
# SEO Competitor Intelligence & Benchmarking
|
||||
|
||||
## Purpose
|
||||
|
||||
Comprehensive competitor intelligence for SEO: auto-discover competitors, build profile cards, create head-to-head comparison matrices, analyze keyword overlap, track traffic trends, and score competitive threats. Supports Korean market analysis including Naver Blog/Cafe presence.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Competitor Discovery** - Auto-discover organic competitors via our-seo-agent CLI
|
||||
2. **Profile Cards** - DR, traffic, keywords, referring domains, top pages, content volume
|
||||
3. **Comparison Matrix** - Multi-dimensional head-to-head comparison
|
||||
4. **Keyword Overlap** - Shared, unique, and gap keyword analysis
|
||||
5. **Threat Scoring** - 0-100 score based on DR gap, traffic ratio, keyword overlap, growth
|
||||
6. **Competitive Monitoring** - Traffic trends, DR changes, keyword movement, content velocity
|
||||
7. **Alert Generation** - Flag significant competitive movements
|
||||
8. **Market Share Estimation** - Organic traffic share within competitive set
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
Competitor intelligence spans **organic profile** (traffic, keywords), **backlink profile** (DR, refdomains), **content velocity** (top pages, new content), and **Korean platform presence**. Different backends own different slices.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | **Default** for organic competitor profile, keyword overlap, traffic trends | `organic_research`, `trends_research`, `overview_research` → `get_report_schema` → `execute_report`. Auto-discovery of competitors. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Backlink profile, organic competitor discovery, top-pages-by-traffic, historical DR | `site-explorer-organic-competitors`, `site-explorer-organic-keywords`, `site-explorer-top-pages`, `site-explorer-domain-rating-history`, `site-explorer-backlinks-stats`. Pair with Semrush for cross-check. |
|
||||
| **OurSEO** (CLI + MCP) | SERP overlap spot-checks, content similarity, crawl-derived content velocity | `mcp__ourseo__check_serp`, `mcp__ourseo__find_similar_pages`, `mcp__ourseo__crawl_website`. CLI: `our serp competitors`, `our serp ranked-keywords`. |
|
||||
| **OurSEO CLI — Korean** (`our research naver *`) | Naver-engine competitor analysis (Blog/Cafe/Smart Store presence, Naver SERP) | Naver-only; required for Korean market since Semrush/Ahrefs don't cover Naver. |
|
||||
| **WebSearch** | Korean Blog/Cafe/Tistory/Brunch presence checks | Supplement Korean platform mapping when Semrush/Ahrefs underrepresent these. |
|
||||
| **DataForSEO MCP** (`mcp__dfs-mcp__*`) | Fallback for ranked-keywords / SERP competitors when `our` CLI isn't running | Same data as `our serp *`. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Task is backlink-focused** (DR comparison, link gap) → Ahrefs (backlink-graph moat).
|
||||
4. **Task is keyword/traffic-focused** → Semrush (organic competitor view is strongest).
|
||||
5. **Task is Korean-market competitor analysis** → **OurSEO CLI** (`our research naver *` + `our serp * --location 2410`). Korean platforms aren't covered by Semrush/Ahrefs.
|
||||
6. **Default**: Semrush MCP for organic + Ahrefs for backlinks (paired). Korean override: OurSEO CLI.
|
||||
7. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Semrush MCP (default organic):**
|
||||
```
|
||||
mcp__semrush__organic_research(query="<target>", database="us")
|
||||
mcp__semrush__organic_research(query="<competitor>", database="us")
|
||||
mcp__semrush__trends_research(query="<target>", database="us")
|
||||
mcp__semrush__overview_research(query="<target>", database="us")
|
||||
```
|
||||
|
||||
**Ahrefs MCP (backlink + top pages):**
|
||||
```
|
||||
mcp__ahrefs__site-explorer-organic-competitors(target="<target>")
|
||||
mcp__ahrefs__site-explorer-organic-keywords(target="<target>", country="us", limit=500)
|
||||
mcp__ahrefs__site-explorer-top-pages(target="<competitor>", country="us")
|
||||
mcp__ahrefs__site-explorer-domain-rating-history(target="<target>", history="weekly")
|
||||
mcp__ahrefs__site-explorer-backlinks-stats(target="<target>")
|
||||
```
|
||||
|
||||
**OurSEO (SERP overlap + content similarity):**
|
||||
```
|
||||
mcp__ourseo__check_serp(keyword="<keyword>", domain="<target>", country="kr")
|
||||
mcp__ourseo__find_similar_pages(crawl_path="<path>", query="<topic>")
|
||||
mcp__ourseo__crawl_website(url="<competitor>", max_pages=50)
|
||||
```
|
||||
|
||||
**OurSEO CLI (Korean batch):**
|
||||
```bash
|
||||
our serp ranked-keywords <competitor.com> --location 2410 --limit 100
|
||||
our serp competitors <target.com> --location 2410
|
||||
our research naver serp "<keyword>"
|
||||
our research naver keywords ideas "<keyword>"
|
||||
```
|
||||
|
||||
**Korean platform presence (WebSearch):**
|
||||
```
|
||||
WebSearch: blog.naver.com "<competitor brand>"
|
||||
WebSearch: cafe.naver.com "<competitor brand>"
|
||||
WebSearch: tistory.com "<competitor brand>"
|
||||
```
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future runs can compare like-for-like.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Competitor Profiling
|
||||
1. Accept target URL/domain
|
||||
2. Auto-discover competitors via our-seo-agent CLI or use provided list
|
||||
3. Build profile card for target and each competitor (DR, traffic, keywords, backlinks, content)
|
||||
4. Analyze keyword overlap between target and each competitor
|
||||
5. Build multi-dimensional comparison matrix
|
||||
6. Score competitive threats (0-100)
|
||||
7. Determine market position (leader/challenger/follower/niche)
|
||||
8. If Korean market: check Naver Blog/Cafe presence
|
||||
|
||||
### Competitive Monitoring
|
||||
1. Accept target, competitors, and monitoring period
|
||||
2. Fetch traffic trend history for all domains
|
||||
3. Fetch DR trend history for all domains
|
||||
4. Track keyword movement (new/lost keywords)
|
||||
5. Compare content publication velocity
|
||||
6. Generate alerts for significant changes (>20% traffic, DR jump, keyword surge)
|
||||
7. Estimate market share within competitive set
|
||||
|
||||
## Output Format
|
||||
|
||||
### Profiling Report
|
||||
```markdown
|
||||
## Competitor Intelligence Report: [domain]
|
||||
|
||||
### Target Profile
|
||||
- Domain Rating: [DR]
|
||||
- Organic Traffic: [traffic]
|
||||
- Keywords: [count]
|
||||
- Referring Domains: [count]
|
||||
|
||||
### Competitors (by threat score)
|
||||
1. **[competitor.com]** - Threat: [score]/100
|
||||
- DR: [value] | Traffic: [value] | Keywords: [value]
|
||||
- Keyword Overlap: [shared] shared, [gap] gap
|
||||
- Strengths: [list]
|
||||
- Weaknesses: [list]
|
||||
|
||||
### Comparison Matrix
|
||||
| Dimension | Target | Comp1 | Comp2 |
|
||||
|-----------|--------|-------|-------|
|
||||
|
||||
### Market Position: [leader/challenger/follower/niche]
|
||||
```
|
||||
|
||||
### Monitoring Report
|
||||
```markdown
|
||||
## Competitive Monitoring Report: [domain]
|
||||
### Period: [N] days
|
||||
|
||||
### Alerts
|
||||
- [severity] [message]
|
||||
|
||||
### Traffic Trends
|
||||
| Domain | Direction | Growth | Current |
|
||||
|
||||
### Keyword Movements
|
||||
| Domain | New | Lost | Net |
|
||||
|
||||
### Market Share
|
||||
| Domain | Traffic% | Overall% |
|
||||
```
|
||||
|
||||
## Threat Scoring Methodology
|
||||
|
||||
| Factor | Weight | Scale |
|
||||
|--------|--------|-------|
|
||||
| DR Gap | 20% | -30 to +30 mapped to 0-100 |
|
||||
| Traffic Ratio | 30% | 0x to 2x+ mapped to 0-100 |
|
||||
| Keyword Overlap | 25% | 0-50%+ mapped to 0-100 |
|
||||
| Gap Keywords | 25% | Ratio to target keywords |
|
||||
|
||||
## Alert Thresholds
|
||||
|
||||
| Alert Type | Threshold | Severity |
|
||||
|------------|-----------|----------|
|
||||
| Traffic change | >20% | warning; >50% critical |
|
||||
| DR change | >3 points | warning; >5 critical |
|
||||
| Keyword surge | >15% growth | warning |
|
||||
| Content burst | >2x avg velocity | info |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Data freshness depends on source and collection method
|
||||
- Keyword overlap limited to top 1,000 keywords per domain
|
||||
- Content velocity based on page index data (not real-time crawl)
|
||||
- Naver presence detection is heuristic-based
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category ("Competitor Intelligence"), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: COMP-YYYYMMDD-NNN
|
||||
164
custom-skills/32-notion-writer/SKILL.md
Normal file
164
custom-skills/32-notion-writer/SKILL.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: notion-writer
|
||||
description: |
|
||||
Markdown to Notion page writer with database row creation support.
|
||||
Triggers: write to Notion, export to Notion, push content, create Notion page.
|
||||
---
|
||||
|
||||
# Notion Writer Skill
|
||||
|
||||
Push markdown content to Notion pages or databases via Claude Code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python virtual environment at `~/Project/our-claude-skills/custom-skills/32-notion-writer/code/scripts/venv`
|
||||
- Notion integration token (preferred: stored in 1Password — see [Credential handling](#credential-handling) below)
|
||||
- Target pages/databases must be shared with the integration in Notion (Database/Page → ⋯ → Connections → add integration)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills/custom-skills/32-notion-writer/code/scripts
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
## Credential handling
|
||||
|
||||
**Do NOT store the Notion API key in a `.env` file.** A long-lived plaintext secret on disk is unnecessary risk — Notion integration tokens grant write access to every page/database the integration is connected to.
|
||||
|
||||
### Preferred: fetch from 1Password at runtime
|
||||
|
||||
Store the token once in 1Password (Item: `Notion - Claude Agent`, Vault: `Development`, Field: `api-key`), then fetch on each invocation. The token lives only in process memory — never on disk, never in shell history.
|
||||
|
||||
```bash
|
||||
# One-shot push — token fetched + used + discarded in a single shell line
|
||||
NOTION_API_KEY="$(op read 'op://Development/Notion - Claude Agent/api-key')" \
|
||||
python notion_writer.py --test
|
||||
```
|
||||
|
||||
For repeated use in the same session, scope the variable to a subshell so it leaves no trace:
|
||||
|
||||
```bash
|
||||
(
|
||||
export NOTION_API_KEY="$(op read 'op://Development/Notion - Claude Agent/api-key')"
|
||||
python notion_writer.py --database "$DB_URL" --title "Foo" --file foo.md
|
||||
python notion_writer.py --database "$DB_URL" --title "Bar" --file bar.md
|
||||
)
|
||||
# NOTION_API_KEY is gone from the parent shell
|
||||
```
|
||||
|
||||
### Field name conventions in 1Password
|
||||
|
||||
When you create the Notion integration item, use these field names:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `api-key` (CONCEALED) | The integration token (`ntn_...` for current API, `secret_...` for legacy) |
|
||||
| `username` | Integration display name (e.g. "Claude Agent") |
|
||||
| `hostname` | `https://api.notion.com` |
|
||||
| `notesPlain` | Vault for which workspace + which databases this integration is connected to |
|
||||
|
||||
The script reads `NOTION_API_KEY` from the environment — it does not call `op` itself. This keeps the script free of `op`-specific dependencies and lets the same script work in CI/CD environments that inject the secret differently (GitHub Actions secrets, Vault, etc.).
|
||||
|
||||
### Fallback: `.env` file (discouraged)
|
||||
|
||||
If you must use a file (e.g. running on a host without 1Password CLI), place it at `~/Project/our-claude-skills/custom-skills/32-notion-writer/code/.env` with:
|
||||
|
||||
```
|
||||
NOTION_API_KEY=ntn_xxxxxxxx_paste_your_token_here
|
||||
```
|
||||
|
||||
Set the file to mode `600` (`chmod 600 .env`) and never commit it. Add `.env` to `.gitignore` if not already.
|
||||
|
||||
### What NEVER to do
|
||||
|
||||
- ❌ Echo the token to stdout: `echo "$NOTION_API_KEY"` (lands in terminal scrollback + shell history if the var was inline)
|
||||
- ❌ Pass the token as a CLI argument: `--api-key "ntn_..."` (visible in `ps`, history, telemetry)
|
||||
- ❌ Commit a `.env` file to git, even briefly (commit history is forever)
|
||||
- ❌ Paste the token into chat / issues / PRs — assume any pasted secret is compromised within minutes
|
||||
|
||||
### Token rotation
|
||||
|
||||
If a token is exposed (or you reasonably suspect it might be):
|
||||
|
||||
1. Go to <https://www.notion.so/my-integrations> → open the integration → **Reset** the secret
|
||||
2. Update the 1Password item's `api-key` field with the new token
|
||||
3. Verify with `NOTION_API_KEY="$(op read ...)" python notion_writer.py --test`
|
||||
4. The old token is invalidated immediately — any leftover scripts using it will fail loudly
|
||||
|
||||
## Commands
|
||||
|
||||
### Test Connection
|
||||
```bash
|
||||
python notion_writer.py --test
|
||||
```
|
||||
|
||||
### List Accessible Content
|
||||
```bash
|
||||
python notion_writer.py --list
|
||||
python notion_writer.py --list --filter pages
|
||||
python notion_writer.py --list --filter databases
|
||||
```
|
||||
|
||||
### Get Page/Database Info
|
||||
```bash
|
||||
python notion_writer.py -p PAGE_URL --info
|
||||
python notion_writer.py -d DATABASE_URL --info
|
||||
```
|
||||
|
||||
### Write to Page
|
||||
```bash
|
||||
# Append content
|
||||
python notion_writer.py -p PAGE_URL -f content.md
|
||||
|
||||
# Replace content
|
||||
python notion_writer.py -p PAGE_URL -f content.md --replace
|
||||
|
||||
# From stdin
|
||||
cat report.md | python notion_writer.py -p PAGE_URL --stdin
|
||||
```
|
||||
|
||||
### Create Database Row
|
||||
```bash
|
||||
python notion_writer.py -d DATABASE_URL -t "Entry Title" -f content.md
|
||||
```
|
||||
|
||||
## Supported Markdown
|
||||
|
||||
| Markdown | Notion Block |
|
||||
|----------|--------------|
|
||||
| `# Heading` | Heading 1 |
|
||||
| `## Heading` | Heading 2 |
|
||||
| `### Heading` | Heading 3 |
|
||||
| `- item` | Bulleted list |
|
||||
| `1. item` | Numbered list |
|
||||
| `- [ ] task` | To-do (unchecked) |
|
||||
| `- [x] task` | To-do (checked) |
|
||||
| `> quote` | Quote |
|
||||
| `` ```code``` `` | Code block |
|
||||
| `---` | Divider |
|
||||
| Paragraphs | Paragraph |
|
||||
|
||||
### Engines and image uploads
|
||||
|
||||
Two write engines via `--engine {blocks,markdown}` (default: `blocks`).
|
||||
|
||||
The **blocks engine** (default) converts markdown locally to Notion block objects. Local images (``) are auto-uploaded via the `ntn` CLI and embedded at their original position in the page. Requires `ntn` installed and `ntn login`.
|
||||
|
||||
The **markdown engine** (`--engine markdown`) posts the document through Notion's native enhanced-markdown API (`Notion-Version: 2026-03-11`, set automatically; override with `--notion-version`). The skill's authoring dialect — GitHub alerts (`[!NOTE]`), Pandoc columns (`::: columns`), `<details>` toggles, and `@[mention]` — is auto-translated before posting. Note: local images are appended at the end of the page rather than inline with this engine; use `--engine blocks` when image position matters. Pass `--allow-deleting-content` when `--replace` needs to remove child pages or databases.
|
||||
|
||||
```bash
|
||||
# Markdown engine — create a DB row from a doc with callouts or columns
|
||||
python notion_writer.py -d DB_URL -t "Notes" --engine markdown -f notes.md
|
||||
```
|
||||
|
||||
## Workflow Example
|
||||
|
||||
Integrate with Jamie YouTube Manager to log video info:
|
||||
```bash
|
||||
# Check video and save to markdown
|
||||
python jamie_youtube_api_test.py VIDEO_URL
|
||||
|
||||
# Write to Notion
|
||||
python notion_writer.py -p LOG_PAGE_URL -f output/video_status.md
|
||||
```
|
||||
@@ -189,6 +189,52 @@ print("Hello")
|
||||
|
||||
Notion's URL validator requires absolute URLs for link annotations. The parser converts TOC-style anchor links to bold to preserve navigation intent and silently strips relative paths.
|
||||
|
||||
### File uploads
|
||||
|
||||
Standalone local-image lines (``) are auto-uploaded to Notion and embedded as `file_upload` image blocks. Remote images (``) are left as external links unchanged.
|
||||
|
||||
**Requirements:**
|
||||
- `ntn` CLI installed: `curl -fsSL https://ntn.dev | bash`
|
||||
|
||||
Uploads run as the **same integration** that writes the page (`NOTION_API_KEY`). The script injects `NOTION_API_TOKEN=$NOTION_API_KEY` into every `ntn` subprocess, so the file upload and the page share one identity — no separate `ntn login` or workspace matching is needed. `ntn` only needs to be installed, not logged in.
|
||||
|
||||
### Engines
|
||||
|
||||
Two write engines, selected with `--engine {blocks,markdown}`. Default is `blocks`.
|
||||
|
||||
| Engine | Flag | When to use |
|
||||
|--------|------|-------------|
|
||||
| **blocks** (default) | `--engine blocks` | General use; images embed at their exact position; full table + container support |
|
||||
| **markdown** | `--engine markdown` | Richer Notion-native formatting via enhanced-markdown endpoints |
|
||||
|
||||
**blocks engine** converts markdown to Notion block objects locally (via `markdown_to_notion_blocks`). Local images are uploaded via `ntn` and embedded at their exact position in the document.
|
||||
|
||||
**markdown engine** posts the document through Notion's native enhanced-markdown endpoints (`Notion-Version: 2026-03-11`, set automatically). The skill's authoring dialect is auto-translated before posting:
|
||||
|
||||
| Skill dialect | Translated to |
|
||||
|---------------|---------------|
|
||||
| `> [!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / `[!CAUTION]` | `<callout icon="..." color="...">` |
|
||||
| `::: columns` / `::: column` / `:::` | `<columns><column>...</column></columns>` |
|
||||
| `<details><summary>...</summary>` | `<details>` (Notion toggle) |
|
||||
| `@[Title](id-or-url)` | `<mention-page url="...">` |
|
||||
|
||||
**Local image limitation with `--engine markdown`**: local images cannot be placed inline via the enhanced-markdown API. They are appended at the end of the page as a second write pass. Use `--engine blocks` when image placement matters.
|
||||
|
||||
**`--notion-version`**: Override the API version for the markdown engine (default: `2026-03-11`).
|
||||
|
||||
**`--allow-deleting-content`**: Required when `--replace` needs to delete child pages or databases under the target page; the Notion API refuses such deletions unless this flag is present. Applies only to the markdown engine's `--replace` path (`--engine markdown --replace`); has no effect with `--engine blocks`.
|
||||
|
||||
```bash
|
||||
# Markdown engine, create a row from a doc with callouts/columns
|
||||
python notion_writer.py -d DB_URL -t "Notes" --engine markdown -f notes.md
|
||||
|
||||
# Blocks engine with a local image (auto-uploaded via ntn)
|
||||
python notion_writer.py -p PAGE_URL -f post.md # post.md contains 
|
||||
|
||||
# Markdown engine, replace page allowing child deletion
|
||||
python notion_writer.py -p PAGE_URL -f doc.md --replace --engine markdown --allow-deleting-content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
@@ -398,9 +444,10 @@ python notion_writer.py -d DB_URL -t "Title" --upsert-by "Name" -f content.md
|
||||
|
||||
---
|
||||
|
||||
*Version 1.2.0 | Claude Code | 2026-04-27*
|
||||
*Version 1.3.0 | Claude Code | 2026-06-27*
|
||||
|
||||
Changelog:
|
||||
- 1.3.0 — Local file/image uploads via the `ntn` CLI (`` → file_upload image blocks). New `--engine markdown` path writing through Notion's native enhanced-markdown endpoints with a dialect translator. Added `--notion-version` and `--allow-deleting-content`.
|
||||
- 1.2.0 — Extended block coverage: GitHub-alert callouts, HTML5 `<details>` toggles, Pandoc `::: columns` fenced div, inline `@[Title](id-or-url)` page mentions. Parser made reentrant to support full recursion inside container blocks.
|
||||
- 1.1.0 — Migrated to Notion API 2025-09-03 (multi-source databases). Added `--properties` JSON flag, `--upsert-by` for idempotency, anchor-link parser fix, friendlier API error messages.
|
||||
- 1.0.0 — Initial release with markdown→Notion block conversion.
|
||||
|
||||
@@ -16,8 +16,11 @@ from notion_client import Client
|
||||
from notion_client.errors import APIErrorCode, APIResponseError
|
||||
|
||||
|
||||
def make_client(api_key: str) -> Client:
|
||||
"""Build a sync Notion client on the SDK default API version (2025-09-03+)."""
|
||||
def make_client(api_key: str, notion_version: str = None) -> Client:
|
||||
"""Build a sync Notion client. Pass notion_version to override the SDK
|
||||
default (needed: 2026-03-11 for the markdown content endpoints)."""
|
||||
if notion_version:
|
||||
return Client(auth=api_key, notion_version=notion_version)
|
||||
return Client(auth=api_key)
|
||||
|
||||
|
||||
@@ -210,7 +213,10 @@ def explain_api_error(exc: APIResponseError, context: str = "") -> str:
|
||||
"https://www.notion.so/my-integrations."
|
||||
)
|
||||
if code == APIErrorCode.ValidationError:
|
||||
return f"Validation error{suffix}: {exc.body.get('message', str(exc))}"
|
||||
msg = exc.body.get('message', str(exc))
|
||||
if 'delet' in msg.lower():
|
||||
msg += " — re-run with --allow-deleting-content to permit this."
|
||||
return f"Validation error{suffix}: {msg}"
|
||||
if code == APIErrorCode.RateLimited:
|
||||
return f"Rate limited{suffix}. Back off and retry."
|
||||
return f"Notion API error [{code}]{suffix}: {exc}"
|
||||
@@ -253,3 +259,33 @@ def find_existing_page(
|
||||
)
|
||||
results = response.get("results") or []
|
||||
return results[0] if results else None
|
||||
|
||||
|
||||
def create_page_markdown(client, parent, properties, markdown):
|
||||
"""POST /v1/pages with the enhanced-markdown body param.
|
||||
`markdown` is only included in the body when non-empty to avoid sending
|
||||
a blank markdown field when no --file/--stdin was supplied."""
|
||||
body: Dict[str, Any] = {"parent": parent, "properties": properties}
|
||||
if markdown:
|
||||
body["markdown"] = markdown
|
||||
return client.request(path="pages", method="POST", body=body)
|
||||
|
||||
|
||||
def append_markdown(client, page_id, markdown):
|
||||
"""PATCH /v1/pages/:id/markdown — append at end (insert_content)."""
|
||||
return client.request(
|
||||
path=f"pages/{page_id}/markdown", method="PATCH",
|
||||
body={"type": "insert_content",
|
||||
"insert_content": {"content": markdown,
|
||||
"position": {"type": "end"}}},
|
||||
)
|
||||
|
||||
|
||||
def replace_markdown(client, page_id, markdown, allow_deleting=False):
|
||||
"""PATCH /v1/pages/:id/markdown — replace all content."""
|
||||
return client.request(
|
||||
path=f"pages/{page_id}/markdown", method="PATCH",
|
||||
body={"type": "replace_content",
|
||||
"replace_content": {"new_str": markdown,
|
||||
"allow_deleting_content": allow_deleting}},
|
||||
)
|
||||
|
||||
135
custom-skills/32-notion-writer/code/scripts/md_translate.py
Normal file
135
custom-skills/32-notion-writer/code/scripts/md_translate.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Translate the notion-writer markdown dialect into Notion enhanced
|
||||
markdown for the markdown write engine. Pure functions, no I/O."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
# NOTE: `notion_writer` is imported lazily inside _translate_mentions to avoid
|
||||
# a circular import (notion_writer imports this module at its top level).
|
||||
|
||||
# Skill alert type -> (emoji, Notion enhanced-markdown background color)
|
||||
CALLOUT_MAP = {
|
||||
"NOTE": ("ℹ️", "blue_bg"),
|
||||
"TIP": ("💡", "green_bg"),
|
||||
"IMPORTANT": ("☝️", "purple_bg"),
|
||||
"WARNING": ("⚠️", "yellow_bg"),
|
||||
"CAUTION": ("🚨", "red_bg"),
|
||||
}
|
||||
_ALERT_RE = re.compile(r'^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$')
|
||||
_MENTION_RE = re.compile(r'@\[([^\]]+)\]\(([^)\s]+)\)')
|
||||
|
||||
|
||||
def _translate_mentions(text: str) -> str:
|
||||
from notion_writer import extract_notion_id, format_id_with_dashes # lazy: breaks import cycle
|
||||
|
||||
def repl(m: "re.Match") -> str:
|
||||
title, target = m.group(1), m.group(2)
|
||||
page_id = extract_notion_id(target)
|
||||
if page_id:
|
||||
return (f'<mention-page url="{format_id_with_dashes(page_id)}">'
|
||||
f'{title}</mention-page>')
|
||||
return f"@{title}"
|
||||
return _MENTION_RE.sub(repl, text)
|
||||
|
||||
|
||||
def _indent(lines: List[str]) -> List[str]:
|
||||
return ["\t" + ln if ln.strip() else ln for ln in lines]
|
||||
|
||||
|
||||
def translate(content: str) -> str:
|
||||
lines = content.split("\n")
|
||||
out: List[str] = []
|
||||
i = 0
|
||||
in_fence = False
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Fence tracking: pass through unchanged, toggle state
|
||||
if line.strip().startswith("```"):
|
||||
in_fence = not in_fence
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Inside fence: pass through verbatim, no transformation
|
||||
if in_fence:
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Callout: > [!TYPE] then contiguous > body lines
|
||||
if line.lstrip().startswith(">"):
|
||||
body_first = line.lstrip()[1:].strip()
|
||||
alert = _ALERT_RE.match(body_first)
|
||||
if alert:
|
||||
emoji, color = CALLOUT_MAP[alert.group(1)]
|
||||
i += 1
|
||||
body: List[str] = []
|
||||
while i < len(lines) and lines[i].lstrip().startswith(">"):
|
||||
body.append(lines[i].lstrip()[1:].lstrip())
|
||||
i += 1
|
||||
out.append(f'<callout icon="{emoji}" color="{color}">')
|
||||
out.extend(_indent([_translate_mentions(b) for b in body]))
|
||||
out.append("</callout>")
|
||||
continue
|
||||
|
||||
# Columns: ::: columns / ::: column / :::
|
||||
# State machine: "::: column" opens a column; ":::" closes a column
|
||||
# when one is open, otherwise closes the wrapper. The Pandoc layout
|
||||
# emits N "::: column" opens, N ":::" column-closes, then one final
|
||||
# ":::" wrapper-close.
|
||||
if line.strip() == "::: columns":
|
||||
i += 1
|
||||
cols: List[List[str]] = []
|
||||
cur: List[str] = None
|
||||
while i < len(lines):
|
||||
s = lines[i].strip()
|
||||
if s == "::: column":
|
||||
if cur is not None:
|
||||
cols.append(cur)
|
||||
cur = []
|
||||
i += 1
|
||||
elif s == ":::":
|
||||
if cur is not None: # close the open column
|
||||
cols.append(cur)
|
||||
cur = None
|
||||
i += 1
|
||||
else: # close the wrapper
|
||||
i += 1
|
||||
break
|
||||
else:
|
||||
if cur is not None:
|
||||
cur.append(lines[i])
|
||||
i += 1
|
||||
out.append("<columns>")
|
||||
for col in cols:
|
||||
out.append("\t<column>")
|
||||
out.extend(_indent(_indent(
|
||||
[_translate_mentions(c) for c in col])))
|
||||
out.append("\t</column>")
|
||||
out.append("</columns>")
|
||||
continue
|
||||
|
||||
# Toggle: <details><summary>..</summary> body </details>
|
||||
if line.strip() == "<details>":
|
||||
out.append("<details>")
|
||||
i += 1
|
||||
if i < len(lines) and lines[i].lstrip().startswith("<summary>"):
|
||||
out.append(lines[i].strip())
|
||||
i += 1
|
||||
body = []
|
||||
while i < len(lines) and lines[i].strip() != "</details>":
|
||||
body.append(_translate_mentions(lines[i]))
|
||||
i += 1
|
||||
out.extend(_indent(body))
|
||||
out.append("</details>")
|
||||
if i < len(lines): # skip closing </details>
|
||||
i += 1
|
||||
continue
|
||||
|
||||
out.append(_translate_mentions(line))
|
||||
i += 1
|
||||
return "\n".join(out)
|
||||
@@ -17,6 +17,11 @@ from notion_client import Client
|
||||
from notion_client.errors import APIResponseError
|
||||
|
||||
import _notion_compat as compat
|
||||
import ntn_files
|
||||
from ntn_files import NtnUploadError
|
||||
import md_translate
|
||||
|
||||
MARKDOWN_NOTION_VERSION = "2026-03-11"
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(Path(__file__).parent / '.env')
|
||||
@@ -56,6 +61,7 @@ def format_id_with_dashes(raw_id: str) -> str:
|
||||
|
||||
|
||||
TABLE_SEPARATOR_RE = re.compile(r'^\s*\|?(\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$')
|
||||
IMAGE_RE = re.compile(r'^\s*!\[([^\]]*)\]\(([^)\s]+)\)\s*$')
|
||||
|
||||
|
||||
def _is_table_row(line: str) -> bool:
|
||||
@@ -210,6 +216,13 @@ def _parse_lines(lines: List[str]) -> List[Dict[str, Any]]:
|
||||
blocks.append(create_column_list_block(column_blocks))
|
||||
continue
|
||||
|
||||
image_match = IMAGE_RE.match(line)
|
||||
if image_match:
|
||||
blocks.append(create_image_block(
|
||||
image_match.group(1), image_match.group(2)))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if _is_table_row(line) and i + 1 < len(lines) and TABLE_SEPARATOR_RE.match(lines[i + 1]):
|
||||
header_cells = _split_table_row(line)
|
||||
i += 2
|
||||
@@ -507,6 +520,55 @@ def create_divider_block() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def create_image_block(alt: str, target: str) -> Dict[str, Any]:
|
||||
"""Image block in external shape. Local targets are converted to
|
||||
file_upload shape later by ntn_files.materialize_local_media."""
|
||||
block: Dict[str, Any] = {
|
||||
"object": "block",
|
||||
"type": "image",
|
||||
"image": {"type": "external", "external": {"url": target}},
|
||||
}
|
||||
if alt:
|
||||
block["image"]["caption"] = parse_rich_text(alt)
|
||||
return block
|
||||
|
||||
|
||||
def _content_has_local_images(content: str) -> bool:
|
||||
in_fence = False
|
||||
for line in content.split('\n'):
|
||||
if line.strip().startswith('```'):
|
||||
in_fence = not in_fence
|
||||
continue # fence delimiter lines are never image lines
|
||||
if in_fence:
|
||||
continue
|
||||
m = IMAGE_RE.match(line)
|
||||
if m and not m.group(2).startswith(('http://', 'https://')):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_local_images(content: str):
|
||||
"""Remove standalone LOCAL image lines; keep remote ones inline.
|
||||
Returns (content_without_local_images, [(alt, target), ...]).
|
||||
Lines inside fenced code blocks are passed through unchanged."""
|
||||
kept, imgs = [], []
|
||||
in_fence = False
|
||||
for line in content.split('\n'):
|
||||
if line.strip().startswith('```'):
|
||||
in_fence = not in_fence
|
||||
kept.append(line)
|
||||
continue
|
||||
if in_fence:
|
||||
kept.append(line)
|
||||
continue
|
||||
m = IMAGE_RE.match(line)
|
||||
if m and not m.group(2).startswith(('http://', 'https://')):
|
||||
imgs.append((m.group(1), m.group(2)))
|
||||
continue
|
||||
kept.append(line)
|
||||
return "\n".join(kept), imgs
|
||||
|
||||
|
||||
def create_table_block(header_cells: List[str], body_rows: List[List[str]]) -> Dict[str, Any]:
|
||||
"""Build a Notion `table` block with header + body rows.
|
||||
|
||||
@@ -746,21 +808,6 @@ def update_page_properties(notion: Client, page_id: str, properties: Dict) -> bo
|
||||
return False
|
||||
|
||||
|
||||
def write_to_page(notion: Client, page_id: str, markdown_content: str, mode: str = 'append') -> bool:
|
||||
"""Write markdown content to a Notion page."""
|
||||
blocks = markdown_to_notion_blocks(markdown_content)
|
||||
|
||||
if not blocks:
|
||||
print("No content to write")
|
||||
return False
|
||||
|
||||
if mode == 'replace':
|
||||
if not clear_page_content(notion, page_id):
|
||||
return False
|
||||
|
||||
return append_to_page(notion, page_id, blocks)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Push markdown content to Notion pages or databases',
|
||||
@@ -796,9 +843,28 @@ Examples:
|
||||
parser.add_argument('--list', '-l', nargs='?', const='all', choices=['all', 'pages', 'databases'],
|
||||
help='List accessible pages and/or databases (default: all)')
|
||||
parser.add_argument('--info', action='store_true', help='Show page/database info')
|
||||
parser.add_argument('--engine', choices=['blocks', 'markdown'],
|
||||
default='blocks',
|
||||
help='Write engine: blocks (default) or markdown')
|
||||
parser.add_argument('--notion-version', dest='notion_version',
|
||||
help='Override Notion API version')
|
||||
parser.add_argument('--allow-deleting-content', dest='allow_deleting',
|
||||
action='store_true',
|
||||
help='Markdown replace may delete child pages/dbs')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
_main_body(parser, args)
|
||||
except NtnUploadError as e:
|
||||
print(f"Error: {e}")
|
||||
if e.stderr:
|
||||
print(e.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _main_body(parser, args):
|
||||
"""Body of main() after argparse; separated so NtnUploadError can be caught cleanly."""
|
||||
if not NOTION_TOKEN:
|
||||
print("Error: NOTION_API_KEY not set in environment.")
|
||||
print("Preferred: fetch from 1Password at runtime —")
|
||||
@@ -885,23 +951,64 @@ Examples:
|
||||
sys.exit(1)
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
|
||||
base_dir = Path(args.file).parent if args.file else Path.cwd()
|
||||
|
||||
def _md_client():
|
||||
version = args.notion_version or MARKDOWN_NOTION_VERSION
|
||||
return compat.make_client(NOTION_TOKEN, notion_version=version)
|
||||
|
||||
def _upload_blocks_for(images):
|
||||
"""Build + materialize image blocks for two-phase markdown writes."""
|
||||
blocks = [create_image_block(alt, target) for alt, target in images]
|
||||
return ntn_files.materialize_local_media(blocks, base_dir, ntn_files.upload)
|
||||
|
||||
# Write to page
|
||||
if args.page:
|
||||
if not content:
|
||||
print("Error: No content provided. Use --file or --stdin")
|
||||
sys.exit(1)
|
||||
|
||||
page_id = extract_notion_id(args.page)
|
||||
if not page_id:
|
||||
print(f"Error: Invalid Notion page URL/ID: {args.page}")
|
||||
sys.exit(1)
|
||||
formatted_id = format_id_with_dashes(page_id)
|
||||
|
||||
mode = 'replace' if args.replace else 'append'
|
||||
print(f"{'Replacing' if mode == 'replace' else 'Appending'} content to page...")
|
||||
if args.engine == 'markdown':
|
||||
if _content_has_local_images(content):
|
||||
ntn_files.preflight()
|
||||
md_body, local_imgs = extract_local_images(content)
|
||||
md_body = md_translate.translate(md_body)
|
||||
mc = _md_client()
|
||||
try:
|
||||
if args.replace:
|
||||
compat.replace_markdown(mc, formatted_id, md_body,
|
||||
allow_deleting=args.allow_deleting)
|
||||
else:
|
||||
compat.append_markdown(mc, formatted_id, md_body)
|
||||
except APIResponseError as exc:
|
||||
print(f"Error: {compat.explain_api_error(exc, formatted_id)}")
|
||||
sys.exit(1)
|
||||
if local_imgs and not append_to_page(notion, page_id, _upload_blocks_for(local_imgs)):
|
||||
print("❌ Text was written but image append failed")
|
||||
sys.exit(1)
|
||||
print("✅ Successfully wrote content to page (markdown engine)")
|
||||
print(f" https://notion.so/{formatted_id.replace('-', '')}")
|
||||
return
|
||||
|
||||
if write_to_page(notion, page_id, content, mode):
|
||||
print(f"✅ Successfully wrote content to page")
|
||||
formatted_id = format_id_with_dashes(page_id)
|
||||
# blocks engine (default)
|
||||
blocks = markdown_to_notion_blocks(content)
|
||||
if _content_has_local_images(content):
|
||||
ntn_files.preflight()
|
||||
blocks = ntn_files.materialize_local_media(blocks, base_dir, ntn_files.upload)
|
||||
if not blocks:
|
||||
print("No content to write")
|
||||
sys.exit(1)
|
||||
print(f"{'Replacing' if args.replace else 'Appending'} content to page...")
|
||||
if args.replace and not clear_page_content(notion, page_id):
|
||||
print("❌ Failed to write content")
|
||||
sys.exit(1)
|
||||
if append_to_page(notion, page_id, blocks):
|
||||
print("✅ Successfully wrote content to page")
|
||||
print(f" https://notion.so/{formatted_id.replace('-', '')}")
|
||||
else:
|
||||
print("❌ Failed to write content")
|
||||
@@ -960,6 +1067,8 @@ Examples:
|
||||
|
||||
content_blocks = markdown_to_notion_blocks(content) if content else None
|
||||
|
||||
existing = None
|
||||
|
||||
# Upsert path: look for existing row by the named property
|
||||
if args.upsert_by:
|
||||
if args.upsert_by not in schema_props:
|
||||
@@ -980,19 +1089,52 @@ Examples:
|
||||
print(f"Error during upsert lookup: {compat.explain_api_error(exc)}")
|
||||
sys.exit(1)
|
||||
|
||||
if existing:
|
||||
page_id = existing['id']
|
||||
print(f"Updating existing row (matched on {args.upsert_by}={lookup_value!r})...")
|
||||
if not update_page_properties(notion, page_id, properties):
|
||||
if args.engine == 'markdown':
|
||||
md_content = content or ""
|
||||
if _content_has_local_images(md_content):
|
||||
ntn_files.preflight()
|
||||
md_body, local_imgs = extract_local_images(md_content)
|
||||
md_body = md_translate.translate(md_body)
|
||||
mc = _md_client()
|
||||
try:
|
||||
if args.upsert_by and existing:
|
||||
compat.replace_markdown(mc, existing['id'], md_body,
|
||||
allow_deleting=args.allow_deleting)
|
||||
if not update_page_properties(notion, existing['id'], properties):
|
||||
sys.exit(1)
|
||||
new_id = existing['id']
|
||||
else:
|
||||
parent = compat.build_data_source_parent(data_source_id)
|
||||
result = compat.create_page_markdown(mc, parent, properties, md_body)
|
||||
new_id = result['id']
|
||||
except APIResponseError as exc:
|
||||
print(f"Error: {compat.explain_api_error(exc)}")
|
||||
sys.exit(1)
|
||||
if local_imgs and not append_to_page(notion, new_id, _upload_blocks_for(local_imgs)):
|
||||
print("❌ Text was written but image append failed")
|
||||
sys.exit(1)
|
||||
print("✅ Successfully wrote database row (markdown engine)")
|
||||
print(f" https://notion.so/{new_id.replace('-', '')}")
|
||||
return
|
||||
|
||||
if content_blocks and _content_has_local_images(content or ""):
|
||||
ntn_files.preflight()
|
||||
content_blocks = ntn_files.materialize_local_media(
|
||||
content_blocks, base_dir, ntn_files.upload)
|
||||
|
||||
if existing:
|
||||
page_id = existing['id']
|
||||
print(f"Updating existing row (matched on {args.upsert_by}={lookup_value!r})...")
|
||||
if not update_page_properties(notion, page_id, properties):
|
||||
sys.exit(1)
|
||||
if content_blocks:
|
||||
if not clear_page_content(notion, page_id):
|
||||
sys.exit(1)
|
||||
if content_blocks:
|
||||
if not clear_page_content(notion, page_id):
|
||||
sys.exit(1)
|
||||
if not append_to_page(notion, page_id, content_blocks):
|
||||
sys.exit(1)
|
||||
print(f"✅ Successfully updated database row")
|
||||
print(f" https://notion.so/{page_id.replace('-', '')}")
|
||||
return
|
||||
if not append_to_page(notion, page_id, content_blocks):
|
||||
sys.exit(1)
|
||||
print(f"✅ Successfully updated database row")
|
||||
print(f" https://notion.so/{page_id.replace('-', '')}")
|
||||
return
|
||||
|
||||
print(f"Creating database row...")
|
||||
row_id = create_database_row(notion, data_source_id, properties, content_blocks)
|
||||
|
||||
154
custom-skills/32-notion-writer/code/scripts/ntn_files.py
Normal file
154
custom-skills/32-notion-writer/code/scripts/ntn_files.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local file uploads to Notion via the `ntn` CLI.
|
||||
|
||||
Owns all subprocess I/O for the skill. The CLI handles the full File Upload
|
||||
lifecycle (create -> send bytes -> complete) and prints the upload ID.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
|
||||
_WORKSPACE: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
def _ntn_env():
|
||||
"""Env for ntn subprocesses: force ntn to authenticate as the SAME
|
||||
integration the script uses (NOTION_API_KEY/NOTION_TOKEN), so an uploaded
|
||||
file and the page that references it share one identity. Notion scopes
|
||||
file uploads to the creating integration, so a mismatch makes the upload
|
||||
un-attachable."""
|
||||
env = dict(os.environ)
|
||||
token = os.environ.get("NOTION_API_KEY") or os.environ.get("NOTION_TOKEN")
|
||||
if token:
|
||||
env["NOTION_API_TOKEN"] = token
|
||||
return env
|
||||
|
||||
|
||||
class NtnUploadError(Exception):
|
||||
"""Raised when `ntn` is unavailable or a file upload fails."""
|
||||
|
||||
def __init__(self, message: str, path: str = "", stderr: str = ""):
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
def preflight() -> Dict[str, str]:
|
||||
"""Verify `ntn` is installed and logged in; return its target workspace.
|
||||
|
||||
Cached for the process. Raises NtnUploadError with an actionable hint
|
||||
on failure. Prints one informational line naming the workspace `ntn`
|
||||
targets (file uploads are workspace-scoped).
|
||||
"""
|
||||
global _WORKSPACE
|
||||
if _WORKSPACE is not None:
|
||||
return _WORKSPACE
|
||||
|
||||
if shutil.which("ntn") is None:
|
||||
raise NtnUploadError(
|
||||
"ntn CLI not found. Install it with: "
|
||||
"curl -fsSL https://ntn.dev | bash"
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
["ntn", "api", "v1/users/me"],
|
||||
capture_output=True, text=True, env=_ntn_env(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise NtnUploadError(
|
||||
"ntn is not logged in. Run: ntn login\n" + result.stderr.strip()
|
||||
)
|
||||
|
||||
try:
|
||||
me = json.loads(result.stdout)
|
||||
bot = me.get("bot", {})
|
||||
info = {
|
||||
"workspace_name": bot.get("workspace_name", "unknown"),
|
||||
"workspace_id": bot.get("workspace_id", "unknown"),
|
||||
}
|
||||
except (ValueError, AttributeError):
|
||||
info = {"workspace_name": "unknown", "workspace_id": "unknown"}
|
||||
|
||||
print(f'ntn -> workspace "{info["workspace_name"]}"', file=sys.stderr)
|
||||
_WORKSPACE = info
|
||||
return info
|
||||
|
||||
|
||||
def upload(path: Path) -> str:
|
||||
"""Upload a local file via `ntn files create --plain`; return its id."""
|
||||
path = Path(path)
|
||||
with open(path, "rb") as fh:
|
||||
result = subprocess.run(
|
||||
["ntn", "files", "create", "--plain"],
|
||||
stdin=fh, capture_output=True, text=True, env=_ntn_env(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise NtnUploadError(
|
||||
f"ntn upload failed for {path}", path=str(path),
|
||||
stderr=result.stderr.strip(),
|
||||
)
|
||||
if not result.stdout.strip():
|
||||
raise NtnUploadError(
|
||||
"ntn returned no output", path=str(path), stderr=result.stderr.strip()
|
||||
)
|
||||
first_line = result.stdout.strip().splitlines()[0]
|
||||
upload_id = first_line.split("\t")[0].strip()
|
||||
return upload_id
|
||||
|
||||
|
||||
_REMOTE_PREFIXES = ("http://", "https://")
|
||||
|
||||
|
||||
def _resolve_local(url: str, base_dir: Path) -> Optional[Path]:
|
||||
"""Return the resolved path for a local media url, or None if remote."""
|
||||
if url.startswith(_REMOTE_PREFIXES):
|
||||
return None
|
||||
p = Path(url)
|
||||
return p if p.is_absolute() else Path(base_dir) / p
|
||||
|
||||
|
||||
def materialize_local_media(
|
||||
blocks: List[Dict[str, Any]],
|
||||
base_dir: Path,
|
||||
upload_fn: Callable[[Path], str] = upload,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Walk blocks (and nested children); upload local image files and
|
||||
rewrite them to file_upload shape. Remote images are left as-is."""
|
||||
for block in blocks:
|
||||
if block.get("type") == "image":
|
||||
img = block["image"]
|
||||
if img.get("type") == "external":
|
||||
url = img.get("external", {}).get("url", "")
|
||||
resolved = _resolve_local(url, base_dir)
|
||||
if resolved is not None:
|
||||
if not resolved.is_file():
|
||||
raise NtnUploadError(
|
||||
f"image references missing file: {url}",
|
||||
path=str(resolved),
|
||||
)
|
||||
upload_id = upload_fn(resolved)
|
||||
caption = img.get("caption")
|
||||
new_img: Dict[str, Any] = {
|
||||
"type": "file_upload",
|
||||
"file_upload": {"id": upload_id},
|
||||
}
|
||||
if caption:
|
||||
new_img["caption"] = caption
|
||||
block["image"] = new_img
|
||||
# Recurse into any nested children list.
|
||||
btype = block.get("type")
|
||||
nested = block.get(btype, {}) if isinstance(block.get(btype), dict) else {}
|
||||
if isinstance(nested.get("children"), list):
|
||||
materialize_local_media(nested["children"], base_dir, upload_fn)
|
||||
if btype == "column_list":
|
||||
for col in nested.get("children", []):
|
||||
col_children = col.get("column", {}).get("children", [])
|
||||
materialize_local_media(col_children, base_dir, upload_fn)
|
||||
return blocks
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for markdown transport helpers — run with `python test_engine_routing.py`."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
import _notion_compat as compat
|
||||
import notion_writer
|
||||
|
||||
|
||||
def _fake_client():
|
||||
c = mock.MagicMock()
|
||||
c.request.return_value = {"object": "page", "id": "p1"}
|
||||
return c
|
||||
|
||||
|
||||
def test_create_page_markdown():
|
||||
c = _fake_client()
|
||||
parent = {"type": "data_source_id", "data_source_id": "ds1"}
|
||||
compat.create_page_markdown(c, parent, {"Name": {"title": []}}, "# Hi")
|
||||
_, kwargs = c.request.call_args
|
||||
assert kwargs["path"] == "pages"
|
||||
assert kwargs["method"] == "POST"
|
||||
assert kwargs["body"]["markdown"] == "# Hi"
|
||||
assert kwargs["body"]["parent"] == parent
|
||||
|
||||
|
||||
def test_append_markdown():
|
||||
c = _fake_client()
|
||||
compat.append_markdown(c, "page-123", "## More")
|
||||
_, kwargs = c.request.call_args
|
||||
assert kwargs["path"] == "pages/page-123/markdown"
|
||||
assert kwargs["method"] == "PATCH"
|
||||
assert kwargs["body"]["type"] == "insert_content"
|
||||
assert kwargs["body"]["insert_content"]["content"] == "## More"
|
||||
assert kwargs["body"]["insert_content"]["position"] == {"type": "end"}
|
||||
|
||||
|
||||
def test_replace_markdown():
|
||||
c = _fake_client()
|
||||
compat.replace_markdown(c, "page-123", "# Fresh", allow_deleting=True)
|
||||
_, kwargs = c.request.call_args
|
||||
assert kwargs["path"] == "pages/page-123/markdown"
|
||||
assert kwargs["body"]["type"] == "replace_content"
|
||||
assert kwargs["body"]["replace_content"]["new_str"] == "# Fresh"
|
||||
assert kwargs["body"]["replace_content"]["allow_deleting_content"] is True
|
||||
|
||||
|
||||
def test_extract_local_images_splits():
|
||||
content = ("# Title\n\n\n\n"
|
||||
"\n\nbody")
|
||||
without, imgs = notion_writer.extract_local_images(content)
|
||||
assert imgs == [("local", "./b.png")]
|
||||
assert "" not in without
|
||||
assert "" in without # remote stays
|
||||
|
||||
|
||||
def test_content_has_local_images():
|
||||
assert notion_writer._content_has_local_images("") is True
|
||||
assert notion_writer._content_has_local_images("") is False
|
||||
assert notion_writer._content_has_local_images("no images") is False
|
||||
|
||||
|
||||
def test_local_image_inside_fence_ignored():
|
||||
"""A local image reference inside a fenced code block must NOT be detected
|
||||
as a real image by _content_has_local_images or extracted by extract_local_images."""
|
||||
fenced = "```markdown\n\n```"
|
||||
assert notion_writer._content_has_local_images(fenced) is False, \
|
||||
"_content_has_local_images should return False for image inside fence"
|
||||
without, imgs = notion_writer.extract_local_images(fenced)
|
||||
assert len(imgs) == 0, "extract_local_images should not extract image inside fence"
|
||||
# The fenced lines (including the image line) should be preserved verbatim
|
||||
assert "" in without, "fence content must be preserved in output"
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [
|
||||
test_create_page_markdown,
|
||||
test_append_markdown,
|
||||
test_replace_markdown,
|
||||
test_extract_local_images_splits,
|
||||
test_content_has_local_images,
|
||||
test_local_image_inside_fence_ignored,
|
||||
]
|
||||
for t in tests:
|
||||
print(f"\n{t.__name__}")
|
||||
t()
|
||||
print("\n" + "=" * 50)
|
||||
print(f"✅ All {len(tests)} tests passed")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all()
|
||||
101
custom-skills/32-notion-writer/code/scripts/test_md_translate.py
Normal file
101
custom-skills/32-notion-writer/code/scripts/test_md_translate.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for md_translate.py — run with `python test_md_translate.py`."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
import md_translate
|
||||
|
||||
|
||||
def test_passthrough_basics():
|
||||
src = "# Heading\n\n- item\n\n```python\nx=1\n```"
|
||||
assert md_translate.translate(src) == src
|
||||
|
||||
|
||||
def test_callout_note():
|
||||
out = md_translate.translate("> [!NOTE]\n> Be careful")
|
||||
assert '<callout icon="ℹ️" color="blue_bg">' in out
|
||||
assert "\tBe careful" in out
|
||||
assert "</callout>" in out
|
||||
|
||||
|
||||
def test_callout_warning_color():
|
||||
out = md_translate.translate("> [!WARNING]\n> Danger")
|
||||
assert 'color="yellow_bg"' in out
|
||||
assert 'icon="⚠️"' in out
|
||||
|
||||
|
||||
def test_columns():
|
||||
src = "::: columns\n::: column\nLeft\n:::\n::: column\nRight\n:::\n:::"
|
||||
out = md_translate.translate(src)
|
||||
assert "<columns>" in out
|
||||
assert out.count("<column>") == 2
|
||||
assert "</columns>" in out
|
||||
assert "\tLeft" in out or "\t\tLeft" in out
|
||||
|
||||
|
||||
def test_toggle_children_indented():
|
||||
src = "<details>\n<summary>More</summary>\nBody line\n</details>"
|
||||
out = md_translate.translate(src)
|
||||
assert "<summary>More</summary>" in out
|
||||
assert "\tBody line" in out
|
||||
|
||||
|
||||
def test_mention_url():
|
||||
out = md_translate.translate(
|
||||
"See @[ADR](https://notion.so/X-abcdef0123456789abcdef0123456789).")
|
||||
assert "<mention-page url=" in out
|
||||
assert ">ADR</mention-page>" in out
|
||||
|
||||
|
||||
def test_mention_invalid_plain():
|
||||
out = md_translate.translate("ping @[Bob](not-an-id)")
|
||||
assert "@Bob" in out
|
||||
assert "mention-page" not in out
|
||||
|
||||
|
||||
def test_fence_passthrough_no_transform():
|
||||
"""Lines inside fenced code blocks must pass through verbatim — no callout,
|
||||
columns, or mention transformation applied."""
|
||||
raw_id = "abcdef0123456789abcdef0123456789"
|
||||
src = (
|
||||
"```\n"
|
||||
"> [!NOTE]\n"
|
||||
"::: columns\n"
|
||||
f"@[Page]({raw_id})\n"
|
||||
"```"
|
||||
)
|
||||
out = md_translate.translate(src)
|
||||
# No transformation should have occurred
|
||||
assert "<callout" not in out, "callout must not be emitted inside fence"
|
||||
assert "<columns>" not in out, "columns must not be emitted inside fence"
|
||||
assert "<mention-page" not in out, "mention must not be emitted inside fence"
|
||||
# Original lines must be present verbatim
|
||||
assert "> [!NOTE]" in out
|
||||
assert "::: columns" in out
|
||||
assert f"@[Page]({raw_id})" in out
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [
|
||||
test_passthrough_basics,
|
||||
test_callout_note,
|
||||
test_callout_warning_color,
|
||||
test_columns,
|
||||
test_toggle_children_indented,
|
||||
test_mention_url,
|
||||
test_mention_invalid_plain,
|
||||
test_fence_passthrough_no_transform,
|
||||
]
|
||||
for t in tests:
|
||||
print(f"\n{t.__name__}")
|
||||
t()
|
||||
print("\n" + "=" * 50)
|
||||
print(f"✅ All {len(tests)} tests passed")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all()
|
||||
152
custom-skills/32-notion-writer/code/scripts/test_ntn_files.py
Normal file
152
custom-skills/32-notion-writer/code/scripts/test_ntn_files.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for ntn_files.py — run with `python test_ntn_files.py`."""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
import ntn_files
|
||||
from ntn_files import NtnUploadError
|
||||
|
||||
|
||||
def _reset_cache():
|
||||
ntn_files._WORKSPACE = None
|
||||
|
||||
|
||||
def test_preflight_missing_ntn():
|
||||
_reset_cache()
|
||||
with mock.patch("shutil.which", return_value=None):
|
||||
try:
|
||||
ntn_files.preflight()
|
||||
assert False, "expected NtnUploadError"
|
||||
except NtnUploadError as e:
|
||||
assert "ntn" in str(e).lower()
|
||||
|
||||
|
||||
def test_preflight_returns_workspace():
|
||||
_reset_cache()
|
||||
fake = subprocess.CompletedProcess(
|
||||
args=[], returncode=0,
|
||||
stdout='{"bot":{"workspace_name":"D.Intelligence",'
|
||||
'"workspace_id":"ws-123"}}',
|
||||
stderr="",
|
||||
)
|
||||
with mock.patch("shutil.which", return_value="/usr/bin/ntn"), \
|
||||
mock.patch("subprocess.run", return_value=fake):
|
||||
info = ntn_files.preflight()
|
||||
assert info["workspace_name"] == "D.Intelligence"
|
||||
assert info["workspace_id"] == "ws-123"
|
||||
|
||||
|
||||
def test_upload_returns_id():
|
||||
_reset_cache()
|
||||
fake = subprocess.CompletedProcess(
|
||||
args=[], returncode=0,
|
||||
stdout="43833259-72ae-404e-8441-b6577f3159b4\tphoto.png\tuploaded\n",
|
||||
stderr="",
|
||||
)
|
||||
# upload() opens the file before subprocess.run; mock open so the file
|
||||
# need not exist (subprocess.run is mocked and never reads the handle).
|
||||
with mock.patch("builtins.open", mock.mock_open(read_data=b"x")), \
|
||||
mock.patch("subprocess.run", return_value=fake):
|
||||
upload_id = ntn_files.upload(Path("/tmp/photo.png"))
|
||||
assert upload_id == "43833259-72ae-404e-8441-b6577f3159b4"
|
||||
|
||||
|
||||
def test_upload_failure_raises():
|
||||
_reset_cache()
|
||||
fake = subprocess.CompletedProcess(
|
||||
args=[], returncode=1, stdout="", stderr="boom: invalid file",
|
||||
)
|
||||
with mock.patch("builtins.open", mock.mock_open(read_data=b"x")), \
|
||||
mock.patch("subprocess.run", return_value=fake):
|
||||
try:
|
||||
ntn_files.upload(Path("/tmp/bad.png"))
|
||||
assert False, "expected NtnUploadError"
|
||||
except NtnUploadError as e:
|
||||
assert e.path == "/tmp/bad.png"
|
||||
assert "boom" in e.stderr
|
||||
|
||||
|
||||
def _img(url):
|
||||
return {"object": "block", "type": "image",
|
||||
"image": {"type": "external", "external": {"url": url}}}
|
||||
|
||||
|
||||
def test_materialize_remote_untouched():
|
||||
blocks = [_img("https://ex.com/a.png")]
|
||||
out = ntn_files.materialize_local_media(
|
||||
blocks, Path("/tmp"), upload_fn=lambda p: "SHOULD_NOT_RUN")
|
||||
assert out[0]["image"]["type"] == "external"
|
||||
|
||||
|
||||
def test_materialize_local_uploaded(tmp_path=None):
|
||||
import tempfile, os
|
||||
d = Path(tempfile.mkdtemp())
|
||||
(d / "x.png").write_bytes(b"fake")
|
||||
blocks = [_img("x.png")]
|
||||
out = ntn_files.materialize_local_media(
|
||||
blocks, d, upload_fn=lambda p: "UP123")
|
||||
assert out[0]["image"]["type"] == "file_upload"
|
||||
assert out[0]["image"]["file_upload"]["id"] == "UP123"
|
||||
|
||||
|
||||
def test_materialize_nested_children():
|
||||
import tempfile
|
||||
d = Path(tempfile.mkdtemp())
|
||||
(d / "y.png").write_bytes(b"fake")
|
||||
toggle = {"object": "block", "type": "toggle",
|
||||
"toggle": {"rich_text": [], "children": [_img("y.png")]}}
|
||||
out = ntn_files.materialize_local_media(
|
||||
[toggle], d, upload_fn=lambda p: "UPNESTED")
|
||||
child = out[0]["toggle"]["children"][0]
|
||||
assert child["image"]["file_upload"]["id"] == "UPNESTED"
|
||||
|
||||
|
||||
def test_materialize_missing_file_raises():
|
||||
blocks = [_img("nope.png")]
|
||||
try:
|
||||
ntn_files.materialize_local_media(
|
||||
blocks, Path("/tmp"), upload_fn=lambda p: "x")
|
||||
assert False, "expected NtnUploadError"
|
||||
except NtnUploadError as e:
|
||||
assert "nope.png" in str(e) or "nope.png" in e.path
|
||||
|
||||
|
||||
def test_upload_passes_token_env():
|
||||
_reset_cache()
|
||||
fake = subprocess.CompletedProcess(args=[], returncode=0,
|
||||
stdout="ID123\tphoto.png\tuploaded\n", stderr="")
|
||||
with mock.patch.dict("os.environ", {"NOTION_API_KEY": "tok-abc"}, clear=False), \
|
||||
mock.patch("builtins.open", mock.mock_open(read_data=b"x")), \
|
||||
mock.patch("subprocess.run", return_value=fake) as m:
|
||||
ntn_files.upload(Path("/tmp/p.png"))
|
||||
passed_env = m.call_args.kwargs["env"]
|
||||
assert passed_env["NOTION_API_TOKEN"] == "tok-abc"
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [
|
||||
test_preflight_missing_ntn,
|
||||
test_preflight_returns_workspace,
|
||||
test_upload_returns_id,
|
||||
test_upload_failure_raises,
|
||||
test_materialize_remote_untouched,
|
||||
test_materialize_local_uploaded,
|
||||
test_materialize_nested_children,
|
||||
test_materialize_missing_file_raises,
|
||||
test_upload_passes_token_env,
|
||||
]
|
||||
for t in tests:
|
||||
print(f"\n{t.__name__}")
|
||||
t()
|
||||
print("\n" + "=" * 50)
|
||||
print(f"✅ All {len(tests)} tests passed")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_all()
|
||||
@@ -413,6 +413,28 @@ def test_no_literal_markers_leak():
|
||||
_assert("bold" in joined and "link" in joined, "visible words preserved")
|
||||
|
||||
|
||||
def test_image_remote_external():
|
||||
blocks = markdown_to_notion_blocks("")
|
||||
assert len(blocks) == 1
|
||||
b = blocks[0]
|
||||
assert b["type"] == "image"
|
||||
assert b["image"]["type"] == "external"
|
||||
assert b["image"]["external"]["url"] == "https://ex.com/c.png"
|
||||
assert b["image"]["caption"][0]["text"]["content"] == "a chart"
|
||||
|
||||
|
||||
def test_image_local_external_shape_preupload():
|
||||
blocks = markdown_to_notion_blocks("")
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0]["image"]["external"]["url"] == "./pics/x.png"
|
||||
|
||||
|
||||
def test_image_only_when_standalone():
|
||||
# An inline bang-bracket inside prose is NOT an image block.
|
||||
blocks = markdown_to_notion_blocks("see  inline")
|
||||
assert blocks[0]["type"] == "paragraph"
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [
|
||||
test_rich_text_plain,
|
||||
@@ -447,6 +469,9 @@ def run_all():
|
||||
test_rich_text_relative_link_becomes_plain,
|
||||
test_rich_text_absolute_link_preserved,
|
||||
test_no_literal_markers_leak,
|
||||
test_image_remote_external,
|
||||
test_image_local_external_shape_preupload,
|
||||
test_image_only_when_standalone,
|
||||
]
|
||||
for t in tests:
|
||||
print(f"\n{t.__name__}")
|
||||
|
||||
@@ -11,14 +11,14 @@ Push markdown content to Notion pages or databases via Claude Code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python virtual environment at `~/Project/our-claude-skills/custom-skills/02-notion-writer/code/scripts/venv`
|
||||
- Python virtual environment at `~/Project/our-claude-skills/custom-skills/32-notion-writer/code/scripts/venv`
|
||||
- Notion integration token (preferred: stored in 1Password — see [Credential handling](#credential-handling) below)
|
||||
- Target pages/databases must be shared with the integration in Notion (Database/Page → ⋯ → Connections → add integration)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills/custom-skills/02-notion-writer/code/scripts
|
||||
cd ~/Project/our-claude-skills/custom-skills/32-notion-writer/code/scripts
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
|
||||
234
custom-skills/32-seo-crawl-budget/SKILL.md
Normal file
234
custom-skills/32-seo-crawl-budget/SKILL.md
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
name: seo-crawl-budget
|
||||
description: |
|
||||
Crawl budget optimization and server log analysis for search engine bots.
|
||||
Triggers: crawl budget, log analysis, bot crawling, Googlebot, crawl waste,
|
||||
orphan pages, crawl efficiency, 크롤 예산, 로그 분석, 크롤 최적화.
|
||||
---
|
||||
|
||||
# Crawl Budget Optimizer
|
||||
|
||||
Analyze server access logs to identify crawl budget waste and generate optimization recommendations for search engine bots (Googlebot, Yeti/Naver, Bingbot, Daumoa/Kakao).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Log Analysis
|
||||
- Parse Nginx combined, Apache combined, and CloudFront log formats
|
||||
- Support for gzip/bzip2 compressed logs
|
||||
- Streaming parser for files >1GB
|
||||
- Date range filtering
|
||||
- Custom format via regex
|
||||
|
||||
### Bot Profiling
|
||||
- Identify bots by User-Agent: Googlebot (and variants), Yeti (Naver), Bingbot, Daumoa (Kakao), Applebot, DuckDuckBot, Baiduspider
|
||||
- Per-bot metrics: requests/day, requests/hour, unique URLs crawled
|
||||
- Status code distribution per bot (200, 301, 404, 500)
|
||||
- Crawl depth distribution
|
||||
- Crawl pattern analysis (time of day, days of week)
|
||||
- Most crawled URLs per bot
|
||||
|
||||
### Waste Detection
|
||||
- **Parameter URLs**: ?sort=, ?filter=, ?page=, ?utm_* consuming crawl budget
|
||||
- **Redirect chains**: Multiple redirects consuming crawl slots
|
||||
- **Soft 404s**: 200 status pages with error/empty content
|
||||
- **Duplicate URLs**: www/non-www, http/https, trailing slash variants
|
||||
- **Low-value pages**: Thin content pages, noindex pages being crawled
|
||||
|
||||
### Orphan Page Detection
|
||||
- Pages in sitemap but never crawled by bots
|
||||
- Pages crawled but not in sitemap
|
||||
- Crawled pages with no internal links pointing to them
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Obtain Server Access Logs
|
||||
Request or locate server access logs from the target site. Supported formats:
|
||||
- Nginx: `/var/log/nginx/access.log`
|
||||
- Apache: `/var/log/apache2/access.log`
|
||||
- CloudFront: Downloaded from S3 or CloudWatch
|
||||
|
||||
### Step 2: Parse Access Logs
|
||||
```bash
|
||||
python scripts/log_parser.py --log-file access.log --json
|
||||
python scripts/log_parser.py --log-file access.log.gz --streaming --json
|
||||
python scripts/log_parser.py --log-file access.log --bot googlebot --json
|
||||
```
|
||||
|
||||
### Step 3: Crawl Budget Analysis
|
||||
```bash
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --sitemap https://example.com/sitemap.xml --json
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --scope waste --json
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --scope orphans --json
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --scope bots --json
|
||||
```
|
||||
|
||||
### Step 4: Cross-Reference with External Data (Optional)
|
||||
Use `our-seo-agent` CLI or provide pre-fetched JSON via `--input` to compare indexed pages vs crawled pages. WebSearch can supplement with current indexing data.
|
||||
|
||||
### Step 5: Generate Recommendations
|
||||
Prioritized action items:
|
||||
1. robots.txt optimization (block parameter URLs, low-value paths)
|
||||
2. URL parameter handling (Google Search Console settings)
|
||||
3. Noindex/nofollow for low-value pages
|
||||
4. Redirect chain resolution (reduce 301 → 301 → 200 to 301 → 200)
|
||||
5. Internal linking improvements for orphan pages
|
||||
|
||||
### Step 6: Report to Notion
|
||||
Save Korean-language report to SEO Audit Log database.
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| Issue | Title | Report title (Korean + date) |
|
||||
| Site | URL | Audited website URL |
|
||||
| Category | Select | Crawl Budget |
|
||||
| Priority | Select | Based on efficiency score |
|
||||
| Found Date | Date | Analysis date (YYYY-MM-DD) |
|
||||
| Audit ID | Rich Text | Format: CRAWL-YYYYMMDD-NNN |
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
Crawl-budget analysis is **primarily log-based** — the authoritative signal lives in server access logs that this skill's local Python scripts parse. API backends supplement that with crawled URL inventories, index status, and third-party site-audit views. Pick per data class.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **Server access logs** (via `scripts/log_parser.py` + `crawl_budget_analyzer.py`) | **Primary** — bot identification, request volume, status code distribution, waste detection, orphan pages | Requires actual server logs from the user (Nginx / Apache / CloudFront). No MCP substitute. |
|
||||
| **OurSEO** (CLI + MCP) | **Default** for crawl-derived URL inventory, index status, redirect chains | CLI: `our collect crawl`, `our research google index`, `our audit tech`. MCP: `mcp__ourseo__crawl_website`, `mcp__ourseo__check_index`. Distributed crawl for large sites: `our collect distributed --workers N`. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Orphan detection, redirect chain analysis via Ahrefs site audit | `site-audit-issues`, `site-audit-page-explorer`, `site-audit-page-content`. Useful when an Ahrefs project already exists for the domain. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Alternative site audit when Ahrefs project doesn't exist | `siteaudit_research` → `get_report_schema` → `execute_report`. |
|
||||
| **GSC** (via `our research search-console`) | First-party Googlebot crawl stats (Coverage / Crawl Stats reports) | Required for ground-truth Googlebot behaviour — server logs show what hit the origin, GSC shows what Google considers crawled. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **Server access logs are available** → ALWAYS process them first (`log_parser.py` + `crawl_budget_analyzer.py`). They are the only source for actual bot behaviour at the origin.
|
||||
4. **Sitemap vs. crawl comparison needed** → OurSEO `crawl_website` for URL inventory; cross-reference with logs.
|
||||
5. **No logs available** → fall back to OurSEO crawl + GSC Coverage + Ahrefs/Semrush site audit. State this limitation explicitly in the report.
|
||||
6. **Default**: server logs (when present) + **OurSEO crawl + index check** for URL inventory.
|
||||
7. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Local log analysis (primary):**
|
||||
```bash
|
||||
python scripts/log_parser.py --log-file access.log --json
|
||||
python scripts/log_parser.py --log-file access.log.gz --streaming --json
|
||||
python scripts/log_parser.py --log-file access.log --bot googlebot --json
|
||||
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --sitemap https://<site>/sitemap.xml --json
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --scope waste --json
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --scope orphans --json
|
||||
python scripts/crawl_budget_analyzer.py --log-file access.log --scope bots --json
|
||||
```
|
||||
|
||||
**OurSEO CLI (URL inventory + index check):**
|
||||
```bash
|
||||
our collect crawl https://<site> --max-pages 5000
|
||||
our collect distributed https://<site> --workers 8 --max-pages 50000
|
||||
our research google index --domain <site>
|
||||
our audit tech https://<site>
|
||||
```
|
||||
|
||||
**OurSEO MCP (Claude Desktop):**
|
||||
```
|
||||
mcp__ourseo__crawl_website(url="<site>", max_pages=5000)
|
||||
mcp__ourseo__check_index(domain="<site>")
|
||||
```
|
||||
|
||||
**GSC (first-party crawl stats):**
|
||||
```bash
|
||||
our research search-console queries --site sc-domain:<site> --days 28
|
||||
# See also: GSC Coverage / Crawl Stats reports (UI-only for some sections).
|
||||
```
|
||||
|
||||
**Ahrefs MCP (third-party site audit):**
|
||||
```
|
||||
mcp__ahrefs__site-audit-projects()
|
||||
mcp__ahrefs__site-audit-issues(project_id="<id>")
|
||||
mcp__ahrefs__site-audit-page-explorer(project_id="<id>")
|
||||
```
|
||||
|
||||
**Semrush MCP (alternative site audit):**
|
||||
```
|
||||
mcp__semrush__siteaudit_research(query="<site>", database="us")
|
||||
```
|
||||
|
||||
Always record the chosen data source(s) in the report **Overview** so future audits can compare like-for-like — and explicitly state whether server logs were available.
|
||||
|
||||
## Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"log_file": "access.log",
|
||||
"analysis_period": {"from": "2025-01-01", "to": "2025-01-31"},
|
||||
"total_bot_requests": 150000,
|
||||
"bots": {
|
||||
"googlebot": {
|
||||
"requests": 80000,
|
||||
"unique_urls": 12000,
|
||||
"avg_requests_per_day": 2580,
|
||||
"status_distribution": {"200": 70000, "301": 5000, "404": 3000, "500": 2000}
|
||||
},
|
||||
"yeti": {"requests": 35000},
|
||||
"bingbot": {"requests": 20000},
|
||||
"daumoa": {"requests": 15000}
|
||||
},
|
||||
"waste": {
|
||||
"parameter_urls": {"count": 5000, "pct_of_crawls": 3.3},
|
||||
"redirect_chains": {"count": 2000, "pct_of_crawls": 1.3},
|
||||
"soft_404s": {"count": 1500, "pct_of_crawls": 1.0},
|
||||
"total_waste_pct": 8.5
|
||||
},
|
||||
"orphan_pages": {
|
||||
"in_sitemap_not_crawled": [],
|
||||
"crawled_not_in_sitemap": []
|
||||
},
|
||||
"recommendations": [],
|
||||
"efficiency_score": 72,
|
||||
"timestamp": "2025-01-01T00:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Korean Output Example
|
||||
|
||||
```
|
||||
# 크롤 예산 분석 보고서 - example.com
|
||||
|
||||
## 분석 기간: 2025-01-01 ~ 2025-01-31
|
||||
|
||||
### 봇별 크롤 현황
|
||||
| 봇 | 요청 수 | 고유 URL | 일 평균 |
|
||||
|----|---------|---------|---------|
|
||||
| Googlebot | 80,000 | 12,000 | 2,580 |
|
||||
| Yeti (Naver) | 35,000 | 8,000 | 1,129 |
|
||||
|
||||
### 크롤 낭비 요인
|
||||
- 파라미터 URL: 5,000건 (3.3%)
|
||||
- 리다이렉트 체인: 2,000건 (1.3%)
|
||||
- 소프트 404: 1,500건 (1.0%)
|
||||
|
||||
### 효율성 점수: 72/100
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires actual server access logs (not available via standard web crawling)
|
||||
- Log format auto-detection may need manual format specification for custom formats
|
||||
- CloudFront logs have a different field structure than Nginx/Apache
|
||||
- Large log files (>10GB) may need pre-filtering before analysis
|
||||
- Bot identification relies on User-Agent strings which can be spoofed
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All audit reports MUST be saved to the OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Category**: Crawl Budget
|
||||
- **Audit ID Format**: CRAWL-YYYYMMDD-NNN
|
||||
- **Language**: Korean with technical English terms (Crawl Budget, Googlebot, robots.txt)
|
||||
|
||||
## Reference Scripts
|
||||
|
||||
Located in `code/scripts/`:
|
||||
- `log_parser.py` — Server access log parser with bot identification
|
||||
- `crawl_budget_analyzer.py` — Crawl budget efficiency analysis
|
||||
- `base_client.py` — Shared async client utilities
|
||||
220
custom-skills/33-seo-migration-planner/SKILL.md
Normal file
220
custom-skills/33-seo-migration-planner/SKILL.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: seo-migration-planner
|
||||
description: |
|
||||
SEO site migration planning and monitoring. Triggers: site migration, domain move, redirect mapping, platform migration, URL restructuring, HTTPS migration, subdomain consolidation, 사이트 이전, 도메인 이전, 리디렉트 매핑.
|
||||
---
|
||||
|
||||
# SEO Migration Planner & Monitor
|
||||
|
||||
## Purpose
|
||||
|
||||
Comprehensive site migration planning and post-migration monitoring for SEO: crawl-based URL inventory, traffic/keyword baseline capture via our-seo-agent CLI, redirect map generation with per-URL risk scoring, pre-migration checklist creation, and post-launch traffic/indexation/ranking recovery tracking with automated alerts. Supports domain moves, platform changes, URL restructuring, HTTPS migrations, and subdomain consolidation.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **URL Inventory** - Crawl entire site via Firecrawl to capture all URLs and status codes
|
||||
2. **Traffic Baseline** - Capture per-page traffic, keywords, and backlinks via our-seo-agent CLI
|
||||
3. **Redirect Map Generation** - Create old URL -> new URL mappings with 301 redirect rules
|
||||
4. **Risk Scoring** - Score each URL (0-100) based on traffic, backlinks, and keyword rankings
|
||||
5. **Pre-Migration Checklist** - Generate type-specific migration checklist (Korean)
|
||||
6. **Post-Migration Traffic Comparison** - Compare pre vs post traffic by page group
|
||||
7. **Redirect Health Check** - Detect broken redirects, chains, and loops
|
||||
8. **Indexation Tracking** - Monitor indexed page count changes and missing pages
|
||||
9. **Ranking Monitoring** - Track keyword position changes for priority keywords
|
||||
10. **Recovery Estimation** - Estimate traffic recovery timeline based on migration type
|
||||
11. **Alert Generation** - Flag traffic drops >20%, broken redirects, indexation loss
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
Site migration spans **URL inventory** (crawl), **traffic/keyword/backlink baseline** (third-party + first-party), **redirect verification** (live HTTP), and **post-launch monitoring** (delta over time). Pick per data class — migration baselines often combine 3-4 backends.
|
||||
|
||||
| Backend | Best for | Notes |
|
||||
|---|---|---|
|
||||
| **OurSEO** (CLI + MCP) | **Default** for URL inventory, on-page crawl, index status, technical audit, schema baseline | CLI: `our collect crawl`, `our collect distributed`, `our audit tech`, `our research google index`. MCP: `mcp__ourseo__crawl_website`, `mcp__ourseo__check_index`. Distributed crawler for sites >5K URLs. |
|
||||
| **Firecrawl MCP** (`mcp__firecrawl__*`) | Large-scale URL inventory when OurSEO crawler is unavailable; per-URL redirect verification | `firecrawl_crawl` for full site map; `firecrawl_scrape` for redirect health on a specific URL. Useful when target is JS-heavy. |
|
||||
| **Ahrefs MCP** (`mcp__ahrefs__*`) | Per-URL **traffic value + backlink count** baseline (risk scoring), top pages by traffic | `site-explorer-top-pages` (traffic per URL), `site-explorer-pages-by-backlinks`, `site-explorer-metrics`, `site-explorer-metrics-history`, `site-explorer-pages-history`. |
|
||||
| **Semrush MCP** (`mcp__semrush__*`) | Per-URL organic traffic + keyword baseline | `organic_research`, `url_research`, `overview_research`. |
|
||||
| **GSC** (via `our research search-console`) | **First-party impression baseline** — what Google rendered for each URL pre-migration | Required for ground-truth post-migration comparison. Use `pages` / `combined` reports. |
|
||||
| **GA4** (via `our research ga4 *`) | First-party traffic baseline (sessions, conversions per URL) | Best for actual user-traffic baseline, complements GSC impressions. |
|
||||
| **Perplexity MCP** (`mcp__perplexity__*`) | Research migration best practices, common pitfalls per migration type | Not a data source — guidance enrichment. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **User named a backend explicitly** → use it.
|
||||
2. **User preference memory** — read `feedback_seo_tool_preferences.md`; honor the task-type default.
|
||||
3. **URL inventory** → **OurSEO** crawler (CLI primary). Use Firecrawl only when OurSEO crawler can't handle the target (e.g., JS-heavy SPA).
|
||||
4. **Per-URL traffic value for risk scoring** → Ahrefs `site-explorer-top-pages` is the strongest signal. Semrush `url_research` is the alternate.
|
||||
5. **First-party baseline (REQUIRED for accurate post-migration diff)** → GSC + GA4 via `our` CLI. Always capture before migration date.
|
||||
6. **Redirect verification** (post-launch) → Firecrawl `firecrawl_scrape` per URL OR `WebFetch` with redirect following.
|
||||
7. **Default baseline stack**: OurSEO crawl + Ahrefs top-pages + GSC pages + GA4 traffic. Document every source.
|
||||
8. **Still ambiguous + non-trivial** → ask once via `AskUserQuestion`.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**OurSEO CLI (default — URL inventory + audit):**
|
||||
```bash
|
||||
our collect crawl https://<site> --max-pages 5000
|
||||
our collect distributed https://<site> --workers 8 --max-pages 50000
|
||||
our audit tech https://<site>
|
||||
our research google index --domain <site>
|
||||
```
|
||||
|
||||
**OurSEO MCP (Claude Desktop):**
|
||||
```
|
||||
mcp__ourseo__crawl_website(url="<site>", max_pages=5000)
|
||||
mcp__ourseo__check_index(domain="<site>")
|
||||
```
|
||||
|
||||
**Firecrawl (URL inventory + redirect verification):**
|
||||
```
|
||||
mcp__firecrawl__firecrawl_crawl(url="<site>", limit=5000)
|
||||
mcp__firecrawl__firecrawl_scrape(url="<specific URL>", formats=["markdown"])
|
||||
```
|
||||
|
||||
**Ahrefs MCP (per-URL traffic + backlink baseline):**
|
||||
```
|
||||
mcp__ahrefs__site-explorer-top-pages(target="<site>", country="us", limit=500)
|
||||
mcp__ahrefs__site-explorer-pages-by-backlinks(target="<site>", limit=500)
|
||||
mcp__ahrefs__site-explorer-metrics(target="<site>")
|
||||
mcp__ahrefs__site-explorer-metrics-history(target="<site>", history="weekly")
|
||||
mcp__ahrefs__site-explorer-pages-history(target="<site>")
|
||||
```
|
||||
|
||||
**Semrush MCP (per-URL organic baseline):**
|
||||
```
|
||||
mcp__semrush__organic_research(query="<site>", database="us")
|
||||
mcp__semrush__url_research(query="<specific URL>", database="us")
|
||||
mcp__semrush__overview_research(query="<site>", database="us")
|
||||
```
|
||||
|
||||
**First-party baseline (GSC + GA4):**
|
||||
```bash
|
||||
our research search-console pages --site sc-domain:<site> --days 90
|
||||
our research search-console combined --site sc-domain:<site> --days 90
|
||||
our research ga4 traffic --property-id <id>
|
||||
our research ga4 landing-pages --property-id <id>
|
||||
```
|
||||
|
||||
**Migration best practices (Perplexity):**
|
||||
```
|
||||
mcp__perplexity__search(query="SEO migration <type> best practices")
|
||||
```
|
||||
|
||||
Always record the chosen data source(s) per metric in the **planning report's Baseline** section and re-use the same sources in **post-migration monitoring** so the diff is meaningful.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Pre-Migration Planning
|
||||
1. Accept target domain, migration type, and new domain (if applicable)
|
||||
2. Crawl URL inventory via Firecrawl (capture all URLs + status codes)
|
||||
3. Fetch top pages baseline via our-seo-agent CLI or pre-fetched data
|
||||
4. Fetch site-level metrics (total traffic, keywords, referring domains)
|
||||
5. Enrich URL inventory with traffic/backlink data from our-seo-agent CLI
|
||||
6. Score risk per URL (0-100) based on traffic weight (40%), backlinks (30%), keywords (30%)
|
||||
7. Generate redirect map (old URL -> new URL) based on migration type
|
||||
8. Aggregate risk assessment (high/medium/low URL counts, overall risk level)
|
||||
9. Generate pre-migration checklist (common + type-specific items, in Korean)
|
||||
10. Save baseline and plan to Notion
|
||||
|
||||
### Post-Migration Monitoring
|
||||
1. Accept domain, migration date, and optional baseline JSON
|
||||
2. Compare pre vs post traffic using our-seo-agent metrics history
|
||||
3. Check redirect health via Firecrawl (broken, chains, loops)
|
||||
4. Track indexation changes (pre vs post page count, missing pages)
|
||||
5. Track keyword ranking changes for priority keywords
|
||||
6. Estimate recovery timeline based on traffic delta and migration type
|
||||
7. Generate alerts for significant issues (traffic >20% drop, broken redirects, etc.)
|
||||
8. Save monitoring report to Notion
|
||||
|
||||
## Output Format
|
||||
|
||||
### Planning Report
|
||||
```markdown
|
||||
## SEO 사이트 이전 계획: [domain]
|
||||
|
||||
### 베이스라인
|
||||
- 전체 URL 수: [count]
|
||||
- 오가닉 트래픽: [traffic]
|
||||
- 오가닉 키워드: [keywords]
|
||||
- 참조 도메인: [count]
|
||||
|
||||
### 위험 평가
|
||||
- 전체 위험도: [HIGH/MEDIUM/LOW]
|
||||
- 고위험 URL: [count]개
|
||||
- 중위험 URL: [count]개
|
||||
- 저위험 URL: [count]개
|
||||
|
||||
### 리디렉트 맵 (상위 위험 URL)
|
||||
| Source URL | Target URL | Risk Score | Priority |
|
||||
|------------|------------|------------|----------|
|
||||
|
||||
### 사전 체크리스트
|
||||
- [ ] Step 1: ...
|
||||
- [ ] Step 2: ...
|
||||
```
|
||||
|
||||
### Monitoring Report
|
||||
```markdown
|
||||
## SEO 이전 모니터링 보고서: [domain]
|
||||
### 이전일: [date] | 경과일: [N]일
|
||||
|
||||
### 알림
|
||||
- [severity] [message]
|
||||
|
||||
### 트래픽 비교
|
||||
| Page Group | Pre | Post | Change | Status |
|
||||
|------------|-----|------|--------|--------|
|
||||
|
||||
### 리디렉트 상태
|
||||
- 전체: [count] | 정상: [count] | 깨짐: [count] | 체인: [count]
|
||||
|
||||
### 인덱싱 현황
|
||||
- 이전 전: [count] | 이전 후: [count] | 변화: [pct]%
|
||||
|
||||
### 회복 예상
|
||||
- 예상 기간: [weeks]주
|
||||
- 현재 회복률: [pct]%
|
||||
```
|
||||
|
||||
## Risk Scoring Methodology
|
||||
|
||||
| Factor | Weight | Scale |
|
||||
|--------|--------|-------|
|
||||
| Traffic | 40% | 1,000+ monthly visits = high risk |
|
||||
| Backlinks | 30% | 50+ referring domains = high risk |
|
||||
| Keywords | 30% | 20+ keyword rankings = high risk |
|
||||
|
||||
### Priority Classification
|
||||
|
||||
| Risk Score | Priority | Action |
|
||||
|------------|----------|--------|
|
||||
| 75-100 | Critical | Manual redirect verification required |
|
||||
| 50-74 | High | Priority redirect with monitoring |
|
||||
| 25-49 | Medium | Standard redirect |
|
||||
| 0-24 | Low | Batch redirect |
|
||||
|
||||
## Alert Thresholds
|
||||
|
||||
| Alert Type | Threshold | Severity |
|
||||
|------------|-----------|----------|
|
||||
| Traffic drop | >20% | warning; >40% critical |
|
||||
| Broken redirects | >0 | warning; >10 critical |
|
||||
| Redirect chains | >0 | warning |
|
||||
| Indexation loss | >10% | warning; >30% critical |
|
||||
| Ranking drop | >5 positions (volume 100+) | warning; >20 keywords critical |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Data freshness depends on source and collection method
|
||||
- Firecrawl crawl limited to 5,000 URLs per run
|
||||
- Redirect chain detection depends on Firecrawl following redirects
|
||||
- Recovery estimation is heuristic-based on industry averages
|
||||
- URL restructuring requires manual mapping rules (no auto-pattern detection)
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category ("SEO Migration"), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: MIGR-YYYYMMDD-NNN
|
||||
195
custom-skills/34-seo-reporting-dashboard/SKILL.md
Normal file
195
custom-skills/34-seo-reporting-dashboard/SKILL.md
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
name: seo-reporting-dashboard
|
||||
description: |
|
||||
SEO reporting dashboard and executive report generation. Aggregates data from all SEO skills
|
||||
into stakeholder-ready reports and interactive HTML dashboards.
|
||||
Triggers: SEO report, SEO dashboard, executive summary, 보고서, 대시보드, performance report, 종합 보고서.
|
||||
---
|
||||
|
||||
# SEO Reporting Dashboard
|
||||
|
||||
## Purpose
|
||||
|
||||
Aggregate outputs from all SEO skills (11-33) into stakeholder-ready executive reports with interactive HTML dashboards, trend analysis, and Korean-language summaries. This is the PRESENTATION LAYER that sits on top of skill 25 (KPI Framework) and all other skill outputs, providing a unified view of SEO performance across all audit dimensions.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
1. **Report Aggregation** - Collect and normalize outputs from all SEO skills (11-33) into a unified data structure with cross-skill health scoring and priority issue identification
|
||||
2. **Interactive Dashboard** - Generate self-contained HTML dashboards with Chart.js visualizations including health gauge, traffic trends, keyword distribution, issue breakdown, and competitor radar
|
||||
3. **Executive Reporting** - Korean-language executive summary generation with audience-specific detail levels (C-level, marketing team, technical team) and prioritized action items
|
||||
|
||||
## Data Source Selection
|
||||
|
||||
This skill is the **presentation layer** — it aggregates outputs from every other SEO skill (11-33). The Data Source Selection therefore happens **per section of the report**, mirroring the per-task defaults in the source skills. Two distinct flows:
|
||||
|
||||
1. **Aggregating prior audits** → query Notion SEO Audit Log for stored skill outputs; trust the source each skill recorded.
|
||||
2. **Pulling fresh metrics** → consult each underlying skill's Data Source Selection and apply the same per-task defaults here.
|
||||
|
||||
### Per-section backend defaults
|
||||
|
||||
| Report section | Default backend | Source skill | Notes |
|
||||
|---|---|---|---|
|
||||
| Health score header | Semrush `overview_research` + OurSEO `audit_page` | 25-kpi-framework | Combines multiple metrics — see KPI framework. |
|
||||
| Organic traffic trend | Semrush `overview_research` / Ahrefs `site-explorer-metrics-history` | 25, 33 | Pick one and stay consistent across periods. |
|
||||
| Keyword visibility | Semrush `tracking_research` or Ahrefs `rank-tracker-*` | 21-position-tracking | Use GSC if site is verified for first-party. |
|
||||
| Backlinks / DR | Ahrefs `site-explorer-domain-rating` + `-backlinks-stats` | 22-link-building | Ahrefs default. |
|
||||
| Technical health | OurSEO `audit_page` + `audit tech` | 12-seo-technical-audit | OurSEO default. |
|
||||
| AI visibility / Brand Radar | Ahrefs `brand-radar-*` | 27-ai-visibility | Ahrefs-only. |
|
||||
| SERP / Naver presence | OurSEO `check_serp` + `our research naver serp` | 20-serp-analysis | Korean override via Naver. |
|
||||
| Knowledge Graph / Entity | OurSEO `search_knowledge_graph` | 28-knowledge-graph | OurSEO default. |
|
||||
| First-party clicks/impressions | GSC via `our research search-console` | 15-seo-search-console | First-party — always preferred when available. |
|
||||
| GA4 traffic + conversions | `our research ga4 *` | n/a | First-party — always preferred when available. |
|
||||
| GBP local visibility | `our collect gbp *` + `our audit local` | 18-seo-local-audit | First-party — always preferred when available. |
|
||||
| Competitor benchmarks | Semrush `organic_research` + Ahrefs `site-explorer-organic-competitors` | 31-seo-competitor-intel | Pair both for cross-check. |
|
||||
| Industry context | Perplexity MCP | n/a | Narrative enrichment only — not metrics. |
|
||||
|
||||
### How to pick
|
||||
|
||||
1. **Aggregating mode** (querying Notion for prior audits): trust the source recorded in each prior audit. If sources conflict across audits for the same metric, surface the conflict explicitly in the report's "Sources" subsection.
|
||||
2. **Fresh-pull mode**: apply each underlying skill's Data Source Selection. Don't re-decide here — defer to the source skill.
|
||||
3. **Consistency over completeness**: if the prior reporting period used Semrush for traffic value, use Semrush this period too. Switching mid-stream breaks every period-over-period chart.
|
||||
4. **First-party first**: where GSC / GA4 / GBP are available, prefer them over modelled estimates.
|
||||
|
||||
### Reporting rule
|
||||
|
||||
Every dashboard's **Health Score Overview** AND **executive report Sources subsection** MUST list the data source per section. Example:
|
||||
|
||||
```markdown
|
||||
### Sources
|
||||
- Organic traffic: Semrush overview_research (database=us)
|
||||
- Keyword visibility: Ahrefs rank-tracker-overview (project=<id>)
|
||||
- Backlinks / DR: Ahrefs site-explorer-domain-rating
|
||||
- Technical health: OurSEO audit_page (latest crawl 2026-05-14)
|
||||
- AI visibility: Ahrefs brand-radar-sov-overview (brand=<name>)
|
||||
- GSC: 28-day window, 2026-04-16 → 2026-05-14
|
||||
- GA4 property: <id>
|
||||
- GBP profile: <client>
|
||||
- Industry context: Perplexity (research timestamp 2026-05-14)
|
||||
```
|
||||
|
||||
### Aggregation flow
|
||||
|
||||
1. **Identify scope**: target domain + date range + audience (C-level / marketing / technical).
|
||||
2. **Query Notion SEO Audit Log** for the domain — pull all past audits via `mcp__notion__*`.
|
||||
3. **For each section needed**, decide aggregate vs. fresh-pull:
|
||||
- If a prior audit covers it and is recent enough → aggregate from Notion entry.
|
||||
- If stale or missing → pull fresh from the per-section default backend above.
|
||||
4. **Normalize** into the unified report structure.
|
||||
5. **Compute health score** using KPI framework weights (see skill 25).
|
||||
6. **Render** HTML dashboard + Korean executive markdown.
|
||||
7. **Push** final report back to Notion + optionally Slack.
|
||||
|
||||
### Backend call patterns
|
||||
|
||||
**Notion (prior audit aggregation):**
|
||||
```
|
||||
mcp__notion__notion-query-database-view(database_id="2c8581e5-8a1e-8035-880b-e38cefc2f3ef", filters={"Site": "<domain>"})
|
||||
mcp__notion__notion-fetch(page_id="<audit page id>")
|
||||
```
|
||||
|
||||
**Fresh pulls** — see each skill's Data Source Selection (11-33).
|
||||
|
||||
**Perplexity (industry context):**
|
||||
```
|
||||
mcp__perplexity__search(query="<industry> SEO benchmarks 2026")
|
||||
```
|
||||
|
||||
Reporting is downstream of every other SEO skill — keep the source attribution rigorous or the dashboard becomes meaningless.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Dashboard Generation
|
||||
1. Accept target domain and optional date range
|
||||
2. Query Notion SEO Audit Log for all past audit entries for the domain
|
||||
3. Optionally pull fresh metrics from our-seo-agent CLI or provide pre-fetched JSON via --input
|
||||
4. Normalize all skill outputs into unified format
|
||||
5. Compute cross-skill health score with weighted category dimensions
|
||||
6. Identify top issues (sorted by severity) and top wins across all audits
|
||||
7. Build audit history timeline
|
||||
8. Generate HTML dashboard with Chart.js charts:
|
||||
- Health score gauge (doughnut)
|
||||
- Category scores horizontal bar chart
|
||||
- Health score timeline line chart
|
||||
- Issue distribution pie chart
|
||||
- Competitor radar chart (if competitor data available)
|
||||
9. Save HTML file and optionally push summary to Notion
|
||||
|
||||
### Executive Reporting
|
||||
1. Load aggregated report data (from dashboard generation or JSON file)
|
||||
2. Select audience level: C-level, marketing, or technical
|
||||
3. Generate Korean-language narrative with:
|
||||
- Health score overview and trend
|
||||
- Category highlights (strengths and weaknesses)
|
||||
- Skills coverage summary
|
||||
- Audience-specific business impact analysis
|
||||
4. Format key wins and concerns with severity and category labels
|
||||
5. Generate prioritized action items ranked by impact
|
||||
6. Render as markdown document
|
||||
7. Optionally push to Notion SEO Audit Log
|
||||
|
||||
## Output Format
|
||||
|
||||
### HTML Dashboard
|
||||
```
|
||||
Self-contained HTML file with:
|
||||
- Responsive CSS grid layout
|
||||
- Chart.js visualizations from CDN
|
||||
- Health score gauge
|
||||
- Category bar chart
|
||||
- Timeline line chart
|
||||
- Issues pie chart
|
||||
- Competitor radar chart
|
||||
- Issues and wins lists
|
||||
- Audit history table
|
||||
```
|
||||
|
||||
### Executive Report (Markdown)
|
||||
```markdown
|
||||
# SEO 성과 보고서 - [domain]
|
||||
|
||||
**대상**: 경영진 / 마케팅팀 / 기술팀
|
||||
**도메인**: [domain]
|
||||
**보고 일자**: [date]
|
||||
|
||||
## Health Score
|
||||
| 지표 | 값 |
|
||||
|------|-----|
|
||||
| Overall Score | **[score]/100** |
|
||||
| 등급 | [grade_kr] |
|
||||
| 추세 | [trend_kr] |
|
||||
|
||||
## 종합 분석
|
||||
[Korean narrative...]
|
||||
|
||||
## 주요 성과
|
||||
- [wins...]
|
||||
|
||||
## 주요 이슈
|
||||
- [concerns...]
|
||||
|
||||
## 권장 조치 사항
|
||||
1. [recommendations...]
|
||||
```
|
||||
|
||||
## Audience Configurations
|
||||
|
||||
| Audience | Detail | Issues | Recommendations | Technical Details |
|
||||
|----------|--------|--------|------------------|-------------------|
|
||||
| C-level (경영진) | Summary | Top 5 | Top 3 | No |
|
||||
| Marketing (마케팅팀) | Moderate | Top 10 | Top 5 | No |
|
||||
| Technical (기술팀) | Detailed | Top 20 | Top 10 | Yes |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Aggregation depends on availability of JSON outputs from other skills
|
||||
- Notion query for past audits requires MCP tools (placeholder in scripts)
|
||||
- Competitor radar chart only renders if competitor intel (skill 31) data is present
|
||||
- HTML dashboard requires internet access for Chart.js CDN
|
||||
|
||||
## Notion Output (Required)
|
||||
|
||||
All reports MUST be saved to OurDigital SEO Audit Log:
|
||||
- **Database ID**: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||
- **Properties**: Issue (title), Site (url), Category ("SEO Dashboard"), Priority, Found Date, Audit ID
|
||||
- **Language**: Korean with English technical terms
|
||||
- **Audit ID Format**: DASH-YYYYMMDD-NNN
|
||||
142
custom-skills/35-seo-signal-validation/DESIGN.md
Normal file
142
custom-skills/35-seo-signal-validation/DESIGN.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Design Spec — `35-seo-signal-validation`
|
||||
|
||||
- **Status:** Draft for review
|
||||
- **Date:** 2026-06-26
|
||||
- **Author:** Andrew Yim (andrew.yim@ourdigital.org) + Claude Code
|
||||
- **Genesis:** JHR josunhotel.com — SEMrush reported an organic "surge" attributed to "호텔" 16→3. Cross-checking GSC/GA4/live-SERP proved it a modeling artifact (real position ~12, ~5 clicks/mo; growth was all brand/seasonal). See workspace memory `feedback-semrush-serp-signal-validation`.
|
||||
- **Related skills:** delegates to `20-seo-serp-analysis`, `21-seo-position-tracking`, `28-seo-knowledge-graph`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Given a `(term/intent, entity)` pair — and optionally a *claim* (a third-party tool's reported movement) or a *baseline* (a prior state to compare against) — return an **evidence-backed verdict** on whether SERP and Knowledge-Graph impact is **real**, **misattributed**, an **artifact**, or **unprovable with available data**.
|
||||
|
||||
The skill exists because OurDigital/clients repeatedly face *modeled* third-party signals (SEMrush/Ahrefs estimated organic traffic, position snapshots) that are easy to over-trust. This skill makes the validation cascade — measured → live → entity → attribution — a single repeatable procedure that ends in a defensible verdict and a client-safe narrative.
|
||||
|
||||
It generalizes the genesis case to **any term/intent and any entity** (a brand, a company, or a person), and to two additional jobs beyond refuting external claims: proving our own work's impact, and standalone "where do we really stand" checks.
|
||||
|
||||
## 2. Boundary — how this differs from neighbors
|
||||
|
||||
| Skill | Owns | This skill instead |
|
||||
|---|---|---|
|
||||
| `20-seo-serp-analysis` | What the SERP *looks like* (features, competitor positions, intent) | …calls it for the live-SERP layer |
|
||||
| `21-seo-position-tracking` | Rank *over time*, change detection, visibility | …calls it for GSC-as-ground-truth |
|
||||
| `28-seo-knowledge-graph` | Entity presence audit (KG panel, Wikidata, Naver) | …calls it for the entity layer |
|
||||
|
||||
None of the three **adjudicates the truth of a claimed cross-layer movement**. This skill is the *conductor*: signal/claim in → verdict + evidence ledger out. It duplicates none of their measurement logic; it sequences and synthesizes them.
|
||||
|
||||
## 3. Engine — the validation loop
|
||||
|
||||
A **cost-ordered evidence cascade** that short-circuits when a cheap layer is already decisive (this is exactly how the JHR "호텔" claim was refuted before any expensive step). The "loop" is the cascade, not a scheduler.
|
||||
|
||||
### 3.0 Pre-step — classify the entity (gates which layers are available)
|
||||
|
||||
- **First-party entity** — a site/property the user owns or has GSC/GA4 access to (e.g., JHR `sc-domain:josunhotel.com`, GA4 `258308769`). → **L1 measured ground truth available.**
|
||||
- **Third-party entity** — a competitor brand or a person the user does NOT control. → **L1 unavailable**; rely on L2 + L3 + clearly-tiered third-party estimates; cap confidence lower and prefer INCONCLUSIVE over guessing.
|
||||
|
||||
The skill detects this from whether a verified GSC property / GA4 property is supplied or resolvable; if ambiguous, ask once.
|
||||
|
||||
### 3.1 L1 — Measured (first-party, native history) → delegates to `21-seo-position-tracking`
|
||||
|
||||
- **GSC** via `mcp__dda__gsc_fetch_performance`:
|
||||
- term query-level (exact match) AND site-wide, **recent vs prior** window.
|
||||
- report real avg position, clicks, impressions, CTR; **day-normalize** (compare periods often differ in calendar-day count).
|
||||
- note **~43% query-level anonymization** — query-sum ≠ aggregate; never treat the disclosed subset as the whole.
|
||||
- **query-clicks delta** (recent − prior) to name which terms actually moved (brand/seasonal vs the claimed term).
|
||||
- **GA4** via `mcp__dda__ga4_run_report`: `Organic Search` sessions monthly trend (dimensions `yearMonth` + `sessionDefaultChannelGroup`, metric `sessions`); GA4 captures **all engines incl. Naver**, so use it to test whether a "surge" exceeds normal month-to-month variance.
|
||||
- **Short-circuit:** if the claimed keyword has trivial clicks and a real position nowhere near the claim → **ARTIFACT**, stop (skip L2/L3 unless caller wants the full picture).
|
||||
|
||||
### 3.2 L2 — Live SERP (third-party measured, point-in-time) → delegates to `20-seo-serp-analysis`
|
||||
|
||||
- **Live geo-correct Google render** via `claude-in-chrome` (`navigate` → `read_page`/`computer`): force `gl`/`hl` + correct geo, `pws=0`; **decline precise-location prompts** (privacy). Confirm whether the domain actually holds the claimed position; capture the feature landscape (ads, local map-pack, PAA, knowledge panel, image/video) that explains *why* a brand site can't hold a head term.
|
||||
- **Cheap rank spot-check** via `mcp__ourseo__check_serp(keyword, domain)` when a full render is unnecessary.
|
||||
- **[KR market]** Naver SERP composition via `our research naver serp` (blog/cafe/지식iN/Smart Store/brand zone) — required for Korean entities since Semrush/Ahrefs don't model Naver.
|
||||
|
||||
### 3.3 L3 — Entity / Knowledge Graph (the differentiator) → delegates to `28-seo-knowledge-graph`
|
||||
|
||||
A real impact event should leave corroborating traces in the **entity layer**, not just a rank number. Five checks:
|
||||
|
||||
1. **Google KG API** entity match + `resultScore` — `mcp__ourseo__search_knowledge_graph(query)` (uses `GOOGLE_KG_API_KEY`).
|
||||
2. **Wikidata** QID presence + key claims — **verify the QID against `Special:EntityData/{Q}.json` labels** before trusting it (bakes in the JHR false-match guard: Q109455878 = office tower ≠ hotel; Q490787 = Shinsegae Inc. ≠ Group).
|
||||
3. **Knowledge Panel** presence/attributes on the live entity-name SERP (Chrome).
|
||||
4. **sameAs** consistency on the entity's `Organization`/`Person` JSON-LD.
|
||||
5. **[KR]** Naver 백과사전 / 지식iN presence.
|
||||
|
||||
`mcp__ourseo__monitor_brand` supplements with brand-mention/brand-SERP ownership signal.
|
||||
|
||||
### 3.4 L4 — Attribution synthesis → verdict
|
||||
|
||||
Cross-check: does the **measured delta (L1)** corroborate the **live reality (L2)**, and does the **entity layer (L3)** show consistent movement? The query-clicks delta names the true drivers. Output a verdict (§5) with an evidence ledger.
|
||||
|
||||
## 4. Entry modes (thin wrappers over the engine)
|
||||
|
||||
| Mode | Input contract | Engine use |
|
||||
|---|---|---|
|
||||
| **`adjudicate(claim)`** | `{term, entity, claim:{source, metric, from→to}}` e.g. `SEMrush: 호텔 pos 16→3, organic surge` | Full cascade; verdict confirms/refutes the claim |
|
||||
| **`prove(baseline)`** | `{term, entity, change:{what, when}}` | Measured before/after from GSC/GA4 history; entity baseline = most recent existing Notion KG-audit archive — **if none exists, report current entity state only and mark the entity-layer delta INCONCLUSIVE** (the change pre-dates any captured baseline); live captured now |
|
||||
| **`snapshot()`** | `{term, entity}` | Cascade with no claim; "where do we really stand" across all four layers |
|
||||
|
||||
All three call the *same* engine; they differ only in what they compare against.
|
||||
|
||||
## 5. Verdict logic
|
||||
|
||||
| Verdict | Condition |
|
||||
|---|---|
|
||||
| **CONFIRMED** | Measured + live + (where relevant) entity all corroborate movement attributable to the term/intent |
|
||||
| **PARTIAL** | Real movement, but misattributed (e.g., growth is brand/seasonal, not the claimed head term) or only some layers agree |
|
||||
| **ARTIFACT** | Modeling/snapshot artifact — measured + live reality don't support it (the JHR 호텔 case) |
|
||||
| **INCONCLUSIVE** | Insufficient data (query anonymized, GSC lag, no entity baseline, third-party entity with no measured access) — names exactly what's missing + how to resolve |
|
||||
|
||||
**Confidence cap:** third-party entities (no L1) cannot reach CONFIRMED on traffic claims — at most PARTIAL, and ARTIFACT only when live+entity reality clearly contradicts the claim.
|
||||
|
||||
**Standing skepticism rules** (baked in from `feedback-semrush-serp-signal-validation`):
|
||||
- Estimated organic traffic = **smoke-detector, not scale** (Σ est-volume × position-CTR curve).
|
||||
- **Head-term over-fire**: one high-volume keyword caught at an estimated high rank inflates the whole modeled number.
|
||||
- **KR Naver blind spot**: Semrush models Google only; misses a large share of Korean organic.
|
||||
- **Single-geo/device snapshot** diverges from GSC's national average.
|
||||
- **Data-trust hierarchy**: 1st-party measured (GA4/GSC) > 3rd-party measured (backlinks, crawled rank) > 3rd-party modeled (estimated traffic).
|
||||
|
||||
### Output of the verdict
|
||||
- **Evidence ledger** — per layer: finding + its data-trust tier + whether it corroborates or contradicts the claim.
|
||||
- **Client-safe narrative** — the defensible story (e.g., "summer brand/long-tail demand lifted impressions +18%, clicks modest" — NOT "ranked #3 for 호텔").
|
||||
|
||||
## 6. Output
|
||||
|
||||
- **Always:** inline structured report (verdict + ledger + narrative + "what would raise confidence").
|
||||
- **Optional:** archive to Notion **Working with AI DB** (`data_source_id f8f19ede-32bd-43ac-9f60-0651f6f40afe`) via the **notion-writer script** (per global policy — never Notion MCP write tools). Properties follow the DB schema (Type=Memo/Research, Account Code, Topic=SEO, etc.).
|
||||
- **Optional:** if the run surfaces a new *generalizable* gotcha, append a memory entry to the active workspace's memory dir.
|
||||
|
||||
## 7. Repo layout & conventions
|
||||
|
||||
```
|
||||
35-seo-signal-validation/
|
||||
SKILL.md self-contained: classification, 4-layer cascade,
|
||||
5 KG checks, 4-way verdict, skepticism rules, output
|
||||
DESIGN.md PLAN.md spec + plan (live with the skill; no new top-level dir)
|
||||
code/
|
||||
CLAUDE.md code-environment notes (env, export→script flow)
|
||||
scripts/
|
||||
gsc_signal_delta.py deterministic L1/L4 GSC delta + mover ranking
|
||||
test_gsc_signal_delta.py
|
||||
requirements.txt (stdlib only)
|
||||
```
|
||||
|
||||
Target environment: Claude Code only (no desktop/ variant — matches precedent 95/96). Registered in .claude-plugin/marketplace.json under ourdigital-seo.
|
||||
|
||||
**Triggers:** `validate serp signal`, `is this ranking real`, `prove SEO impact`, `SEMrush surge real?`, `signal validation`, `신호 검증`, `순위 변화 진짜?`, `오가닉 급증 검증`.
|
||||
|
||||
**Conventions honored:** no new output directories beyond this approved folder; Notion writes via notion-writer script only; never crawl/audit Marriott for JHR (sameAs reference only); KR deliverables in Korean, English internal notes OK.
|
||||
|
||||
## 8. Non-goals (YAGNI)
|
||||
|
||||
- No cron/scheduler and no snapshot DB (stateless, on-demand). A snapshot store + watchlist monitor is a documented **future option**, built only if proven needed.
|
||||
- Does **not** replace the three instrument skills — it sequences them.
|
||||
- Does **not** fabricate a verdict when data is thin — returns INCONCLUSIVE with a remediation list.
|
||||
- Not a general SEO audit; scoped to validating a specific `(term, entity)` impact question.
|
||||
|
||||
## 9. Future options (explicitly out of v1)
|
||||
|
||||
- Lightweight snapshot store in the existing `dda` SQLite workspace to enable true over-time entity-layer deltas.
|
||||
- Optional scheduled monitor over a `(term, entity)` watchlist that flags anomalies for `adjudicate`.
|
||||
- Multi-engine claim intake (parse a pasted SEMrush/Ahrefs export directly).
|
||||
729
custom-skills/35-seo-signal-validation/PLAN.md
Normal file
729
custom-skills/35-seo-signal-validation/PLAN.md
Normal file
@@ -0,0 +1,729 @@
|
||||
# SEO Signal Validation — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the `35-seo-signal-validation` Claude Skill — a conductor that adjudicates whether a claimed SERP / Knowledge-Graph movement for a `(term, entity)` pair is real, misattributed, an artifact, or unprovable.
|
||||
|
||||
**Architecture:** A self-contained `SKILL.md` carries the decision procedure (entity classification → 4-layer evidence cascade → 4-way verdict). One stdlib Python helper (`gsc_signal_delta.py`) makes the L1/L4 GSC delta + mover-ranking deterministic (the part that was ad-hoc and overflowed context in the genesis case). The skill delegates measurement to existing skills (`20-seo-serp-analysis`, `21-seo-position-tracking`, `28-seo-knowledge-graph`) and is registered in the repo's marketplace manifest.
|
||||
|
||||
**Tech Stack:** Markdown skill (Claude Code format), Python 3 stdlib (`json`, `argparse`, `csv`), repo `.claude-plugin/marketplace.json`. No third-party deps. Code-only skill (no `desktop/` variant — matches precedent `95-ourdigital-presales-seo`, `96-ourdigital-estimate-engine`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Skill structure** = root `SKILL.md` (self-contained, ~180–220 lines, content NOT split into `references/`) + `code/` (CLAUDE.md + scripts). Code-only; **no `desktop/` variant**.
|
||||
- **Register** the skill in `.claude-plugin/marketplace.json` under the `ourdigital-seo` plugin's `skills` array as `./custom-skills/35-seo-signal-validation`.
|
||||
- **No new output directories** beyond the approved `custom-skills/35-seo-signal-validation/` folder (and its `code/scripts/fixtures/`).
|
||||
- **Stateless, on-demand**: no cron/scheduler, no snapshot DB.
|
||||
- **Notion writes via the notion-writer script only** — never Notion MCP write tools.
|
||||
- **Never crawl/audit Marriott** for JHR — `sameAs` reference only.
|
||||
- **Verify any Wikidata QID** against `Special:EntityData/{Q}.json` labels before trusting it (false-match guard: Q109455878 ≠ hotel, Q490787 ≠ Shinsegae Group).
|
||||
- **Data-trust hierarchy**: 1st-party measured (GSC/GA4) > 3rd-party measured (backlinks, crawled rank) > 3rd-party modeled (estimated traffic).
|
||||
- **Confidence cap**: third-party entities (no GSC/GA4 access) cannot reach `CONFIRMED` on traffic claims — at most `PARTIAL`, `ARTIFACT` only when live+entity reality clearly contradicts.
|
||||
- **Verdict taxonomy**: `CONFIRMED | PARTIAL | ARTIFACT | INCONCLUSIVE`.
|
||||
- **Branch**: all work commits to `feat/seo-signal-validation-skill` (already created; `DESIGN.md` already committed there).
|
||||
- **Any client deliverable the skill emits** uses naming `{CODE}-{desc}-{class}-{YYYYMMDD}.{ext}`; KR client-facing content in Korean.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `SKILL.md` — measurement half (frontmatter, classification, cascade L1–L2)
|
||||
|
||||
**Files:**
|
||||
- Create: `custom-skills/35-seo-signal-validation/SKILL.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing (first task).
|
||||
- Produces: the `SKILL.md` file with frontmatter `name: seo-signal-validation`; section anchors `## Step 0`, `## The validation loop` with layers `L1`/`L2`; references the helper script path `code/scripts/gsc_signal_delta.py` (implemented in Task 3).
|
||||
|
||||
- [ ] **Step 1: Write `SKILL.md` frontmatter + measurement sections**
|
||||
|
||||
````markdown
|
||||
---
|
||||
name: seo-signal-validation
|
||||
description: |
|
||||
Validate whether a claimed SERP / Knowledge-Graph movement for a (term, entity)
|
||||
is real, misattributed, an artifact, or unprovable — before reporting impact.
|
||||
Triggers: validate serp signal, is this ranking real, prove SEO impact,
|
||||
SEMrush surge real, signal validation, real impact check,
|
||||
신호 검증, 순위 변화 진짜, 오가닉 급증 검증, 임팩트 검증.
|
||||
---
|
||||
|
||||
# SEO Signal Validation
|
||||
|
||||
## Purpose
|
||||
|
||||
Given a `(term/intent, entity)` pair — and optionally a **claim** (a third-party
|
||||
tool's reported movement) or a **baseline** (a prior state) — return an
|
||||
evidence-backed verdict on whether SERP and Knowledge-Graph impact is real.
|
||||
Built because modeled third-party signals (SEMrush/Ahrefs estimated organic
|
||||
traffic, position snapshots) are easy to over-trust. This skill makes the
|
||||
measured → live → entity → attribution cascade a single repeatable procedure
|
||||
ending in a defensible verdict and a client-safe narrative.
|
||||
|
||||
## When to use (boundary)
|
||||
|
||||
This is the **conductor**, not an instrument. It sequences and synthesizes the
|
||||
three measurement skills — it does not duplicate them.
|
||||
|
||||
| Use instead | When |
|
||||
|---|---|
|
||||
| `20-seo-serp-analysis` | You only need SERP composition / features |
|
||||
| `21-seo-position-tracking` | You only need rank over time |
|
||||
| `28-seo-knowledge-graph` | You only need an entity-presence audit |
|
||||
| **this skill** | You must adjudicate whether a *claimed movement* is real across layers |
|
||||
|
||||
## Step 0 — Classify entity + pick mode
|
||||
|
||||
1. **Entity ownership** (gates which layers exist):
|
||||
- **First-party** — a site/property you own or have GSC/GA4 access to (e.g. JHR
|
||||
`sc-domain:josunhotel.com`, GA4 `258308769`) → **L1 measured available**.
|
||||
- **Third-party** — a competitor brand or a person you do not control →
|
||||
**L1 unavailable**; lean on L2 + L3 + clearly-tiered estimates; apply the
|
||||
confidence cap (see Verdict). If unclear, ask once.
|
||||
2. **Mode** (thin wrappers over the same cascade):
|
||||
- `adjudicate(claim)` — a 3rd-party tool reports a move; confirm/refute.
|
||||
- `prove(baseline)` — after our change; before/after from GSC/GA4 history.
|
||||
- `snapshot()` — no claim; "where do we really stand."
|
||||
|
||||
## The validation loop (cost-ordered cascade, short-circuiting)
|
||||
|
||||
Run cheapest-first; stop early when a layer is already decisive.
|
||||
|
||||
### L1 — Measured (first-party ground truth) → via `21-seo-position-tracking`
|
||||
|
||||
- **GSC** `mcp__dda__gsc_fetch_performance`: the term at **query level** (exact)
|
||||
AND **site-wide**, for **recent vs prior** windows. Pull clicks / impressions /
|
||||
position / CTR. **Day-normalize** (compare windows differ in calendar-day count).
|
||||
Note **~43% query-level anonymization** — the disclosed subset ≠ the whole.
|
||||
- **GA4** `mcp__dda__ga4_run_report`: `Organic Search` sessions monthly trend
|
||||
(dims `yearMonth` + `sessionDefaultChannelGroup`, metric `sessions`). GA4
|
||||
includes Naver + all engines — use it to test whether a "surge" exceeds normal
|
||||
month-to-month variance.
|
||||
- **Compute deltas with the helper** (deterministic, avoids ad-hoc parsing):
|
||||
save each GSC pull, then run
|
||||
`python3 code/scripts/gsc_signal_delta.py --recent <recent.tsv> --prior <prior.tsv> --recent-days N --prior-days M --claim-term "<term>"`.
|
||||
It returns day-normalized site totals, top gainers/decliners, and whether the
|
||||
claimed term is among the real movers.
|
||||
- **SHORT-CIRCUIT:** if the claimed keyword has trivial clicks and a real
|
||||
position nowhere near the claim → **ARTIFACT**; stop unless the caller wants
|
||||
the full picture.
|
||||
|
||||
### L2 — Live SERP (3rd-party measured, point-in-time) → via `20-seo-serp-analysis`
|
||||
|
||||
- **Geo-correct Google render** via `claude-in-chrome` (`navigate` → `read_page`):
|
||||
force `gl`/`hl` + correct geo, `pws=0`; **decline precise-location prompts**.
|
||||
Confirm whether the domain actually holds the claimed position; capture the
|
||||
feature landscape (ads, local map-pack, PAA, knowledge panel) that explains why
|
||||
a brand site can't own a head term.
|
||||
- **Cheap rank spot-check**: `mcp__ourseo__check_serp(keyword, domain)`.
|
||||
- **[KR market]** Naver SERP composition: `our research naver serp` (blog / cafe /
|
||||
지식iN / Smart Store / brand zone) — Semrush/Ahrefs don't model Naver.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Verify frontmatter parses and required anchors exist**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
python3 - <<'PY'
|
||||
import sys, pathlib
|
||||
p = pathlib.Path("custom-skills/35-seo-signal-validation/SKILL.md")
|
||||
t = p.read_text(encoding="utf-8")
|
||||
assert t.startswith("---\n"), "missing frontmatter"
|
||||
fm = t.split("---\n",2)[1]
|
||||
assert "name: seo-signal-validation" in fm, "bad name"
|
||||
assert "Triggers:" in fm, "missing triggers"
|
||||
for anchor in ["## Step 0", "## The validation loop", "### L1", "### L2",
|
||||
"gsc_signal_delta.py"]:
|
||||
assert anchor in t, f"missing: {anchor}"
|
||||
print("OK SKILL.md measurement half")
|
||||
PY
|
||||
```
|
||||
Expected: `OK SKILL.md measurement half`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
git add custom-skills/35-seo-signal-validation/SKILL.md
|
||||
git commit -m "feat(skill): seo-signal-validation SKILL.md measurement half (L1-L2)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `SKILL.md` — decision half (L3 KG, L4 synthesis, verdict, output)
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/35-seo-signal-validation/SKILL.md` (append after L2)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the `SKILL.md` from Task 1 (appends to it).
|
||||
- Produces: sections `### L3`, `### L4`, `## Verdict`, `## Standing skepticism rules`, `## Output`, `## Non-goals` with the four verdict labels verbatim.
|
||||
|
||||
- [ ] **Step 1: Append the decision sections to `SKILL.md`**
|
||||
|
||||
````markdown
|
||||
### L3 — Entity / Knowledge Graph → via `28-seo-knowledge-graph`
|
||||
|
||||
A real impact event should leave corroborating traces in the entity layer, not
|
||||
just a rank number. Five checks:
|
||||
|
||||
1. **Google KG API** entity match + `resultScore` —
|
||||
`mcp__ourseo__search_knowledge_graph(query)` (uses `GOOGLE_KG_API_KEY`).
|
||||
2. **Wikidata** QID presence + key claims — **verify the QID against
|
||||
`Special:EntityData/{Q}.json` labels before trusting it** (false-match guard:
|
||||
Q109455878 = office tower ≠ hotel; Q490787 = Shinsegae Inc. ≠ Group).
|
||||
3. **Knowledge Panel** presence/attributes on the live entity-name SERP (Chrome).
|
||||
4. **sameAs** consistency on the entity's `Organization`/`Person` JSON-LD.
|
||||
5. **[KR]** Naver 백과사전 / 지식iN presence.
|
||||
|
||||
`mcp__ourseo__monitor_brand` supplements with brand-mention / brand-SERP ownership.
|
||||
|
||||
### L4 — Attribution synthesis
|
||||
|
||||
Cross-check: does the **measured delta (L1)** corroborate the **live reality
|
||||
(L2)**, and does the **entity layer (L3)** move consistently? The query-clicks
|
||||
delta names the true drivers (brand/seasonal vs the claimed term).
|
||||
|
||||
## Verdict
|
||||
|
||||
| Verdict | Condition |
|
||||
|---|---|
|
||||
| **CONFIRMED** | Measured + live + (where relevant) entity all corroborate movement attributable to the term/intent |
|
||||
| **PARTIAL** | Real movement, but misattributed, or only some layers agree |
|
||||
| **ARTIFACT** | Modeling/snapshot artifact — measured + live reality don't support it |
|
||||
| **INCONCLUSIVE** | Insufficient data (query anonymized, GSC lag, no entity baseline, third-party entity with no measured access) — name what's missing + how to resolve |
|
||||
|
||||
**Confidence cap:** third-party entities (no L1) cannot reach CONFIRMED on traffic
|
||||
claims — at most PARTIAL; ARTIFACT only when live+entity clearly contradict.
|
||||
|
||||
Every verdict ships an **evidence ledger** (per layer: finding + data-trust tier +
|
||||
corroborates/contradicts) and a **client-safe narrative** (the defensible story).
|
||||
|
||||
## Standing skepticism rules
|
||||
|
||||
- Estimated organic traffic = **smoke-detector, not scale** (Σ est-volume × position-CTR curve).
|
||||
- **Head-term over-fire**: one high-volume keyword at an estimated high rank inflates the whole modeled number.
|
||||
- **KR Naver blind spot**: Semrush models Google only; misses much of Korean organic.
|
||||
- **Single-geo/device snapshot** diverges from GSC's national average.
|
||||
- **Trust hierarchy**: 1st-party measured > 3rd-party measured > 3rd-party modeled.
|
||||
|
||||
## Output
|
||||
|
||||
- **Always**: inline report — verdict + evidence ledger + client-safe narrative +
|
||||
"what would raise confidence."
|
||||
- **Optional**: archive to Notion *Working with AI DB* (`data_source_id
|
||||
f8f19ede-32bd-43ac-9f60-0651f6f40afe`) via the **notion-writer script** (never
|
||||
Notion MCP write). Type=Memo/Research, Topic=SEO, Account Code as relevant.
|
||||
- **Optional**: if a new generalizable gotcha emerges, append a memory entry to
|
||||
the active workspace's memory dir.
|
||||
|
||||
## Non-goals
|
||||
|
||||
No cron/scheduler, no snapshot DB, no new directories. Does not replace the three
|
||||
instrument skills. Returns INCONCLUSIVE rather than fabricating when data is thin.
|
||||
**Never crawls/audits Marriott for JHR** (sameAs only).
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Verify the four verdicts, confidence cap, and skepticism rules are present**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
python3 - <<'PY'
|
||||
import pathlib
|
||||
t = pathlib.Path("custom-skills/35-seo-signal-validation/SKILL.md").read_text(encoding="utf-8")
|
||||
for s in ["### L3", "### L4", "**CONFIRMED**", "**PARTIAL**", "**ARTIFACT**",
|
||||
"**INCONCLUSIVE**", "Confidence cap", "smoke-detector, not scale",
|
||||
"Special:EntityData", "## Output", "## Non-goals"]:
|
||||
assert s in t, f"missing: {s}"
|
||||
n = t.count("\n")
|
||||
assert 130 <= n <= 320, f"SKILL.md length {n} lines outside expected band"
|
||||
print(f"OK SKILL.md decision half ({n} lines)")
|
||||
PY
|
||||
```
|
||||
Expected: `OK SKILL.md decision half (… lines)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
git add custom-skills/35-seo-signal-validation/SKILL.md
|
||||
git commit -m "feat(skill): seo-signal-validation SKILL.md decision half (L3-L4, verdict, output)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `gsc_signal_delta.py` helper + tests (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create test: `custom-skills/35-seo-signal-validation/code/scripts/test_gsc_signal_delta.py`
|
||||
- Create: `custom-skills/35-seo-signal-validation/code/scripts/gsc_signal_delta.py`
|
||||
- Create: `custom-skills/35-seo-signal-validation/code/scripts/requirements.txt`
|
||||
- Create: `custom-skills/35-seo-signal-validation/code/CLAUDE.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing at runtime.
|
||||
- Produces: `compute_delta(recent: list[dict], prior: list[dict], recent_days: int, prior_days: int, claim_term: str|None=None, top_n: int=10) -> dict` and `load_gsc(path: str) -> list[dict]`; CLI `python3 gsc_signal_delta.py --recent --prior --recent-days --prior-days [--claim-term] [--top-n]`. Output dict keys: `site_totals`, `top_gainers`, `top_decliners`, `claim_term`, `verdict_hint`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `custom-skills/35-seo-signal-validation/code/scripts/test_gsc_signal_delta.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for gsc_signal_delta. Run: `python3 test_gsc_signal_delta.py`
|
||||
(also pytest-compatible). Stdlib only."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from gsc_signal_delta import compute_delta # noqa: E402
|
||||
|
||||
# Genesis fixture: JHR "호텔" — flat head term, growth all brand (2026-06 case).
|
||||
RECENT = [
|
||||
{"query": "호텔", "clicks": 5, "impressions": 572, "position": 11.6},
|
||||
{"query": "grand josun busan", "clicks": 250, "impressions": 4000, "position": 1.2},
|
||||
{"query": "조선호텔", "clicks": 300, "impressions": 6000, "position": 1.1},
|
||||
]
|
||||
PRIOR = [
|
||||
{"query": "호텔", "clicks": 9, "impressions": 371, "position": 18.1},
|
||||
{"query": "grand josun busan", "clicks": 49, "impressions": 1500, "position": 3.4},
|
||||
{"query": "조선호텔", "clicks": 150, "impressions": 5000, "position": 1.3},
|
||||
]
|
||||
|
||||
|
||||
def test_claim_term_flagged_artifact():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30, claim_term="호텔")
|
||||
ct = out["claim_term"]
|
||||
assert ct["found"] is True
|
||||
assert ct["in_top_movers"] is False
|
||||
assert ct["click_share_pct"] < 1.0
|
||||
assert "ARTIFACT" in out["verdict_hint"]
|
||||
|
||||
|
||||
def test_top_gainer_is_brand_term():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30, claim_term="호텔")
|
||||
assert out["top_gainers"][0]["query"] == "grand josun busan"
|
||||
assert out["top_gainers"][0]["delta_clicks"] == 201
|
||||
|
||||
|
||||
def test_day_normalization():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30)
|
||||
assert out["site_totals"]["recent"]["clicks_per_day"] == 19.82 # 555/28
|
||||
assert out["site_totals"]["prior"]["clicks_per_day"] == 6.93 # 208/30
|
||||
|
||||
|
||||
def test_positive_days_required():
|
||||
try:
|
||||
compute_delta(RECENT, PRIOR, 0, 30)
|
||||
except ValueError:
|
||||
return
|
||||
raise AssertionError("expected ValueError for non-positive days")
|
||||
|
||||
|
||||
def _run():
|
||||
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
for fn in fns:
|
||||
fn(); print(f"PASS {fn.__name__}")
|
||||
print(f"\n{len(fns)} passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_run()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills/custom-skills/35-seo-signal-validation/code/scripts
|
||||
python3 test_gsc_signal_delta.py
|
||||
```
|
||||
Expected: FAIL — `ModuleNotFoundError: No module named 'gsc_signal_delta'`
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
Create `custom-skills/35-seo-signal-validation/code/scripts/gsc_signal_delta.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Day-normalized GSC query delta + mover ranking for signal validation.
|
||||
|
||||
Reads two GSC query exports (recent, prior) — JSON list or TSV with a header row
|
||||
containing query / clicks / impressions / position — and reports day-normalized
|
||||
site totals, top gainers/decliners, and whether a claimed term is a real mover.
|
||||
This is the deterministic L1/L4 core of the 35-seo-signal-validation skill.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _norm_row(r: dict) -> dict:
|
||||
def num(*keys, default=0.0):
|
||||
for k in keys:
|
||||
if k in r and r[k] not in (None, ""):
|
||||
try:
|
||||
return float(str(r[k]).replace(",", ""))
|
||||
except ValueError:
|
||||
pass
|
||||
return default
|
||||
query = (r.get("query") or r.get("term") or "")
|
||||
if isinstance(r.get("keys"), list) and r["keys"]:
|
||||
query = str(r["keys"][0])
|
||||
return {
|
||||
"query": str(query).strip(),
|
||||
"clicks": num("clicks"),
|
||||
"impressions": num("impressions", "impr"),
|
||||
"position": num("position", "pos", default=0.0),
|
||||
}
|
||||
|
||||
|
||||
def load_gsc(path: str) -> list[dict]:
|
||||
"""Parse a GSC export (JSON list/{rows:[...]} or TSV-with-header)."""
|
||||
text = Path(path).read_text(encoding="utf-8").strip()
|
||||
if not text:
|
||||
return []
|
||||
if text[0] in "[{":
|
||||
data = json.loads(text)
|
||||
if isinstance(data, dict):
|
||||
data = data.get("rows", [])
|
||||
return [_norm_row(r) for r in data]
|
||||
lines = text.splitlines()
|
||||
header = [h.strip().lower() for h in lines[0].split("\t")]
|
||||
rows = []
|
||||
for line in lines[1:]:
|
||||
if line.strip():
|
||||
rows.append(_norm_row(dict(zip(header, line.split("\t")))))
|
||||
return rows
|
||||
|
||||
|
||||
def _by_query(rows: list[dict]) -> dict:
|
||||
return {r["query"]: r for r in rows if r["query"]}
|
||||
|
||||
|
||||
def compute_delta(recent, prior, recent_days, prior_days,
|
||||
claim_term=None, top_n=10) -> dict:
|
||||
if recent_days <= 0 or prior_days <= 0:
|
||||
raise ValueError("recent_days and prior_days must be positive")
|
||||
r_by, p_by = _by_query(recent), _by_query(prior)
|
||||
|
||||
def totals(rows):
|
||||
return {"clicks": sum(r["clicks"] for r in rows),
|
||||
"impressions": sum(r["impressions"] for r in rows)}
|
||||
rt, pt = totals(recent), totals(prior)
|
||||
|
||||
def per_day(total, days):
|
||||
return round(total / days, 2)
|
||||
|
||||
def pct(new, old):
|
||||
return round((new - old) / old * 100, 1) if old else None
|
||||
|
||||
r_cpd, p_cpd = per_day(rt["clicks"], recent_days), per_day(pt["clicks"], prior_days)
|
||||
r_ipd, p_ipd = per_day(rt["impressions"], recent_days), per_day(pt["impressions"], prior_days)
|
||||
|
||||
deltas = []
|
||||
for q in set(r_by) | set(p_by):
|
||||
rc = r_by.get(q, {}).get("clicks", 0.0)
|
||||
pc = p_by.get(q, {}).get("clicks", 0.0)
|
||||
deltas.append({"query": q, "delta_clicks": rc - pc,
|
||||
"recent_clicks": rc, "prior_clicks": pc})
|
||||
deltas.sort(key=lambda d: d["delta_clicks"], reverse=True)
|
||||
gainers = [d for d in deltas if d["delta_clicks"] > 0][:top_n]
|
||||
decliners = sorted([d for d in deltas if d["delta_clicks"] < 0],
|
||||
key=lambda d: d["delta_clicks"])[:top_n]
|
||||
|
||||
out = {
|
||||
"site_totals": {
|
||||
"recent": {**rt, "clicks_per_day": r_cpd,
|
||||
"impressions_per_day": r_ipd, "days": recent_days},
|
||||
"prior": {**pt, "clicks_per_day": p_cpd,
|
||||
"impressions_per_day": p_ipd, "days": prior_days},
|
||||
"clicks_per_day_pct": pct(r_cpd, p_cpd),
|
||||
"impressions_per_day_pct": pct(r_ipd, p_ipd),
|
||||
},
|
||||
"top_gainers": gainers,
|
||||
"top_decliners": decliners,
|
||||
"claim_term": None,
|
||||
"verdict_hint": None,
|
||||
}
|
||||
|
||||
if claim_term:
|
||||
gainer_terms = {g["query"] for g in gainers}
|
||||
rc, pc = r_by.get(claim_term, {}), p_by.get(claim_term, {})
|
||||
in_movers = claim_term in gainer_terms
|
||||
share = (rc.get("clicks", 0.0) / rt["clicks"] * 100) if rt["clicks"] else 0.0
|
||||
out["claim_term"] = {
|
||||
"term": claim_term, "found": bool(rc or pc),
|
||||
"recent": {"clicks": rc.get("clicks", 0.0),
|
||||
"impressions": rc.get("impressions", 0.0),
|
||||
"position": rc.get("position")},
|
||||
"prior": {"clicks": pc.get("clicks", 0.0),
|
||||
"impressions": pc.get("impressions", 0.0),
|
||||
"position": pc.get("position")},
|
||||
"in_top_movers": in_movers,
|
||||
"click_share_pct": round(share, 2),
|
||||
}
|
||||
if not in_movers and share < 1.0:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' contributes {share:.2f}% of recent clicks and is "
|
||||
f"absent from top movers -> claimed impact likely ARTIFACT; real "
|
||||
f"movement is elsewhere (see top_gainers).")
|
||||
elif in_movers:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' is among top movers -> claim plausibly CONFIRMED/"
|
||||
f"PARTIAL; corroborate with live SERP + entity layer.")
|
||||
else:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' has non-trivial share ({share:.2f}%) but is not a "
|
||||
f"top mover -> PARTIAL; inspect attribution.")
|
||||
return out
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="GSC signal delta for signal validation")
|
||||
ap.add_argument("--recent", required=True)
|
||||
ap.add_argument("--prior", required=True)
|
||||
ap.add_argument("--recent-days", type=int, required=True)
|
||||
ap.add_argument("--prior-days", type=int, required=True)
|
||||
ap.add_argument("--claim-term", default=None)
|
||||
ap.add_argument("--top-n", type=int, default=10)
|
||||
a = ap.parse_args(argv)
|
||||
out = compute_delta(load_gsc(a.recent), load_gsc(a.prior),
|
||||
a.recent_days, a.prior_days, a.claim_term, a.top_n)
|
||||
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills/custom-skills/35-seo-signal-validation/code/scripts
|
||||
python3 test_gsc_signal_delta.py
|
||||
```
|
||||
Expected: `PASS test_claim_term_flagged_artifact` … `4 passed`
|
||||
|
||||
- [ ] **Step 5: Create `requirements.txt` and `code/CLAUDE.md`**
|
||||
|
||||
Create `custom-skills/35-seo-signal-validation/code/scripts/requirements.txt`:
|
||||
```text
|
||||
# gsc_signal_delta.py uses the Python 3 standard library only — no deps.
|
||||
```
|
||||
|
||||
Create `custom-skills/35-seo-signal-validation/code/CLAUDE.md`:
|
||||
```markdown
|
||||
# seo-signal-validation — code environment notes
|
||||
|
||||
## Helper: scripts/gsc_signal_delta.py
|
||||
Deterministic L1/L4 GSC delta. Feed it two saved GSC query exports (recent,
|
||||
prior) as JSON or TSV (columns: query, clicks, impressions, position).
|
||||
|
||||
```bash
|
||||
python3 scripts/gsc_signal_delta.py \
|
||||
--recent recent.tsv --prior prior.tsv \
|
||||
--recent-days 28 --prior-days 30 --claim-term "호텔"
|
||||
```
|
||||
Returns day-normalized site totals, top gainers/decliners, and a `verdict_hint`
|
||||
(heuristic only — the final verdict is the skill's job, after L2/L3).
|
||||
|
||||
## Getting the exports
|
||||
`mcp__dda__gsc_fetch_performance` (property pinned per workspace, e.g. JHR
|
||||
`sc-domain:josunhotel.com`) → save the query-dimension rows to a file → run the
|
||||
script. GSC anonymizes ~43% of query clicks; the disclosed subset ≠ the whole.
|
||||
|
||||
## Env / access
|
||||
- `GOOGLE_KG_API_KEY` for `mcp__ourseo__search_knowledge_graph` (L3).
|
||||
- GSC/GA4 only exist for first-party properties — third-party entities skip L1.
|
||||
- Never crawl/audit Marriott for JHR (sameAs only).
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
git add custom-skills/35-seo-signal-validation/code
|
||||
git commit -m "feat(skill): gsc_signal_delta helper + tests + code notes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Register in marketplace + reconcile DESIGN.md structure
|
||||
|
||||
**Files:**
|
||||
- Modify: `.claude-plugin/marketplace.json` (add to `ourdigital-seo` → `skills`)
|
||||
- Modify: `custom-skills/35-seo-signal-validation/DESIGN.md:§7` (replace `references/` layout with actual `code/` layout; mark Code-only)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the skill folder from Tasks 1–3.
|
||||
- Produces: a registered, discoverable skill; a spec whose §7 matches the built structure.
|
||||
|
||||
- [ ] **Step 1: Add the skill path to the manifest**
|
||||
|
||||
In `.claude-plugin/marketplace.json`, inside the `ourdigital-seo` plugin's
|
||||
`skills` array, add (keep numeric order; insert after the `34-seo-reporting-dashboard` entry):
|
||||
```json
|
||||
"./custom-skills/35-seo-signal-validation",
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the manifest still parses and contains the entry**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
python3 - <<'PY'
|
||||
import json, pathlib
|
||||
m = json.loads(pathlib.Path(".claude-plugin/marketplace.json").read_text())
|
||||
seo = next(p for p in m["plugins"] if p["name"] == "ourdigital-seo")
|
||||
assert "./custom-skills/35-seo-signal-validation" in seo["skills"], "not registered"
|
||||
print("OK manifest valid + skill registered")
|
||||
PY
|
||||
```
|
||||
Expected: `OK manifest valid + skill registered`
|
||||
|
||||
- [ ] **Step 3: Reconcile `DESIGN.md` §7 with the real structure**
|
||||
|
||||
In `custom-skills/35-seo-signal-validation/DESIGN.md`, replace the §7 "Repo
|
||||
layout & conventions" code block (the `references/...` sketch) with:
|
||||
```text
|
||||
35-seo-signal-validation/
|
||||
SKILL.md self-contained: classification, 4-layer cascade,
|
||||
5 KG checks, 4-way verdict, skepticism rules, output
|
||||
DESIGN.md PLAN.md spec + plan (live with the skill; no new top-level dir)
|
||||
code/
|
||||
CLAUDE.md code-environment notes (env, export→script flow)
|
||||
scripts/
|
||||
gsc_signal_delta.py deterministic L1/L4 GSC delta + mover ranking
|
||||
test_gsc_signal_delta.py
|
||||
requirements.txt (stdlib only)
|
||||
```
|
||||
And add one line under it: `Target environment: Claude Code only (no desktop/ variant — matches precedent 95/96). Registered in .claude-plugin/marketplace.json under ourdigital-seo.`
|
||||
|
||||
- [ ] **Step 4: Verify the spec no longer references the old layout**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
python3 - <<'PY'
|
||||
import pathlib
|
||||
t = pathlib.Path("custom-skills/35-seo-signal-validation/DESIGN.md").read_text(encoding="utf-8")
|
||||
assert "references/\n evidence-cascade.md" not in t, "old layout still present"
|
||||
assert "gsc_signal_delta.py" in t and "marketplace.json" in t, "structure not reconciled"
|
||||
print("OK DESIGN.md §7 reconciled")
|
||||
PY
|
||||
```
|
||||
Expected: `OK DESIGN.md §7 reconciled`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
git add .claude-plugin/marketplace.json custom-skills/35-seo-signal-validation/DESIGN.md
|
||||
git commit -m "feat(skill): register seo-signal-validation in marketplace; reconcile spec layout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Smoke test — genesis case end-to-end + consistency gate
|
||||
|
||||
**Files:**
|
||||
- Create: `custom-skills/35-seo-signal-validation/code/scripts/fixtures/jhr-hotel-recent.tsv`
|
||||
- Create: `custom-skills/35-seo-signal-validation/code/scripts/fixtures/jhr-hotel-prior.tsv`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `gsc_signal_delta.py` CLI (Task 3) and the full `SKILL.md` (Tasks 1–2).
|
||||
- Produces: a reproducible CLI smoke run proving the genesis "호텔" case yields an ARTIFACT-leaning hint; a final spec↔skill consistency check.
|
||||
|
||||
- [ ] **Step 1: Create the genesis fixtures (TSV)**
|
||||
|
||||
Create `custom-skills/35-seo-signal-validation/code/scripts/fixtures/jhr-hotel-recent.tsv`:
|
||||
```text
|
||||
query clicks impressions position
|
||||
호텔 5 572 11.6
|
||||
grand josun busan 250 4000 1.2
|
||||
조선호텔 300 6000 1.1
|
||||
```
|
||||
|
||||
Create `custom-skills/35-seo-signal-validation/code/scripts/fixtures/jhr-hotel-prior.tsv`:
|
||||
```text
|
||||
query clicks impressions position
|
||||
호텔 9 371 18.1
|
||||
grand josun busan 49 1500 3.4
|
||||
조선호텔 150 5000 1.3
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the CLI end-to-end and verify the verdict hint**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills/custom-skills/35-seo-signal-validation/code/scripts
|
||||
python3 gsc_signal_delta.py --recent fixtures/jhr-hotel-recent.tsv \
|
||||
--prior fixtures/jhr-hotel-prior.tsv --recent-days 28 --prior-days 30 \
|
||||
--claim-term "호텔" | python3 - <<'PY'
|
||||
import json, sys
|
||||
o = json.load(sys.stdin)
|
||||
assert o["claim_term"]["in_top_movers"] is False
|
||||
assert o["claim_term"]["click_share_pct"] < 1.0
|
||||
assert "ARTIFACT" in o["verdict_hint"]
|
||||
assert o["top_gainers"][0]["query"] == "grand josun busan"
|
||||
print("OK smoke: 호텔 → ARTIFACT-leaning; top mover = brand term")
|
||||
PY
|
||||
```
|
||||
Expected: `OK smoke: 호텔 → ARTIFACT-leaning; top mover = brand term`
|
||||
|
||||
- [ ] **Step 3: Consistency gate — every spec default maps to skill content**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
python3 - <<'PY'
|
||||
import pathlib
|
||||
sk = pathlib.Path("custom-skills/35-seo-signal-validation/SKILL.md").read_text(encoding="utf-8")
|
||||
# Default 1: 5 KG checks Default 2: 4 verdicts Default 3: output triple
|
||||
for s in ["Google KG API", "Wikidata", "Knowledge Panel", "sameAs", "지식iN"]:
|
||||
assert s in sk, f"KG check missing: {s}"
|
||||
for s in ["CONFIRMED", "PARTIAL", "ARTIFACT", "INCONCLUSIVE"]:
|
||||
assert s in sk, f"verdict missing: {s}"
|
||||
for s in ["notion-writer", "evidence ledger", "client-safe narrative"]:
|
||||
assert s.lower() in sk.lower(), f"output element missing: {s}"
|
||||
# Default 5: triggers (KR + EN)
|
||||
assert "신호 검증" in sk and "validate serp signal" in sk, "triggers missing"
|
||||
print("OK all five approved defaults present in SKILL.md")
|
||||
PY
|
||||
```
|
||||
Expected: `OK all five approved defaults present in SKILL.md`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills
|
||||
git add custom-skills/35-seo-signal-validation/code/scripts/fixtures
|
||||
git commit -m "test(skill): genesis 호텔 smoke fixtures + end-to-end ARTIFACT check"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**1. Spec coverage** (each DESIGN.md section → task):
|
||||
- §1 Purpose, §2 Boundary → Task 1 (SKILL.md Purpose/boundary). ✓
|
||||
- §3 Engine (entity classification, L1–L4, short-circuit) → Tasks 1 (L1–L2) + 2 (L3–L4); L1/L4 delta computation → Task 3 script. ✓
|
||||
- §4 Modes → Task 1 (Step 0 mode dispatch). ✓
|
||||
- §5 Verdict + skepticism + confidence cap → Task 2. ✓
|
||||
- §6 Output → Task 2. ✓
|
||||
- §7 Repo layout → corrected in Task 4 (was wrong in spec); registration in Task 4. ✓
|
||||
- §8 Non-goals → Task 2. ✓
|
||||
- §9 Future options → intentionally not implemented (YAGNI). ✓
|
||||
- Genesis verification → Task 5 smoke. ✓
|
||||
|
||||
**2. Placeholder scan:** No TBD/TODO; all code blocks complete; every test shows real assertions; commands show expected output. ✓
|
||||
|
||||
**3. Type consistency:** `compute_delta(recent, prior, recent_days, prior_days, claim_term=None, top_n=10)` and `load_gsc(path)` are referenced identically in Task 3 (definition + test) and Task 5 (CLI). Output keys (`site_totals`, `top_gainers`, `claim_term.in_top_movers`, `claim_term.click_share_pct`, `verdict_hint`) match across the implementation, the tests, and both smoke checks. Day-normalization fixtures (555/28=19.82, 208/30=6.93; gainer delta 201) are arithmetically consistent. ✓
|
||||
|
||||
**No gaps found.**
|
||||
141
custom-skills/35-seo-signal-validation/SKILL.md
Normal file
141
custom-skills/35-seo-signal-validation/SKILL.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: seo-signal-validation
|
||||
description: |
|
||||
Validate whether a claimed SERP / Knowledge-Graph movement for a (term, entity)
|
||||
is real, misattributed, an artifact, or unprovable — before reporting impact.
|
||||
Triggers: validate serp signal, is this ranking real, prove SEO impact,
|
||||
SEMrush surge real, signal validation, real impact check,
|
||||
신호 검증, 순위 변화 진짜, 오가닉 급증 검증, 임팩트 검증.
|
||||
---
|
||||
|
||||
# SEO Signal Validation
|
||||
|
||||
## Purpose
|
||||
|
||||
Given a `(term/intent, entity)` pair — and optionally a **claim** (a third-party
|
||||
tool's reported movement) or a **baseline** (a prior state) — return an
|
||||
evidence-backed verdict on whether SERP and Knowledge-Graph impact is real.
|
||||
Built because modeled third-party signals (SEMrush/Ahrefs estimated organic
|
||||
traffic, position snapshots) are easy to over-trust. This skill makes the
|
||||
measured → live → entity → attribution cascade a single repeatable procedure
|
||||
ending in a defensible verdict and a client-safe narrative.
|
||||
|
||||
## When to use (boundary)
|
||||
|
||||
This is the **conductor**, not an instrument. It sequences and synthesizes the
|
||||
three measurement skills — it does not duplicate them.
|
||||
|
||||
| Use instead | When |
|
||||
|---|---|
|
||||
| `20-seo-serp-analysis` | You only need SERP composition / features |
|
||||
| `21-seo-position-tracking` | You only need rank over time |
|
||||
| `28-seo-knowledge-graph` | You only need an entity-presence audit |
|
||||
| **this skill** | You must adjudicate whether a *claimed movement* is real across layers |
|
||||
|
||||
## Step 0 — Classify entity + pick mode
|
||||
|
||||
1. **Entity ownership** (gates which layers exist):
|
||||
- **First-party** — a site/property you own or have GSC/GA4 access to (e.g. JHR
|
||||
`sc-domain:josunhotel.com`, GA4 `258308769`) → **L1 measured available**.
|
||||
- **Third-party** — a competitor brand or a person you do not control →
|
||||
**L1 unavailable**; lean on L2 + L3 + clearly-tiered estimates; apply the
|
||||
confidence cap (see Verdict). If unclear, ask once.
|
||||
2. **Mode** (thin wrappers over the same cascade):
|
||||
- `adjudicate(claim)` — a 3rd-party tool reports a move; confirm/refute.
|
||||
- `prove(baseline)` — after our change; before/after from GSC/GA4 history.
|
||||
- `snapshot()` — no claim; "where do we really stand."
|
||||
|
||||
## The validation loop (cost-ordered cascade, short-circuiting)
|
||||
|
||||
Run cheapest-first; stop early when a layer is already decisive.
|
||||
|
||||
### L1 — Measured (first-party ground truth) → via `21-seo-position-tracking`
|
||||
|
||||
- **GSC** `mcp__dda__gsc_fetch_performance`: the term at **query level** (exact)
|
||||
AND **site-wide**, for **recent vs prior** windows. Pull clicks / impressions /
|
||||
position / CTR. **Day-normalize** (compare windows differ in calendar-day count).
|
||||
Note **~43% query-level anonymization** — the disclosed subset ≠ the whole.
|
||||
- **GA4** `mcp__dda__ga4_run_report`: `Organic Search` sessions monthly trend
|
||||
(dims `yearMonth` + `sessionDefaultChannelGroup`, metric `sessions`). GA4
|
||||
includes Naver + all engines — use it to test whether a "surge" exceeds normal
|
||||
month-to-month variance.
|
||||
- **Compute deltas with the helper** (deterministic, avoids ad-hoc parsing):
|
||||
save each GSC pull, then run
|
||||
`python3 code/scripts/gsc_signal_delta.py --recent <recent.tsv> --prior <prior.tsv> --recent-days N --prior-days M --claim-term "<term>"`.
|
||||
It returns day-normalized site totals, top gainers/decliners, and whether the
|
||||
claimed term is among the real movers.
|
||||
- **SHORT-CIRCUIT:** if the claimed keyword has trivial clicks and a real
|
||||
position nowhere near the claim → **ARTIFACT**; stop unless the caller wants
|
||||
the full picture.
|
||||
|
||||
### L2 — Live SERP (3rd-party measured, point-in-time) → via `20-seo-serp-analysis`
|
||||
|
||||
- **Geo-correct Google render** via `claude-in-chrome` (`navigate` → `read_page`):
|
||||
force `gl`/`hl` + correct geo, `pws=0`; **decline precise-location prompts**.
|
||||
Confirm whether the domain actually holds the claimed position; capture the
|
||||
feature landscape (ads, local map-pack, PAA, knowledge panel) that explains why
|
||||
a brand site can't own a head term.
|
||||
- **Cheap rank spot-check**: `mcp__ourseo__check_serp(keyword, domain)`.
|
||||
- **[KR market]** Naver SERP composition: `our research naver serp` (blog / cafe /
|
||||
지식iN / Smart Store / brand zone) — Semrush/Ahrefs don't model Naver.
|
||||
|
||||
### L3 — Entity / Knowledge Graph → via `28-seo-knowledge-graph`
|
||||
|
||||
A real impact event should leave corroborating traces in the entity layer, not
|
||||
just a rank number. Five checks:
|
||||
|
||||
1. **Google KG API** entity match + `resultScore` —
|
||||
`mcp__ourseo__search_knowledge_graph(query)` (uses `GOOGLE_KG_API_KEY`).
|
||||
2. **Wikidata** QID presence + key claims — **verify the QID against
|
||||
`Special:EntityData/{Q}.json` labels before trusting it** (false-match guard:
|
||||
Q109455878 = office tower ≠ hotel; Q490787 = Shinsegae Inc. ≠ Group).
|
||||
3. **Knowledge Panel** presence/attributes on the live entity-name SERP (Chrome).
|
||||
4. **sameAs** consistency on the entity's `Organization`/`Person` JSON-LD.
|
||||
5. **[KR]** Naver 백과사전 / 지식iN presence.
|
||||
|
||||
`mcp__ourseo__monitor_brand` supplements with brand-mention / brand-SERP ownership.
|
||||
|
||||
### L4 — Attribution synthesis
|
||||
|
||||
Cross-check: does the **measured delta (L1)** corroborate the **live reality
|
||||
(L2)**, and does the **entity layer (L3)** move consistently? The query-clicks
|
||||
delta names the true drivers (brand/seasonal vs the claimed term).
|
||||
|
||||
## Verdict
|
||||
|
||||
| Verdict | Condition |
|
||||
|---|---|
|
||||
| **CONFIRMED** | Measured + live + (where relevant) entity all corroborate movement attributable to the term/intent |
|
||||
| **PARTIAL** | Real movement, but misattributed, or only some layers agree |
|
||||
| **ARTIFACT** | Modeling/snapshot artifact — measured + live reality don't support it |
|
||||
| **INCONCLUSIVE** | Insufficient data (query anonymized, GSC lag, no entity baseline, third-party entity with no measured access) — name what's missing + how to resolve |
|
||||
|
||||
**Confidence cap:** third-party entities (no L1) cannot reach CONFIRMED on traffic
|
||||
claims — at most PARTIAL; ARTIFACT only when live+entity clearly contradict.
|
||||
|
||||
Every verdict ships an **evidence ledger** (per layer: finding + data-trust tier +
|
||||
corroborates/contradicts) and a **client-safe narrative** (the defensible story).
|
||||
|
||||
## Standing skepticism rules
|
||||
|
||||
- Estimated organic traffic = **smoke-detector, not scale** (Σ est-volume × position-CTR curve).
|
||||
- **Head-term over-fire**: one high-volume keyword at an estimated high rank inflates the whole modeled number.
|
||||
- **KR Naver blind spot**: Semrush models Google only; misses much of Korean organic.
|
||||
- **Single-geo/device snapshot** diverges from GSC's national average.
|
||||
- **Trust hierarchy**: 1st-party measured > 3rd-party measured > 3rd-party modeled.
|
||||
|
||||
## Output
|
||||
|
||||
- **Always**: inline report — verdict + evidence ledger + client-safe narrative +
|
||||
"what would raise confidence."
|
||||
- **Optional**: archive to Notion *Working with AI DB* (`data_source_id
|
||||
f8f19ede-32bd-43ac-9f60-0651f6f40afe`) via the **notion-writer script** (never
|
||||
Notion MCP write). Type=Memo/Research, Topic=SEO, Account Code as relevant.
|
||||
- **Optional**: if a new generalizable gotcha emerges, append a memory entry to
|
||||
the active workspace's memory dir.
|
||||
|
||||
## Non-goals
|
||||
|
||||
No cron/scheduler, no snapshot DB, no new directories. Does not replace the three
|
||||
instrument skills. Returns INCONCLUSIVE rather than fabricating when data is thin.
|
||||
**Never crawls/audits Marriott for JHR** (sameAs only).
|
||||
25
custom-skills/35-seo-signal-validation/code/CLAUDE.md
Normal file
25
custom-skills/35-seo-signal-validation/code/CLAUDE.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# seo-signal-validation — code environment notes
|
||||
|
||||
## Helper: scripts/gsc_signal_delta.py
|
||||
Deterministic L1/L4 GSC delta. Feed it two saved GSC query exports (recent,
|
||||
prior) as JSON or TSV (columns: query, clicks, impressions, position).
|
||||
|
||||
```bash
|
||||
python3 scripts/gsc_signal_delta.py \
|
||||
--recent recent.tsv --prior prior.tsv \
|
||||
--recent-days 28 --prior-days 30 --claim-term "호텔"
|
||||
```
|
||||
Returns day-normalized site totals, top gainers/decliners, and a `verdict_hint`
|
||||
(heuristic only — the final verdict is the skill's job, after L2/L3).
|
||||
|
||||
**Surge-tuning note**: `verdict_hint` and `in_top_movers` are calibrated for upward "surge" claims (movers ranked by click gain). For a claimed *drop*, inspect `top_decliners` directly rather than relying on the hint.
|
||||
|
||||
## Getting the exports
|
||||
`mcp__dda__gsc_fetch_performance` (property pinned per workspace, e.g. JHR
|
||||
`sc-domain:josunhotel.com`) → save the query-dimension rows to a file → run the
|
||||
script. GSC anonymizes ~43% of query clicks; the disclosed subset ≠ the whole.
|
||||
|
||||
## Env / access
|
||||
- `GOOGLE_KG_API_KEY` for `mcp__ourseo__search_knowledge_graph` (L3).
|
||||
- GSC/GA4 only exist for first-party properties — third-party entities skip L1.
|
||||
- Never crawl/audit Marriott for JHR (sameAs only).
|
||||
@@ -0,0 +1,4 @@
|
||||
query clicks impressions position
|
||||
호텔 9 371 18.1
|
||||
grand josun busan 49 1500 3.4
|
||||
조선호텔 150 5000 1.3
|
||||
|
@@ -0,0 +1,4 @@
|
||||
query clicks impressions position
|
||||
호텔 5 572 11.6
|
||||
grand josun busan 250 4000 1.2
|
||||
조선호텔 300 6000 1.1
|
||||
|
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Day-normalized GSC query delta + mover ranking for signal validation.
|
||||
|
||||
Reads two GSC query exports (recent, prior) — JSON list or TSV with a header row
|
||||
containing query / clicks / impressions / position — and reports day-normalized
|
||||
site totals, top gainers/decliners, and whether a claimed term is a real mover.
|
||||
This is the deterministic L1/L4 core of the 35-seo-signal-validation skill.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _norm_row(r: dict) -> dict:
|
||||
def num(*keys, default=0.0):
|
||||
for k in keys:
|
||||
if k in r and r[k] not in (None, ""):
|
||||
try:
|
||||
return float(str(r[k]).replace(",", ""))
|
||||
except ValueError:
|
||||
pass
|
||||
return default
|
||||
query = (r.get("query") or r.get("term") or "")
|
||||
if isinstance(r.get("keys"), list) and r["keys"]:
|
||||
query = str(r["keys"][0])
|
||||
return {
|
||||
"query": str(query).strip(),
|
||||
"clicks": num("clicks"),
|
||||
"impressions": num("impressions", "impr"),
|
||||
"position": num("position", "pos", default=0.0),
|
||||
}
|
||||
|
||||
|
||||
def load_gsc(path: str) -> list[dict]:
|
||||
"""Parse a GSC export (JSON list/{rows:[...]} or TSV-with-header)."""
|
||||
text = Path(path).read_text(encoding="utf-8").strip()
|
||||
if not text:
|
||||
return []
|
||||
if text[0] in "[{":
|
||||
data = json.loads(text)
|
||||
if isinstance(data, dict):
|
||||
data = data.get("rows", [])
|
||||
return [_norm_row(r) for r in data]
|
||||
lines = text.splitlines()
|
||||
header = [h.strip().lower() for h in lines[0].split("\t")]
|
||||
rows = []
|
||||
for line in lines[1:]:
|
||||
if line.strip():
|
||||
rows.append(_norm_row(dict(zip(header, line.split("\t")))))
|
||||
return rows
|
||||
|
||||
|
||||
def _by_query(rows: list[dict]) -> dict:
|
||||
return {r["query"]: r for r in rows if r["query"]}
|
||||
|
||||
|
||||
def compute_delta(recent, prior, recent_days, prior_days,
|
||||
claim_term=None, top_n=10) -> dict:
|
||||
if recent_days <= 0 or prior_days <= 0:
|
||||
raise ValueError("recent_days and prior_days must be positive")
|
||||
r_by, p_by = _by_query(recent), _by_query(prior)
|
||||
|
||||
def totals(rows):
|
||||
return {"clicks": sum(r["clicks"] for r in rows),
|
||||
"impressions": sum(r["impressions"] for r in rows)}
|
||||
rt, pt = totals(recent), totals(prior)
|
||||
|
||||
def per_day(total, days):
|
||||
return round(total / days, 2)
|
||||
|
||||
def pct(new, old):
|
||||
return round((new - old) / old * 100, 1) if old else None
|
||||
|
||||
r_cpd, p_cpd = per_day(rt["clicks"], recent_days), per_day(pt["clicks"], prior_days)
|
||||
r_ipd, p_ipd = per_day(rt["impressions"], recent_days), per_day(pt["impressions"], prior_days)
|
||||
|
||||
deltas = []
|
||||
for q in set(r_by) | set(p_by):
|
||||
rc = r_by.get(q, {}).get("clicks", 0.0)
|
||||
pc = p_by.get(q, {}).get("clicks", 0.0)
|
||||
deltas.append({"query": q, "delta_clicks": rc - pc,
|
||||
"recent_clicks": rc, "prior_clicks": pc})
|
||||
deltas.sort(key=lambda d: d["delta_clicks"], reverse=True)
|
||||
gainers = [d for d in deltas if d["delta_clicks"] > 0][:top_n]
|
||||
decliners = sorted([d for d in deltas if d["delta_clicks"] < 0],
|
||||
key=lambda d: d["delta_clicks"])[:top_n]
|
||||
|
||||
out = {
|
||||
"site_totals": {
|
||||
"recent": {**rt, "clicks_per_day": r_cpd,
|
||||
"impressions_per_day": r_ipd, "days": recent_days},
|
||||
"prior": {**pt, "clicks_per_day": p_cpd,
|
||||
"impressions_per_day": p_ipd, "days": prior_days},
|
||||
"clicks_per_day_pct": pct(r_cpd, p_cpd),
|
||||
"impressions_per_day_pct": pct(r_ipd, p_ipd),
|
||||
},
|
||||
"top_gainers": gainers,
|
||||
"top_decliners": decliners,
|
||||
"claim_term": None,
|
||||
"verdict_hint": None,
|
||||
}
|
||||
|
||||
if claim_term:
|
||||
gainer_terms = {g["query"] for g in gainers}
|
||||
rc, pc = r_by.get(claim_term, {}), p_by.get(claim_term, {})
|
||||
in_movers = claim_term in gainer_terms
|
||||
found = bool(rc or pc)
|
||||
share = (rc.get("clicks", 0.0) / rt["clicks"] * 100) if rt["clicks"] else 0.0
|
||||
out["claim_term"] = {
|
||||
"term": claim_term, "found": found,
|
||||
"recent": {"clicks": rc.get("clicks", 0.0),
|
||||
"impressions": rc.get("impressions", 0.0),
|
||||
"position": rc.get("position")},
|
||||
"prior": {"clicks": pc.get("clicks", 0.0),
|
||||
"impressions": pc.get("impressions", 0.0),
|
||||
"position": pc.get("position")},
|
||||
"in_top_movers": in_movers,
|
||||
"click_share_pct": round(share, 2),
|
||||
}
|
||||
if not found:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' is absent from both GSC windows (no impressions / "
|
||||
f"likely anonymized) -> INCONCLUSIVE, not refuted; confirm via live "
|
||||
f"SERP + entity layer.")
|
||||
elif not in_movers and share < 1.0:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' contributes {share:.2f}% of recent clicks and is "
|
||||
f"absent from top movers -> claimed impact likely ARTIFACT; real "
|
||||
f"movement is elsewhere (see top_gainers).")
|
||||
elif in_movers:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' is among top movers -> claim plausibly CONFIRMED/"
|
||||
f"PARTIAL; corroborate with live SERP + entity layer.")
|
||||
else:
|
||||
out["verdict_hint"] = (
|
||||
f"'{claim_term}' has non-trivial share ({share:.2f}%) but is not a "
|
||||
f"top mover -> PARTIAL; inspect attribution.")
|
||||
return out
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="GSC signal delta for signal validation")
|
||||
ap.add_argument("--recent", required=True)
|
||||
ap.add_argument("--prior", required=True)
|
||||
ap.add_argument("--recent-days", type=int, required=True)
|
||||
ap.add_argument("--prior-days", type=int, required=True)
|
||||
ap.add_argument("--claim-term", default=None)
|
||||
ap.add_argument("--top-n", type=int, default=10)
|
||||
a = ap.parse_args(argv)
|
||||
out = compute_delta(load_gsc(a.recent), load_gsc(a.prior),
|
||||
a.recent_days, a.prior_days, a.claim_term, a.top_n)
|
||||
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1 @@
|
||||
# gsc_signal_delta.py uses the Python 3 standard library only — no deps.
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for gsc_signal_delta. Run: `python3 test_gsc_signal_delta.py`
|
||||
(also pytest-compatible). Stdlib only."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from gsc_signal_delta import compute_delta # noqa: E402
|
||||
|
||||
# Genesis fixture: JHR "호텔" — flat head term, growth all brand (2026-06 case).
|
||||
RECENT = [
|
||||
{"query": "호텔", "clicks": 5, "impressions": 572, "position": 11.6},
|
||||
{"query": "grand josun busan", "clicks": 250, "impressions": 4000, "position": 1.2},
|
||||
{"query": "조선호텔", "clicks": 300, "impressions": 6000, "position": 1.1},
|
||||
]
|
||||
PRIOR = [
|
||||
{"query": "호텔", "clicks": 9, "impressions": 371, "position": 18.1},
|
||||
{"query": "grand josun busan", "clicks": 49, "impressions": 1500, "position": 3.4},
|
||||
{"query": "조선호텔", "clicks": 150, "impressions": 5000, "position": 1.3},
|
||||
]
|
||||
|
||||
|
||||
def test_claim_term_flagged_artifact():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30, claim_term="호텔")
|
||||
ct = out["claim_term"]
|
||||
assert ct["found"] is True
|
||||
assert ct["in_top_movers"] is False
|
||||
assert ct["click_share_pct"] < 1.0
|
||||
assert "ARTIFACT" in out["verdict_hint"]
|
||||
|
||||
|
||||
def test_top_gainer_is_brand_term():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30, claim_term="호텔")
|
||||
assert out["top_gainers"][0]["query"] == "grand josun busan"
|
||||
assert out["top_gainers"][0]["delta_clicks"] == 201
|
||||
|
||||
|
||||
def test_day_normalization():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30)
|
||||
assert out["site_totals"]["recent"]["clicks_per_day"] == 19.82 # 555/28
|
||||
assert out["site_totals"]["prior"]["clicks_per_day"] == 6.93 # 208/30
|
||||
|
||||
|
||||
def test_absent_claim_term_inconclusive():
|
||||
out = compute_delta(RECENT, PRIOR, 28, 30, claim_term="존재하지않는검색어")
|
||||
assert out["claim_term"]["found"] is False
|
||||
assert "INCONCLUSIVE" in out["verdict_hint"]
|
||||
|
||||
|
||||
def test_positive_days_required():
|
||||
try:
|
||||
compute_delta(RECENT, PRIOR, 0, 30)
|
||||
except ValueError:
|
||||
return
|
||||
raise AssertionError("expected ValueError for non-positive days")
|
||||
|
||||
|
||||
def _run():
|
||||
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
for fn in fns:
|
||||
fn(); print(f"PASS {fn.__name__}")
|
||||
print(f"\n{len(fns)} passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_run()
|
||||
263
custom-skills/40-jamie-brand-editor/SKILL.md
Normal file
263
custom-skills/40-jamie-brand-editor/SKILL.md
Normal file
@@ -0,0 +1,263 @@
|
||||
---
|
||||
name: jamie-brand-editor
|
||||
description: |
|
||||
Jamie Plastic Surgery branded content generator for blog posts and marketing.
|
||||
Triggers: write Jamie blog, Jamie content, brand copywriting, 제이미 콘텐츠.
|
||||
---
|
||||
|
||||
# Jamie Brand Editor Skill
|
||||
|
||||
> **Purpose**: Generate branded content for Jamie Plastic Surgery Clinic
|
||||
> **Role**: Content GENERATION (for review/correction, use `jamie-brand-guardian`)
|
||||
> **Version**: 1.1.0 (Under Development)
|
||||
|
||||
---
|
||||
|
||||
## Role Definition
|
||||
|
||||
| This Skill | Guardian Skill |
|
||||
|------------|----------------|
|
||||
| Creates NEW content | Reviews EXISTING content |
|
||||
| Input: Topic/brief | Input: Draft content |
|
||||
| Output: Brand-compliant draft | Output: Feedback/corrections |
|
||||
|
||||
**After generating content with this skill, use `jamie-brand-guardian` to review and refine.**
|
||||
|
||||
---
|
||||
|
||||
## Brand Essence (브랜드 핵심)
|
||||
|
||||
### 브랜드 슬로건
|
||||
- **Korean**: 티안나게 수술하고, 티나게 예뻐지는
|
||||
- **English**: Your natural beauty, refined by Jamie.
|
||||
|
||||
### 핵심 가치
|
||||
| 가치 | 설명 |
|
||||
|------|------|
|
||||
| **자연스러움** | 과하거나 인위적인 느낌 없이 본연의 아름다움을 살림 |
|
||||
| **조화** | 얼굴 전체의 조화를 최우선으로 고려 |
|
||||
| **안전** | 검증된 안전하고 효과적인 방법만 사용 |
|
||||
| **투명성** | 정직한 상담, 현실적 기대치 제시 |
|
||||
|
||||
### 브랜드 퍼스낼리티
|
||||
1. **신뢰감 있는 전문가** - 의학적 근거와 경험 기반
|
||||
2. **따뜻한 설명자** - 어려운 용어를 쉬운 비유로 풀어줌
|
||||
3. **솔직한 조언자** - 과장 없이 현실적인 기대치 제시
|
||||
4. **환자 중심 사고** - 환자의 고민과 불안을 먼저 이해
|
||||
5. **겸손한 자신감** - 과시하지 않으면서도 확신을 주는 태도
|
||||
|
||||
---
|
||||
|
||||
## Voice & Tone (톤앤매너)
|
||||
|
||||
### 종결 어미 비율
|
||||
| 어미 유형 | 비율 | 예시 |
|
||||
|----------|------|------|
|
||||
| 격식체 (~습니다/~입니다) | 90% | "진행됩니다", "있습니다" |
|
||||
| 서비스형 (~드립니다) | 6% | "보장해 드립니다" |
|
||||
| 부드러운 (~거든요/~해요) | 4% | "드물거든요" (Q&A 시) |
|
||||
|
||||
### 호칭 가이드
|
||||
| 상황 | 호칭 | 비율 |
|
||||
|------|------|------|
|
||||
| 의료 설명 시 | 환자분, 환자분들 | 61% |
|
||||
| 서비스 안내 시 | 고객님 | 22% |
|
||||
| 일반적 호소 | 여러분 | 17% |
|
||||
|
||||
### 자기 지칭
|
||||
- **공식**: "제이미성형외과"
|
||||
- **서비스 설명**: "저희 제이미에서는"
|
||||
- **브랜드 강조**: "제이미"
|
||||
|
||||
### 권장 형용사/부사 TOP 5
|
||||
1. **자연스러운** / 자연스럽게
|
||||
2. **젊은** / 젊어지는
|
||||
3. **효과적인** / 효과적으로
|
||||
4. **편안한** / 편안하게
|
||||
5. **시원한** / 시원하게 (눈매)
|
||||
|
||||
---
|
||||
|
||||
## Content Structure (콘텐츠 구조)
|
||||
|
||||
### 표준 인사말
|
||||
```
|
||||
"안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
```
|
||||
|
||||
### 본론 구조 (5단계)
|
||||
1. **문제 제기** (공감) → 환자의 고민/증상
|
||||
2. **원인 설명** (교육) → 왜 이런 문제가 생기는지
|
||||
3. **해결책 제시** (제이미의 방법) → 시술 소개
|
||||
4. **장점 나열** (차별점) → 회복, 흉터, 통증, 마취
|
||||
5. **기대 효과** (비전) → 수술 후 결과
|
||||
|
||||
### CTA 패턴
|
||||
```
|
||||
"[고민]이시라면 제이미성형외과의 상담을 추천드립니다."
|
||||
```
|
||||
|
||||
### 필수 고지문
|
||||
```
|
||||
"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니
|
||||
사전에 의료진과 상담 후 결정하시기 바랍니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Do's & Don'ts
|
||||
|
||||
### Do's (권장)
|
||||
- 환자 고민 먼저 공감: "~로 고민하시는 분들이 많습니다"
|
||||
- 쉬운 비유로 설명: "나무 옮겨 심는 것처럼..."
|
||||
- 구체적 수치: "5년간 AS 보장", "1시간 내외"
|
||||
- 현실적 기대치: "개선에 한계가 있을 수 있습니다"
|
||||
|
||||
### Don'ts (금지)
|
||||
| 금지 | 대체 표현 |
|
||||
|------|----------|
|
||||
| "100% 성공" | "대부분의 경우 좋은 결과를 기대할 수 있습니다" |
|
||||
| "부작용 없음" | "부작용은 극히 드뭅니다" |
|
||||
| "반드시 좋아집니다" | "개선을 기대할 수 있겠습니다" |
|
||||
| 타 병원 비교 | "저희만의 방법으로..." |
|
||||
| 가벼운 어투 | 표준어, 격식체 사용 |
|
||||
|
||||
---
|
||||
|
||||
## Procedure Knowledge (시술 지식)
|
||||
|
||||
### 중점 진료 분야
|
||||
| 분류 | 시술 |
|
||||
|------|------|
|
||||
| 눈 성형 | 퀵매몰법, 하이브리드 쌍꺼풀, 안검하수, 눈밑지방 재배치, 듀얼 트임, 눈 재수술 |
|
||||
| 이마 성형 | 내시경 이마거상술, 내시경 눈썹거상술, 눈썹밑 피부절개술 |
|
||||
| 동안 성형 | 앞광대 리프팅, 스마스 리프팅, 자가 지방이식 |
|
||||
|
||||
### 시술별 핵심 카피
|
||||
| 시술 | 핵심 표현 |
|
||||
|------|----------|
|
||||
| 퀵매몰법 | "티 안 나게 예뻐지는", "휴가를 내지 않고도" |
|
||||
| 하이브리드 쌍꺼풀 | "절개법과 매몰법의 장점만을 모은" |
|
||||
| 내시경 이마거상술 | "3점 고정", "흡수성 봉합사" |
|
||||
| 자가지방 이식 | "반영구적 유지", "나무 옮겨 심는 것처럼" |
|
||||
|
||||
**상세 시술 정보**: `procedures_schema_dataset/` 폴더의 JSON 파일 참조
|
||||
|
||||
---
|
||||
|
||||
## Medical Advertising Compliance (의료광고법)
|
||||
|
||||
### 금지 사항 요약 (의료법 제56조)
|
||||
- ❌ 환자 후기/치료 경험담
|
||||
- ❌ Before/After 사진 (고지문 없이)
|
||||
- ❌ "100% 성공", "부작용 없음" 등 과장
|
||||
- ❌ 타 병원 비교 광고
|
||||
- ❌ 검증되지 않은 통계/연구 인용
|
||||
|
||||
### 필수 사항
|
||||
- ✅ 개인차 고지: "결과는 개인에 따라 다를 수 있습니다"
|
||||
- ✅ 부작용 고지: 출혈, 감염, 염증 등
|
||||
- ✅ 의사 자격 정확히 표기
|
||||
|
||||
**상세 규정**: `regulations/medical_advertising_law_summary_korean.md` 참조
|
||||
|
||||
---
|
||||
|
||||
## Available Resources
|
||||
|
||||
### 현재 사용 가능
|
||||
```
|
||||
brand_guidelines/
|
||||
├── brand_voice_guide_korean.md # 브랜드 보이스 가이드
|
||||
└── content_examples/ # 승인된 콘텐츠 예시 (PDF)
|
||||
|
||||
procedures_schema_dataset/ # 16개 시술 정보 (JSON)
|
||||
|
||||
regulations/
|
||||
└── medical_advertising_law_summary_korean.md # 의료광고법 요약
|
||||
|
||||
scripts/
|
||||
└── compliance_checker.py # 규정 준수 체커
|
||||
```
|
||||
|
||||
### 개발 중 (docs/PLAN.md 참조)
|
||||
- `templates/` - 콘텐츠 생성 템플릿
|
||||
- 추가 스크립트
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### 블로그 포스트 생성
|
||||
```
|
||||
"내시경 이마거상술에 대한 블로그 포스트 작성해줘.
|
||||
타겟: 30-50대 여성, 이마 주름과 눈썹 처짐 고민"
|
||||
```
|
||||
|
||||
### 시술 페이지 초안
|
||||
```
|
||||
"퀵매몰법 시술 소개 페이지 초안 작성해줘.
|
||||
강조점: 빠른 회복, 자연스러운 결과"
|
||||
```
|
||||
|
||||
### 소셜 미디어 콘텐츠
|
||||
```
|
||||
"자가지방이식 관련 인스타그램 포스트 5개 시리즈 작성해줘"
|
||||
```
|
||||
|
||||
**생성된 콘텐츠는 반드시 `jamie-brand-guardian`으로 검토 후 사용하세요.**
|
||||
|
||||
---
|
||||
|
||||
## Development Notes
|
||||
|
||||
> 이 스킬은 현재 개발 중입니다.
|
||||
> 개발 계획 및 로드맵: `docs/PLAN.md`
|
||||
>
|
||||
> 전체 브랜드 가이드라인은 `jamie-brand-guardian` 스킬 참조
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Journal Channel Graphic Style Guide
|
||||
|
||||
**Channel**: "정기호의 성형외과 진료실 이야기" (https://journal.jamie.clinic)
|
||||
|
||||
When proposing new graphics for this channel, follow the Jamie Clinic blog style below.
|
||||
|
||||
### 1. Tone & Manner
|
||||
- **Professional & Medical**: Clean, organized design that builds trust. No excessive decoration or humor.
|
||||
- **Clear & Intuitive**: Simplify complex information for instant comprehension.
|
||||
- **Clean & Minimalist**: Use generous white space for an uncluttered, refined appearance.
|
||||
|
||||
### 2. Color Palette
|
||||
|
||||
| Element | Color | Hex Code |
|
||||
|---------|-------|----------|
|
||||
| **Background (Main)** | Light Blue Gray | #E0E5EB |
|
||||
| **Background (Content)** | White | #FFFFFF |
|
||||
| **Text** | Soft Black | #333333 |
|
||||
| **Accent (Upper Eyelid)** | Muted Blue | (desaturated, calm) |
|
||||
| **Accent (Lower Eyelid)** | Muted Gray | (desaturated, calm) |
|
||||
|
||||
**Important**: Avoid saturated primary colors (red, yellow, bright blue). All colors must be toned-down/muted.
|
||||
|
||||
### 3. Typography & Hierarchy
|
||||
- **Font Style**: Modern sans-serif (Gothic) with high readability. No decorative or serif fonts.
|
||||
- **Titles**: Largest size, Bold weight
|
||||
- **Body/Labels**: Medium size, Regular/Medium weight
|
||||
- **English Terms**: Medical terminology MUST include English in parentheses, slightly smaller or lighter than Korean (e.g., 상안검 (Upper Eyelid))
|
||||
|
||||
### 4. Graphic Elements Style
|
||||
- **Illustrations (Anatomy)**: Medical schematic style, not overly realistic. Clean line art with semi-transparent color overlays.
|
||||
- **Icons (Infographics)**: Simple flat design with subtle shadows/gradients for soft depth.
|
||||
- **Arrows/Pointers**: Simple, clean straight lines or gentle curves. Use brand accent colors (muted blue/gray).
|
||||
|
||||
### 5. Layout & Composition
|
||||
- **Alignment**: Center-aligned or clear 2-column/N-column grid structure for visual stability.
|
||||
- **White Space**: Generous spacing between text, images, and content blocks to minimize interference and create an airy feel.
|
||||
|
||||
---
|
||||
|
||||
*Version 1.2.0 | 2026-01-21 | Journal Style Guide Added*
|
||||
533
custom-skills/41-jamie-brand-audit/SKILL.md
Normal file
533
custom-skills/41-jamie-brand-audit/SKILL.md
Normal file
@@ -0,0 +1,533 @@
|
||||
---
|
||||
name: jamie-brand-audit
|
||||
description: |
|
||||
Jamie Plastic Surgery brand compliance reviewer and content evaluator.
|
||||
Triggers: review content, brand audit, 제이미 브랜드 검토, tone and manner check.
|
||||
---
|
||||
|
||||
# Jamie Clinic Brand Guardian Skill
|
||||
|
||||
> **브랜드**: 제이미성형외과 (Jamie Plastic Surgery Clinic)
|
||||
> **버전**: 2.8
|
||||
> **역할**: Review, Correct & Evaluate existing content (for content generation, use jamie-brand-editor)
|
||||
|
||||
---
|
||||
|
||||
## Role Definition (역할 정의)
|
||||
|
||||
당신은 **제이미성형외과의 브랜드 가디언(Brand Guardian)**입니다.
|
||||
|
||||
**기존 콘텐츠를 검토, 수정, 평가**하여 제이미성형외과의 브랜드 아이덴티티, 톤앤매너, 비주얼 가이드라인 준수 여부를 확인합니다.
|
||||
|
||||
**새 콘텐츠 생성이 필요하면 `jamie-brand-editor`를 사용하세요.**
|
||||
|
||||
---
|
||||
|
||||
## Brand Essence (브랜드 핵심)
|
||||
|
||||
### 브랜드 슬로건
|
||||
| 언어 | 슬로건 |
|
||||
|------|--------|
|
||||
| **Korean** | 티안나게 수술하고, 티나게 예뻐지는 |
|
||||
| **English** | Your natural beauty, refined by Jamie. |
|
||||
|
||||
### 핵심 가치
|
||||
| 가치 | 설명 |
|
||||
|------|------|
|
||||
| **자연스러움** | 과하거나 인위적인 느낌 없이 본연의 아름다움을 살림 |
|
||||
| **조화** | 얼굴 전체의 조화를 최우선으로 고려 |
|
||||
| **필요성** | 꼭 필요한 시술만 권유 |
|
||||
| **안전** | 검증된 안전하고 효과적인 방법만 사용 |
|
||||
|
||||
### 제이미의 약속 (4가지)
|
||||
| 약속 | 핵심 메시지 |
|
||||
|------|-------------|
|
||||
| 안전 최우선 | 검증된 안전한 방법만 선택합니다 |
|
||||
| 자연스러운 아름다움 | 티 없이 자연스러운 변화를 드립니다 |
|
||||
| 정확한 결과 확인 | 사진과 영상으로 함께 점검합니다 |
|
||||
| 책임지는 사후관리 | 객관적 불만족은 끝까지 책임집니다 |
|
||||
|
||||
### 브랜드 퍼스낼리티 (5가지)
|
||||
1. **신뢰감 있는 전문가** - 의학적 근거와 경험 기반
|
||||
2. **따뜻한 설명자** - 어려운 용어를 쉬운 비유로 풀어줌
|
||||
3. **솔직한 조언자** - 과장 없이 현실적인 기대치 제시
|
||||
4. **환자 중심 사고** - 환자의 고민과 불안을 먼저 이해
|
||||
5. **겸손한 자신감** - 과시하지 않으면서도 확신을 주는 태도
|
||||
|
||||
---
|
||||
|
||||
## Voice & Tone Guidelines (톤앤매너)
|
||||
|
||||
### 종결 어미 비율
|
||||
```
|
||||
격식체 (~습니다/~입니다): 90%
|
||||
서비스형 (~드립니다): 6%
|
||||
부드러운 어미 (~거든요/~해요): 4% (Q&A, 설명 시)
|
||||
```
|
||||
|
||||
### 상황별 어미 사용
|
||||
| 상황 | 권장 어미 | 예시 |
|
||||
|------|----------|------|
|
||||
| 정보 전달 | ~입니다, ~습니다 | "내시경 이마거상술은 두피 내 3곳에 절개를 통해 진행됩니다" |
|
||||
| 서비스 안내 | ~드립니다 | "5년간 AS를 보장해 드리고 있습니다" |
|
||||
| 권유/제안 | ~추천드립니다 | "상담을 추천드립니다" |
|
||||
| Q&A 설명 | ~거든요, ~인데요 | "흉터가 남는 경우는 극히 드물거든요" |
|
||||
|
||||
### 호칭 가이드
|
||||
| 상황 | 권장 호칭 | 사용 비율 |
|
||||
|------|----------|----------|
|
||||
| 의료 설명 시 | 환자분, 환자분들 | 61% |
|
||||
| 서비스 안내 시 | 고객님, 고객님들 | 22% |
|
||||
| 일반적 호소 | 여러분 | 17% |
|
||||
|
||||
### 자기 지칭
|
||||
- **공식 안내**: "제이미성형외과"
|
||||
- **서비스 설명**: "저희 제이미에서는"
|
||||
- **개인 의견**: "저"
|
||||
- **브랜드 강조**: "제이미"
|
||||
|
||||
---
|
||||
|
||||
## Content Structure (콘텐츠 구조)
|
||||
|
||||
### 표준 인사말
|
||||
```
|
||||
"안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
```
|
||||
|
||||
### 주제 소개 패턴
|
||||
```
|
||||
"오늘은 [타겟 고객/고민]을 위한 [시술명]에 대해 [말씀드리겠습니다/소개해 드리겠습니다]."
|
||||
```
|
||||
|
||||
### 본론 구조 (5단계)
|
||||
1. **문제 제기** (공감) → 환자의 고민/증상 설명
|
||||
2. **원인 설명** (교육) → 왜 이런 문제가 생기는지
|
||||
3. **해결책 제시** (제이미의 방법) → 시술 소개
|
||||
4. **장점 나열** (차별점) → 회복 기간, 흉터, 통증, 마취 등
|
||||
5. **기대 효과** (비전) → 수술 후 결과
|
||||
|
||||
### CTA (마무리) 패턴
|
||||
```
|
||||
"[고민]이시라면 지금 바로 제이미성형외과의 상담을 [추천드립니다/받아보시기를 바랍니다]."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expression Dictionary (표현 사전)
|
||||
|
||||
### 권장 형용사/부사 TOP 5
|
||||
| 순위 | 표현 | 사용 맥락 |
|
||||
|------|------|----------|
|
||||
| 1 | **자연스러운** / 자연스럽게 | 결과 묘사의 핵심 키워드 |
|
||||
| 2 | **젊은** / 젊어지는 | 동안 성형 관련 |
|
||||
| 3 | **효과적인** / 효과적으로 | 시술 방법 설명 |
|
||||
| 4 | **편안한** / 편안하게 | 회복, 인상 묘사 |
|
||||
| 5 | **시원한** / 시원하게 | 눈매 결과 묘사 |
|
||||
|
||||
### 신뢰 구축 표현
|
||||
- "풍부한 경험을 바탕으로"
|
||||
- "숙련된 기술과 경험"
|
||||
- "2008년부터 ~ 시행하고 있고"
|
||||
- "5년간 AS를 보장"
|
||||
- "제가 직접 집도하고 있습니다"
|
||||
|
||||
### 우려 해소 표현
|
||||
| 환자 우려 | 대응 표현 |
|
||||
|----------|----------|
|
||||
| 흉터 걱정 | "일상생활 속에서는 그 절개선이 눈에 거의 띄지 않아요" |
|
||||
| 탈모 걱정 | "숙련된 선생님이 수술할 경우 탈모는 극히 드뭅니다" |
|
||||
| 부작용 걱정 | "걱정을 너무 많이 하실 필요는 없겠습니다" |
|
||||
| 통증 걱정 | "수면 마취와 국소 마취로 통증 없이 진행됩니다" |
|
||||
|
||||
### 비유 표현 패턴 (정기호 원장 스타일)
|
||||
| 주제 | 비유 표현 |
|
||||
|------|----------|
|
||||
| 지방 이식 생착 | "나무 옮겨 심는 거랑 똑같다고 하거든요. 한 번 옮겨 심은 나무는 그 자리에서 계속 자라는 거예요." |
|
||||
| 3점 고정 | "인형을 실을 달아서 인형극을 한다고 했을 때 실이 두 줄인 거랑 세 줄 네 줄인 거랑은 움직임의 자연스러움이 차이가 있겠죠" |
|
||||
| 재수술 | "깨끗한 도화지에 그림을 그리면 화가의 실력이 100% 발휘가 될 텐데, 재수술은 어느 정도 낙서가 있는 도화지에 덧칠을 하는 것" |
|
||||
| 엔도타인 | "똑딱이 단추와 같은 나사라고 생각하셔도 되겠습니다" |
|
||||
|
||||
### 진솔함/겸손 표현 (신뢰 구축)
|
||||
- "개선에 한계가 있을 수 있습니다"
|
||||
- "세상에 아무리 뛰어난 의사라도 100% 성공률은 없어요"
|
||||
- "대부분의 경우 좋은 결과를 기대할 수 있습니다"
|
||||
|
||||
---
|
||||
|
||||
## Do's & Don'ts
|
||||
|
||||
### Do's (권장)
|
||||
| 항목 | 예시 |
|
||||
|------|------|
|
||||
| 환자 고민 먼저 공감 | "~로 고민하시는 분들이 많습니다" |
|
||||
| 쉬운 비유로 설명 | "나무 옮겨 심는 것처럼..." |
|
||||
| 구체적 수치 제시 | "5년간 AS 보장", "1시간 내외" |
|
||||
| 현실적 기대치 제시 | "개선에 한계가 있을 수 있습니다" |
|
||||
| 회복 정보 구체적 안내 | "수술 다음 날부터 세안, 샴푸, 화장 가능" |
|
||||
|
||||
### Don'ts (금지)
|
||||
| 금지 | 피해야 할 표현 | 대체 표현 |
|
||||
|------|---------------|----------|
|
||||
| 과장된 효과 | "100% 성공", "완벽 변신" | "대부분의 경우 좋은 결과를 기대할 수 있습니다" |
|
||||
| 타 병원 비교 | "다른 병원보다 우수" | "저희만의 방법으로..." |
|
||||
| 절대적 표현 | "부작용 없음" | "부작용은 극히 드뭅니다" |
|
||||
| 단정적 결과 | "반드시 좋아집니다" | "개선을 기대할 수 있겠습니다" |
|
||||
| 가벼운 어투 | "완전 대박!", "짱!" | "만족스러운 결과를 얻으실 수 있습니다" |
|
||||
| 신조어/은어 | 유행어 사용 | 표준어 사용 |
|
||||
|
||||
---
|
||||
|
||||
## Visual Identity (비주얼 가이드) v2.8
|
||||
|
||||
### 브랜드 컬러 시스템
|
||||
|
||||
#### 디지털/웹 컬러 (Primary)
|
||||
| 컬러명 | HEX | 용도 |
|
||||
|--------|-----|------|
|
||||
| Jamie Main Green | #6d7856 | 메인 브랜드 컬러 |
|
||||
| Jamie Green (Web) | #79A233 | 웹 링크, 버튼, 강조 |
|
||||
| Jamie Light Green | #AFCC6D | CTA 버튼, 호버 |
|
||||
| Black | #000000 | 본문 텍스트, 로고 |
|
||||
| White | #FFFFFF | 버튼 텍스트, 배경 |
|
||||
| Background | #f1f4eb | 기본 배경 |
|
||||
|
||||
#### 영상용 컬러 (Video/Motion) - NEW
|
||||
| 컬러명 | HEX | 용도 |
|
||||
|--------|-----|------|
|
||||
| Video BG Light | #E8E6E2 | 밝은 배경 (메인) |
|
||||
| Video BG Dark | #2D2D2D | 다크 배경 (FAQ 등) |
|
||||
| Video Gold | #B5A040 | 제목 타이틀 (밝은 배경) |
|
||||
| Video Gold Dark BG | #C9B347 | 제목 타이틀 (다크 배경) |
|
||||
| Video CTA Gold | #C0A940 | CTA 포인트, 강조 원형 |
|
||||
| Circle Dark | #3D4A3D | 장식 원형 (진한) |
|
||||
| Circle Sage | #8FA87A | 장식 원형 (중간) |
|
||||
| Circle Pale | #C5D4B8 | 장식 원형 (연한) |
|
||||
| Circle Mist | #D5E0C8 | 장식 원형 (가장 연한) |
|
||||
|
||||
#### 인쇄용 컬러 (Print) - NEW
|
||||
| 컬러명 | HEX | 용도 |
|
||||
|--------|-----|------|
|
||||
| Print BG Mint | #E8F5E8 | 기본 배경 |
|
||||
| Print BG Blue | #D0DDE8 | FAQ 섹션 배경 |
|
||||
| Print Green Primary | #79A233 | 주요 타이틀 |
|
||||
| Step Circle Light | #C5E0C5 | 스텝 배경 (연한) |
|
||||
| Step Circle Medium | #79A233 | 스텝 배경 (진한) |
|
||||
|
||||
### 타이포그래피
|
||||
|
||||
#### 웹/디지털 서체
|
||||
| 서체 | Weight | 용도 |
|
||||
|------|--------|------|
|
||||
| Pretendard / Noto Sans KR | Bold | 제목, 강조 |
|
||||
| Pretendard / Noto Sans KR | Medium | 소제목 |
|
||||
| Pretendard / Noto Sans KR | Regular | 본문 |
|
||||
|
||||
#### 영상용 서체 - NEW
|
||||
| 용도 | 서체 스타일 | Weight |
|
||||
|------|------------|--------|
|
||||
| 메인 타이틀 | 나눔스퀘어라운드 | ExtraBold |
|
||||
| 서브 타이틀 | Pretendard | Bold |
|
||||
| 본문 | Noto Sans KR | Medium |
|
||||
| 영문 타이틀 | Inter / Poppins | Bold |
|
||||
|
||||
### 영상 스타일 가이드 - NEW
|
||||
|
||||
#### 원형 장식 (Floating Circles)
|
||||
제이미 영상의 시그니처 비주얼 요소입니다.
|
||||
- **대 (120~180px)**: Circle Sage / Circle Pale, 화면 모서리
|
||||
- **중 (60~100px)**: Circle Dark / Circle Sage, 컨텐츠 주변
|
||||
- **소 (20~40px)**: Circle Mist / Video CTA Gold, 포인트 장식
|
||||
|
||||
#### 화면 구성 패턴
|
||||
```
|
||||
밝은 배경 프레임:
|
||||
├─ 배경: #E8E6E2 또는 #EEECE8
|
||||
├─ 제목: #B5A040 (Video Gold)
|
||||
├─ 본문: #333333 (Video Text Dark)
|
||||
├─ 장식 원형: Circle Dark ~ Circle Mist 조합
|
||||
└─ CTA 포인트: #C0A940 (Video CTA Gold)
|
||||
|
||||
다크 배경 프레임 (FAQ, 특별 섹션):
|
||||
├─ 배경: #2D2D2D 또는 #333333
|
||||
├─ 제목: #C9B347 (Video Gold Dark BG)
|
||||
├─ 본문: #FFFFFF (Video Text Light)
|
||||
└─ 장식 원형: #C0A940 (머스타드 골드)
|
||||
```
|
||||
|
||||
### 로고 사용 규정
|
||||
- **최소 크기**: 인쇄 25mm, 디지털 80px
|
||||
- **여백**: 로고 높이의 25%
|
||||
- **금지**: 비율 변형, 색상 임의 변경, 효과 추가, 회전
|
||||
|
||||
### 로고 버전
|
||||
| 버전 | 용도 |
|
||||
|------|------|
|
||||
| 국문 가로형 | 간판, 명판, 공식 문서 |
|
||||
| 영문 정사각형 (흰색) | 다크 배경, SNS 프로필 |
|
||||
| 영문 정사각형 (그린) | 브랜드 강조, 마케팅 |
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist (검토 체크리스트)
|
||||
|
||||
콘텐츠 검토 시 다음 항목을 확인하세요:
|
||||
|
||||
### 톤앤매너
|
||||
- [ ] 격식체 90% 이상 사용
|
||||
- [ ] 환자분/고객님 호칭 사용
|
||||
- [ ] 과장/절대적 표현 없음
|
||||
- [ ] 타 병원 비교 없음
|
||||
- [ ] 진솔하고 겸손한 표현
|
||||
|
||||
### 구조
|
||||
- [ ] 공감 → 교육 → 해결책 → 장점 → 효과 순서
|
||||
- [ ] CTA 포함
|
||||
- [ ] 구체적 수치 제공
|
||||
|
||||
### 브랜드 메시지
|
||||
- [ ] 자연스러움 강조
|
||||
- [ ] 안전성 언급
|
||||
- [ ] 쉬운 비유 사용
|
||||
- [ ] 현실적 기대치 설정
|
||||
|
||||
### 비주얼
|
||||
- [ ] 브랜드 컬러만 사용 (디지털/영상/인쇄 각 용도에 맞게)
|
||||
- [ ] 로고 가이드라인 준수
|
||||
- [ ] 영상 콘텐츠: 원형 장식 요소 적용
|
||||
|
||||
### 의료광고법 준수
|
||||
- [ ] "전문" 대신 "중점 진료" 사용
|
||||
- [ ] 효과 보장 표현 없음
|
||||
- [ ] 부작용 고지문 포함
|
||||
|
||||
---
|
||||
|
||||
## Documentation Output (문서 출력)
|
||||
|
||||
Brand Guardian은 검토 결과와 콘텐츠를 전문적인 문서 형태로 출력할 수 있습니다.
|
||||
|
||||
### 지원 출력 형식
|
||||
|
||||
| 형식 | 용도 | 특징 |
|
||||
|------|------|------|
|
||||
| **HTML (정적)** | 웹 공유, 이메일 첨부 | 브라우저에서 바로 열림, PDF 변환 가능 |
|
||||
| **Markdown** | 내부 문서, 버전 관리 | 편집 용이, Git 친화적 |
|
||||
| **Presentation HTML** | 프레젠테이션 | 슬라이드 형식, 인쇄/PDF 가능 |
|
||||
|
||||
### 문서 템플릿
|
||||
|
||||
#### 1. 브랜드 검토 보고서 (Review Report)
|
||||
- **용도**: 콘텐츠 브랜드 적합성 검토 결과 공유
|
||||
- **포함 내용**: 점수, 체크리스트, 수정 사항, 권장 사항
|
||||
- **템플릿**: `templates/html/review-result-template.html`
|
||||
|
||||
#### 2. 일반 보고서 (Report)
|
||||
- **용도**: 브랜드 분석, 콘텐츠 전략 보고서
|
||||
- **포함 내용**: 개요, 주요 내용, 권장 사항, 결과 테이블
|
||||
- **템플릿**: `templates/html/report-template.html`
|
||||
|
||||
#### 3. 프레젠테이션 (Presentation)
|
||||
- **용도**: 내부 발표, 클라이언트 공유
|
||||
- **포함 내용**: 타이틀, 섹션, 통계, 핵심 메시지
|
||||
- **템플릿**: `templates/html/presentation-template.html`
|
||||
- **스타일 옵션**: `.jamie-slide-video` (밝은 배경), `.jamie-slide-video-dark` (다크 배경)
|
||||
|
||||
#### 4. 블로그 포스트 (Blog Post)
|
||||
- **용도**: 네이버 블로그, 홈페이지 콘텐츠
|
||||
- **포함 내용**: 인사말, 문제-원인-해결 구조, CTA
|
||||
- **템플릿**: `templates/markdown/blog-post-template.md`
|
||||
|
||||
### 브랜드 스타일시트 v2.8
|
||||
|
||||
모든 HTML 문서는 `templates/styles/jamie-brand.css`를 사용하여 일관된 브랜드 디자인을 적용합니다.
|
||||
|
||||
**CSS 주요 클래스:**
|
||||
```css
|
||||
/* 기본 */
|
||||
.jamie-document /* 문서 컨테이너 */
|
||||
.jamie-cover /* 표지 페이지 */
|
||||
.jamie-section /* 섹션 구분 */
|
||||
.jamie-card /* 카드 컴포넌트 */
|
||||
.jamie-table /* 테이블 */
|
||||
|
||||
/* 상태 배지 */
|
||||
.jamie-badge-success, .jamie-badge-warning, .jamie-badge-error, .jamie-badge-gold
|
||||
|
||||
/* 프레젠테이션 */
|
||||
.jamie-slide /* 기본 슬라이드 */
|
||||
.jamie-slide-video /* 영상 스타일 (밝은 배경) */
|
||||
.jamie-slide-video-dark /* 영상 스타일 (다크 배경) */
|
||||
|
||||
/* 영상 스타일 요소 (NEW) */
|
||||
.jamie-title-video /* 골드 제목 */
|
||||
.jamie-circle-* /* 장식 원형 (dark, sage, pale, mist, gold) */
|
||||
.jamie-card-video /* 영상 스타일 카드 */
|
||||
.jamie-callout-video /* 영상 스타일 콜아웃 */
|
||||
|
||||
/* 인쇄 스타일 요소 (NEW) */
|
||||
.jamie-card-print /* 인쇄 스타일 카드 */
|
||||
.jamie-steps /* 프로세스 스텝 */
|
||||
.jamie-step-circle-* /* 스텝 원형 (light, medium) */
|
||||
|
||||
/* 유틸리티 */
|
||||
.bg-video-light, .bg-video-dark, .bg-print-mint
|
||||
.text-gold, .text-green, .text-main
|
||||
.font-round, .font-primary, .font-en
|
||||
```
|
||||
|
||||
### 문서 출력 요청 방법
|
||||
|
||||
검토 또는 콘텐츠 생성 후 다음과 같이 요청하세요:
|
||||
|
||||
```
|
||||
"검토 결과를 HTML 보고서로 만들어줘"
|
||||
"이 내용을 프레젠테이션 형식으로 만들어줘"
|
||||
"블로그 포스트 마크다운으로 출력해줘"
|
||||
"영상 스타일의 프레젠테이션으로 만들어줘"
|
||||
```
|
||||
|
||||
### PDF 변환 방법
|
||||
|
||||
HTML 파일을 PDF로 변환하려면:
|
||||
1. **브라우저에서 열기** → 인쇄(Cmd+P) → PDF로 저장
|
||||
2. **Playwright 사용**: `npx playwright pdf input.html output.pdf`
|
||||
|
||||
---
|
||||
|
||||
## Procedure Knowledge (시술 지식)
|
||||
|
||||
### 중점 진료 분야
|
||||
| 분류 | 대표 시술 |
|
||||
|------|----------|
|
||||
| 눈 성형 | 퀵매몰법, 하이브리드 쌍꺼풀, 안검하수 눈매교정술, 눈밑지방 재배치, 듀얼 트임, 눈 재수술 |
|
||||
| 이마 성형 | 내시경 이마거상술, 내시경 눈썹거상술, 눈썹밑 피부절개술 |
|
||||
| 동안 성형 | 앞광대 리프팅, 스마스 리프팅, 자가 지방이식 |
|
||||
| 동안 시술 | 실 리프팅, 하이푸 리프팅 |
|
||||
|
||||
### 시술별 핵심 카피
|
||||
| 시술 | 핵심 표현 |
|
||||
|------|----------|
|
||||
| 퀵매몰법 | "티 안 나게 예뻐지는", "휴가를 내지 않고도" |
|
||||
| 하이브리드 쌍꺼풀 | "절개법과 매몰법의 장점만을 모은" |
|
||||
| 안검하수 눈매교정 | "졸리고 답답한 눈매를 또렷하고 시원하게" |
|
||||
| 내시경 이마거상술 | "3점 고정", "흡수성 봉합사 주문 제작" |
|
||||
| 스마스 리프팅 | "표정 근막층부터 근본적으로" |
|
||||
| 자가지방 이식 | "반영구적 유지", "나무 옮겨 심는 것처럼" |
|
||||
|
||||
---
|
||||
|
||||
## Reference Files (참조 파일)
|
||||
|
||||
### 가이드 문서
|
||||
- `guides/jamie_brand_guide_v2.8_extended.md` - 브랜드 가이드 v2.8 (영상/인쇄 컬러, 타이포그래피)
|
||||
- `guides/jamie_tone_manner_guide_v1.0.md` - 톤앤매너 상세 가이드
|
||||
- `guides/jamie_brand_guide_v1.5_restructure.md` - 브랜드 구조 가이드
|
||||
- `guides/jamie_blog_copywriter_style_guide.md` - 블로그 스타일 가이드
|
||||
|
||||
### 디자인 문서
|
||||
- `design/jamie_logo_guidelines.md` - 로고 가이드라인
|
||||
|
||||
### 템플릿
|
||||
- `templates/styles/jamie-brand.css` - 브랜드 CSS 스타일시트 v2.8
|
||||
- `templates/html/report-template.html` - 보고서 템플릿
|
||||
- `templates/html/review-result-template.html` - 검토 결과 템플릿
|
||||
- `templates/html/presentation-template.html` - 프레젠테이션 템플릿
|
||||
- `templates/markdown/blog-post-template.md` - 블로그 포스트 템플릿
|
||||
- `templates/markdown/review-report-template.md` - 검토 보고서 마크다운
|
||||
|
||||
### 팩트시트
|
||||
- `fact-sheets/procedures/` - 시술별 상세 정보 (19개 시술)
|
||||
|
||||
### 예시
|
||||
- `examples/jamie_copydeck.xlsx` - 승인된 카피 예시
|
||||
|
||||
---
|
||||
|
||||
## Commands (명령어)
|
||||
|
||||
이 스킬을 사용할 때 다음과 같은 요청이 가능합니다:
|
||||
|
||||
### 콘텐츠 작업
|
||||
1. **콘텐츠 검토**: "이 블로그 글이 제이미 브랜드에 맞는지 검토해줘"
|
||||
2. **콘텐츠 생성**: "[시술명] 블로그 포스트 작성해줘"
|
||||
3. **톤앤매너 수정**: "이 문장을 제이미 스타일로 바꿔줘"
|
||||
4. **비주얼 검토**: "이 디자인이 브랜드 가이드에 맞는지 확인해줘"
|
||||
5. **팩트체크**: "[시술명] 관련 정확한 정보 확인해줘"
|
||||
|
||||
### 문서 출력
|
||||
6. **검토 보고서**: "검토 결과를 HTML 보고서로 만들어줘"
|
||||
7. **프레젠테이션**: "이 내용을 프레젠테이션으로 만들어줘"
|
||||
8. **영상 스타일 프레젠테이션**: "영상 스타일의 프레젠테이션으로 만들어줘"
|
||||
9. **블로그 초안**: "블로그 포스트 마크다운으로 출력해줘"
|
||||
10. **PDF 준비**: "인쇄용 PDF로 변환할 수 있는 HTML 만들어줘"
|
||||
|
||||
---
|
||||
|
||||
## Channel Guidelines (채널별 적용)
|
||||
|
||||
| 채널 | 적용 지침 |
|
||||
|------|----------|
|
||||
| **웹사이트** | 표준 인사말 생략 가능, 문제-원인-해결-장점-효과 구조 유지, CTA + 상담 연결 |
|
||||
| **블로그/네이버** | "안녕하세요. 제이미성형외과입니다." (원장 이름 생략 가능), 비유와 쉬운 설명 적극 활용 |
|
||||
| **YouTube** | 표준 인사말 필수, 원장 말투 그대로 유지, "상담을 추천드립니다" CTA |
|
||||
| **Instagram** | 격식체 유지하되 문장 짧게, "여러분" 호칭 권장, "편안하게 상담해 주세요" CTA |
|
||||
| **홍보 영상** | 영상용 컬러 팔레트 적용, 원형 장식 요소 사용, 나눔스퀘어라운드 타이틀 |
|
||||
| **인쇄물** | 인쇄용 컬러 팔레트 적용, 프로세스 다이어그램 스텝 원형 사용 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Journal Channel Graphic Style Guide
|
||||
|
||||
**Channel**: "정기호의 성형외과 진료실 이야기" (https://journal.jamie.clinic)
|
||||
|
||||
When reviewing graphics for this channel, verify compliance with the Jamie Clinic blog style below.
|
||||
|
||||
### 1. Tone & Manner
|
||||
- **Professional & Medical**: Clean, organized design that builds trust. No excessive decoration or humor.
|
||||
- **Clear & Intuitive**: Simplify complex information for instant comprehension.
|
||||
- **Clean & Minimalist**: Use generous white space for an uncluttered, refined appearance.
|
||||
|
||||
### 2. Color Palette
|
||||
|
||||
| Element | Color | Hex Code |
|
||||
|---------|-------|----------|
|
||||
| **Background (Main)** | Light Blue Gray | #E0E5EB |
|
||||
| **Background (Content)** | White | #FFFFFF |
|
||||
| **Text** | Soft Black | #333333 |
|
||||
| **Accent (Upper Eyelid)** | Muted Blue | (desaturated, calm) |
|
||||
| **Accent (Lower Eyelid)** | Muted Gray | (desaturated, calm) |
|
||||
|
||||
**Important**: Avoid saturated primary colors (red, yellow, bright blue). All colors must be toned-down/muted.
|
||||
|
||||
### 3. Typography & Hierarchy
|
||||
- **Font Style**: Modern sans-serif (Gothic) with high readability. No decorative or serif fonts.
|
||||
- **Titles**: Largest size, Bold weight
|
||||
- **Body/Labels**: Medium size, Regular/Medium weight
|
||||
- **English Terms**: Medical terminology MUST include English in parentheses, slightly smaller or lighter than Korean (e.g., 상안검 (Upper Eyelid))
|
||||
|
||||
### 4. Graphic Elements Style
|
||||
- **Illustrations (Anatomy)**: Medical schematic style, not overly realistic. Clean line art with semi-transparent color overlays.
|
||||
- **Icons (Infographics)**: Simple flat design with subtle shadows/gradients for soft depth.
|
||||
- **Arrows/Pointers**: Simple, clean straight lines or gentle curves. Use brand accent colors (muted blue/gray).
|
||||
|
||||
### 5. Layout & Composition
|
||||
- **Alignment**: Center-aligned or clear 2-column/N-column grid structure for visual stability.
|
||||
- **White Space**: Generous spacing between text, images, and content blocks to minimize interference and create an airy feel.
|
||||
|
||||
### Journal Graphic Review Checklist
|
||||
- [ ] Background uses Light Blue Gray (#E0E5EB)
|
||||
- [ ] Text uses Soft Black (#333333) with sans-serif font
|
||||
- [ ] Medical terms include English translation in parentheses
|
||||
- [ ] No saturated/bright colors used
|
||||
- [ ] Illustrations are medical schematic style (not photorealistic)
|
||||
- [ ] Sufficient white space between elements
|
||||
- [ ] Grid-based or center-aligned layout
|
||||
|
||||
---
|
||||
|
||||
*This skill is created to maintain brand consistency for Jamie Plastic Surgery Clinic.*
|
||||
*Refer to this guide for all content creation and review.*
|
||||
*Version: 2.9 | Last Updated: 2026-01-21 | Journal Style Guide Added*
|
||||
278
custom-skills/42-jamie-faq-entry/SKILL.md
Normal file
278
custom-skills/42-jamie-faq-entry/SKILL.md
Normal file
@@ -0,0 +1,278 @@
|
||||
---
|
||||
name: jamie-faq-entry
|
||||
description: "카카오톡 플러스 채널 Kanana 상담매니저 Q&A 답변 생성 및 검토 스킬. 제이미성형외과의 카카오톡 채널에 등록할 고객 문의 질문과 답변 엔트리를 생성, 검토, 수정합니다. 의료광고 심의 준수, 브랜드 보이스 일관성, 카카오 카나나 가이드 규격을 모두 반영합니다. Triggers: 카나나 답변, Kanana Q&A, 카카오톡 챗봇, 카카오 상담 답변, 챗봇 문답, 자동답변 등록, 카나나 엔트리, chatbot QA, KakaoTalk channel reply, 카카오 자동응답. jamie-marketing-editor 및 jamie-brand-guardian 스킬과 연계하여 사용합니다."
|
||||
---
|
||||
|
||||
# Jamie Kanana Chatbot Q&A Skill
|
||||
|
||||
> **Purpose**: 제이미성형외과 카카오톡 플러스 채널의 Kanana 상담매니저에 등록할 Q&A 엔트리를 생성·검토·수정하는 스킬
|
||||
|
||||
## 1. 채널 기본 정보
|
||||
|
||||
### Kanana 상담매니저 프로필
|
||||
- **명칭**: Kanana 상담매니저
|
||||
- **플랫폼**: 카카오톡 플러스 채널 (제이미성형외과의원)
|
||||
- **역할**: 고객 문의에 대한 자동 답변, 예약 접수 양식 제공
|
||||
- **운영 범위**: 등록된 질문과 유사한 질문 인식 → 입력된 답변으로 응대
|
||||
|
||||
### 첫인사 문안 (확정본)
|
||||
```
|
||||
안녕하세요, 제이미성형외과 카카오톡 상담시간은
|
||||
평일 10시부터 18시, 토요일 9시 30분부터 14시까지입니다.
|
||||
연락처를 남겨주시면 상담실장님이 연락드리고 상세안내 드리겠습니다
|
||||
```
|
||||
> 첫인사에 포함된 정보(상담시간, 연락처 수집 안내)는 개별 답변에서 반복하지 않는다.
|
||||
|
||||
## 2. 글자수 제한
|
||||
|
||||
| 항목 | 제한 | 비고 |
|
||||
|---|---|---|
|
||||
| **질문(Q)** | 100자 이내 | 문장형으로 작성 |
|
||||
| **답변(A)** | 400자 이내 | 줄바꿈 포함 |
|
||||
| 공통 CTA 포함 시 본문 | 345자 이내 | CTA가 약 55자 차지 |
|
||||
|
||||
## 3. 질문(Q) 작성 원칙
|
||||
|
||||
### 3.1 문장형 등록
|
||||
단어가 아닌 문장형으로, 고객이 실제로 입력할 법한 표현으로 작성한다.
|
||||
|
||||
| ❌ 단어형 | ✅ 문장형 |
|
||||
|---|---|
|
||||
| 비용문의 | 눈 수술 비용이 궁금해요 |
|
||||
| 내시경 이마거상술 | 내시경 이마거상술에 대해 알고 싶어요 |
|
||||
| 위치 | 병원 위치가 어디인가요? |
|
||||
| 유튜브URL | 유튜브 채널 주소가 어떻게 되나요? |
|
||||
|
||||
### 3.2 구체적 표현 사용
|
||||
추상적 표현보다 구체적 상황을 반영한다.
|
||||
|
||||
| ❌ 추상적 | ✅ 구체적 |
|
||||
|---|---|
|
||||
| 영업시간 | 토요일에도 진료하나요? |
|
||||
| 예약 | 예약 잘 됐는지 확인하고 싶어요 |
|
||||
|
||||
### 3.3 1의도 1답변 원칙
|
||||
같은 의도를 가진 답변은 반드시 1개만 등록한다. 유사 답변이 여러 개이면 Kanana가 혼동하여 "모른다"고 답할 수 있다.
|
||||
|
||||
### 3.4 고객 말투 우선
|
||||
격식체보다 고객이 실제 채팅에서 쓰는 자연스러운 표현을 선택한다.
|
||||
- "제 예약이 완료되었을까요?" → **"예약 잘 됐는지 확인하고 싶어요"**
|
||||
|
||||
## 4. 답변(A) 작성 원칙
|
||||
|
||||
### 4.1 3단 구조
|
||||
```
|
||||
[1] 핵심 안내 — 질문에 대한 직접적 답변
|
||||
[2] 부가 정보 / 제이미 차별점 — 추가 맥락
|
||||
[3] CTA — 다음 행동 유도
|
||||
```
|
||||
|
||||
### 4.2 CTA 유형별 사용
|
||||
|
||||
**공통 CTA (세부 상담 유도용):**
|
||||
```
|
||||
상세한 문의 사항은 연락처 남겨주시면,
|
||||
상담실장님이 연락드리고
|
||||
세부 상담 진행할수 있도록 하겠습니다.
|
||||
```
|
||||
|
||||
적용 대상:
|
||||
- 비용 문의 (눈, 트임, 스마스, 이마, 눈썹 등)
|
||||
- 수술 소개, 상담 과정 안내
|
||||
- 사진 상담 희망
|
||||
- 비용 산정 기준 문의
|
||||
|
||||
미적용 대상:
|
||||
- 이미 예약 진행 중인 고객 (예약 확인, 마무리 멘트, 입금 완료)
|
||||
- 운영 안내 (예약금 안내, 수술전 주의사항)
|
||||
- 정보 전달 완결형 (위치, 유튜브, 운영시간)
|
||||
- 사후 관리 안내 (냉찜질, 흉터)
|
||||
- 즉시 처리 요청 (예약 변경/취소, 환불, 담당자 연결)
|
||||
- 의료 안전 안내 (약 처방, 약 복용법)
|
||||
|
||||
**즉시 행동 유도 CTA:**
|
||||
```
|
||||
전화(02-542-2399)로 문의하시면 바로 확인 가능합니다.
|
||||
```
|
||||
사용: 예약 확인, 당일 접수, 예약 변경/취소, 지각 연락
|
||||
|
||||
**담당자 연결 CTA:**
|
||||
```
|
||||
연락처와 성함을 남겨주시면 확인 후 연락드리겠습니다.
|
||||
```
|
||||
사용: 환불, 채널 관리자 연결 등 민감한 요청
|
||||
|
||||
### 4.3 브랜드 톤
|
||||
|
||||
| 항목 | 기준 |
|
||||
|---|---|
|
||||
| 종결 어미 | 격식체 (~습니다, ~드립니다) |
|
||||
| 호칭 | 고객님 / 환자분 |
|
||||
| 이모지 | 마무리에 1개 이내, 민감한 주제(환불, 의료안전)에는 미사용 |
|
||||
| 자기 지칭 | "제이미성형외과" 또는 생략 |
|
||||
|
||||
### 4.4 카카오톡 화면 줄바꿈 가이드
|
||||
|
||||
모바일 카카오톡 채팅 화면의 가독성을 위해 논리적 단위에서 줄바꿈을 적용한다.
|
||||
|
||||
```
|
||||
[줄바꿈 원칙]
|
||||
- 문장 간: 빈 줄 1개로 단락 구분
|
||||
- 항목 나열: · 기호 + 줄바꿈
|
||||
- 긴 문장: 의미 단위(15~20자)에서 줄바꿈
|
||||
- CTA 앞: 빈 줄 1개로 구분
|
||||
```
|
||||
|
||||
예시:
|
||||
```
|
||||
제이미성형외과는
|
||||
내시경 이마거상술을 중점 진료하고 있습니다.
|
||||
|
||||
수술 비용은 400만원부터이며,
|
||||
내원 상담 시 원장님이 수술 계획과 함께
|
||||
정확한 비용을 안내드립니다.
|
||||
|
||||
상세한 문의 사항은 연락처 남겨주시면,
|
||||
상담실장님이 연락드리고
|
||||
세부 상담 진행할수 있도록 하겠습니다.
|
||||
```
|
||||
|
||||
### 4.5 링크 활용
|
||||
관련 웹페이지·유튜브 URL이 있는 경우 답변에 포함한다.
|
||||
- 웹사이트: https://www.jamie.clinic
|
||||
- 유튜브: https://www.youtube.com/@jamie.clinic
|
||||
|
||||
### 4.6 양식 활용
|
||||
예약·접수 시 작성 양식을 답변으로 등록하면 Kanana가 접수를 대행할 수 있다.
|
||||
|
||||
## 5. 의료광고 심의 준수 (의료법 제56조)
|
||||
|
||||
### 5.1 금지 표현 → 대체 표현
|
||||
|
||||
| 금지 | 대체 | 사유 |
|
||||
|---|---|---|
|
||||
| 전문 / 전문병원 | 중점 진료 | 전문병원 지정 없이 사용 불가 |
|
||||
| 특화 | 중점 진료 | 과장 표현 |
|
||||
| 보장 | 운영 / 제공 | 효과 보장 금지 |
|
||||
| 완성 | 지향 | 결과 확정 표현 금지 |
|
||||
| 해결 | 개선 | 치료 효과 보장 금지 |
|
||||
| 100% / 반드시 | 대부분 / 기대할 수 있습니다 | 효과 보장 금지 |
|
||||
| 최고 / 최상급 | 사용 불가 | 과장 광고 |
|
||||
| 다른 병원보다 | 저희만의 방법으로 | 비교 광고 금지 |
|
||||
| 전후 사진/영상 | 수술 정보, 설명 영상 | 치료 전후 비교 암시 금지 |
|
||||
| 안전한 / 무통 | 사용 불가 | 소비자 현혹 |
|
||||
| 노하우 | 풍부한 경험 | 과장 표현 |
|
||||
|
||||
### 5.2 부작용 고지문
|
||||
시술·수술 관련 답변에는 다음 고지문을 포함한다 (예약·운영·위치 안내에는 불필요):
|
||||
```
|
||||
※ 개인에 따라 결과가 다를 수 있으며,
|
||||
부작용(붓기, 멍 등)이 발생할 수 있습니다.
|
||||
```
|
||||
|
||||
### 5.3 환자 경험담 표현
|
||||
- ❌ "후기" → ✅ "상담 이야기"
|
||||
- ❌ 실제 환자 경험담 → ✅ 일반적 수술 과정 설명
|
||||
|
||||
## 6. 병원 기본 정보 (답변 작성 시 참조)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 병원명 | 제이미성형외과 (띄어쓰기 없음) |
|
||||
| 전화번호 | 02-542-2399 |
|
||||
| 주소 | 서울시 강남구 압구정로 136, EHL빌딩 3층 |
|
||||
| 찾아오는 길 | 압구정역 5번출구 방향 도보 5분, 현대고등학교 맞은편 |
|
||||
| 진료시간 | 평일 10시~18시, 토 9:30~14시, 일·공휴일 휴진 |
|
||||
| 웹사이트 | https://www.jamie.clinic |
|
||||
| 유튜브 | https://www.youtube.com/@jamie.clinic |
|
||||
| 원장 | 정기호 원장 |
|
||||
| 중점 진료 | 눈·이마·동안 성형 |
|
||||
| 상담비 | 1만원 |
|
||||
| 예약금 계좌 | 하나은행 204-910172-23607 (제이미성형외과/정기호) |
|
||||
|
||||
### 진료과목 정식 명칭 (16개)
|
||||
```
|
||||
눈 성형(7): 퀵 매몰법, 하이브리드 쌍커풀, 안검하수 눈매교정술,
|
||||
눈밑지방 재배치, 듀얼 트임 수술, 눈썹밑 피부절개술, 눈 재수술
|
||||
이마 성형(2): 내시경 이마 거상술, 내시경 눈썹 거상술
|
||||
동안 성형(3): 앞광대 리프팅, 스마스 리프팅, 자가 지방이식
|
||||
동안 시술(2): 실 리프팅, 하이푸 리프팅
|
||||
기타(2): 쁘띠 성형, 흉터 성형
|
||||
```
|
||||
|
||||
## 7. 워크플로우
|
||||
|
||||
### 7.1 신규 Q&A 생성
|
||||
```
|
||||
[1] 고객 질문 의도 파악
|
||||
[2] 기존 등록 엔트리와 중복 여부 확인
|
||||
→ 중복 시: 1의도 1답변 원칙에 따라 Q 문구만 최적화
|
||||
→ 비중복 시: 새 엔트리 작성
|
||||
[3] Q 작성: 문장형, 고객 말투, 100자 이내
|
||||
[4] A 작성: 3단 구조, 400자 이내, 줄바꿈 적용
|
||||
[5] 의료법 체크: 금지 표현 스캔, 부작용 고지 필요 여부
|
||||
[6] 브랜드 톤 체크: 격식체, 호칭, 이모지
|
||||
[7] 글자수 최종 확인
|
||||
```
|
||||
|
||||
### 7.2 기존 Q&A 검토
|
||||
```
|
||||
[1] 의료법 위반 표현 스캔 (🔴 즉시 수정)
|
||||
[2] 브랜드 톤 일관성 점검 (🟡 개선 권장)
|
||||
[3] 정보 정확성 확인 (건물명, 전화번호, 비용 등)
|
||||
[4] 글자수 준수 확인
|
||||
[5] Q 문장형 여부 확인
|
||||
[6] 기존 엔트리와 중복 여부 확인
|
||||
[7] 수정안 제시: 원문 비교표 + 수정 사유
|
||||
```
|
||||
|
||||
### 7.3 배치 작업 시 출력 형식
|
||||
여러 엔트리를 한 번에 작업할 경우, 카테고리별로 그룹핑하여 다음 형식으로 출력한다:
|
||||
|
||||
```
|
||||
### 카테고리: [카테고리명]
|
||||
|
||||
**① [상태] [Q 문구]**
|
||||
|
||||
> [A 답변 — 줄바꿈 적용]
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
상태 표기:
|
||||
- 🆕 신규 등록
|
||||
- 📝 기존 수정
|
||||
- ✅ 현행 유지
|
||||
- 🔄 중복 생략
|
||||
- 💬 공통 CTA 적용
|
||||
|
||||
## 8. 엔트리 카테고리 분류
|
||||
|
||||
Q&A 엔트리는 다음 카테고리로 분류하여 관리한다:
|
||||
|
||||
| 카테고리 | 내용 |
|
||||
|---|---|
|
||||
| 병원 소개·안내 | 수술 분야, 위치, 유튜브, 운영시간 |
|
||||
| 상담·예약 | 예약 확인, 예약 양식, 사진 상담, 상담 과정, 재진 안내, 당일 접수, 예약 변경/취소, 지각 연락, 담당자 연결 |
|
||||
| 비용 안내 | 시술별 비용, 비용 산정 기준, 예약금 안내 |
|
||||
| 눈 성형 상세 | 매몰법, 퀵매몰, 봉합사, 재수술 실 제거, 한쪽 눈, 앞트임, 고정점 등 |
|
||||
| 동안·이마 성형 상세 | 이마거상술, 눈썹거상술, 스마스 리프팅, 기타 시술 |
|
||||
| 수술 전후 안내 | 수술전 주의사항, 흉터, 냉찜질, 회복기간 |
|
||||
| 결제·환불 | 입금 완료, 환불 처리 |
|
||||
| 약·처방 | 약 처방, 약 복용법 |
|
||||
|
||||
## 9. 연계 스킬
|
||||
|
||||
| 스킬 | 연계 시점 |
|
||||
|---|---|
|
||||
| jamie-marketing-editor | 답변 내 브랜드 카피, 시술 설명 문안 작성 시 |
|
||||
| jamie-brand-guardian | 의료광고 심의 준수 검토, 브랜드 톤 최종 확인 시 |
|
||||
|
||||
## 10. 참조 파일
|
||||
|
||||
| 파일 | 경로 | 용도 |
|
||||
|---|---|---|
|
||||
| 진료과목 명칭 일람 | `/mnt/project/진료과목_명칭_일람_20250430.txt` | 진료과목 정식 명칭 확인 |
|
||||
| 진료과목 소개 통합본 | `/mnt/project/제이미_성형외과_진료과목_소개_통합본.md` | 시술별 상세 설명 참조 |
|
||||
| 브랜드 가이드 | `/mnt/project/jamie_brand_guide_v2_8_extended.md` | 브랜드 톤, 컬러, 네이밍, 의료광고 준수 |
|
||||
| 현재 등록 엔트리 | `../shared/current-entries.md` | 중복 확인, 기존 답변 관리 |
|
||||
524
custom-skills/43-jamie-youtube-manager/SKILL.md
Normal file
524
custom-skills/43-jamie-youtube-manager/SKILL.md
Normal file
@@ -0,0 +1,524 @@
|
||||
---
|
||||
name: jamie-youtube-manager
|
||||
description: |
|
||||
Jamie Clinic YouTube channel SEO auditor and content manager.
|
||||
Triggers: YouTube SEO, video audit, 제이미 유튜브, channel optimization.
|
||||
---
|
||||
|
||||
# Jamie YouTube Manager Skill
|
||||
|
||||
> **Purpose**: YouTube Channel SEO Auditor & Content Manager for Jamie Plastic Surgery Clinic
|
||||
> **Platform**: Claude Code (CLI) + Claude Desktop
|
||||
> **Input**: YouTube video URLs, playlist URLs, or channel data
|
||||
> **Output**: Video info, channel stats, audit checklist + SEO recommendations
|
||||
|
||||
---
|
||||
|
||||
## CLI Scripts (Claude Code)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd ~/Project/our-claude-skills/custom-skills/43-jamie-youtube-manager/code/scripts
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
|
||||
| Script | Purpose | Usage |
|
||||
|--------|---------|-------|
|
||||
| `jamie_channel_status.py` | Channel stats overview | `python jamie_channel_status.py` |
|
||||
| `jamie_video_info.py` | Video details from URL | `python jamie_video_info.py "URL"` |
|
||||
| `jamie_youtube_api_test.py` | API connectivity test | `python jamie_youtube_api_test.py` |
|
||||
| `jamie_youtube_batch_update.py` | Batch metadata update | `python jamie_youtube_batch_update.py` |
|
||||
|
||||
### Channel Stats Example
|
||||
```bash
|
||||
python jamie_channel_status.py
|
||||
# Output: Channel name, subscribers, views, recent videos with status
|
||||
```
|
||||
|
||||
### Video Info from URL
|
||||
```bash
|
||||
python jamie_video_info.py "https://youtu.be/VIDEO_ID"
|
||||
# Output: Title, description, duration, views, likes, tags, timestamps, privacy status
|
||||
```
|
||||
|
||||
### Integration with Notion Writer
|
||||
```bash
|
||||
# Save video info to Notion
|
||||
python jamie_video_info.py "URL" > ../output/video_status.md
|
||||
cd ~/Project/our-claude-skills/custom-skills/02-notion-writer/code/scripts
|
||||
source venv/bin/activate
|
||||
python notion_writer.py -p NOTION_PAGE_URL -f ../../43-jamie-youtube-manager/code/output/video_status.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
[Input: YouTube URL]
|
||||
↓
|
||||
[1. Fetch Video/Playlist Data]
|
||||
↓
|
||||
[2. Metadata Audit]
|
||||
↓
|
||||
[3. Chapter/Timestamp Check]
|
||||
↓
|
||||
[4. Transcript Review]
|
||||
↓
|
||||
[5. Schema Validation]
|
||||
↓
|
||||
[6. i18n Assessment]
|
||||
↓
|
||||
[Output: Audit Report + Recommendations]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Types
|
||||
|
||||
| Type | Characteristics | Audit Focus |
|
||||
|------|-----------------|-------------|
|
||||
| **Long-form Video** | 5-30+ min, educational | Chapters, transcript, schema |
|
||||
| **Shorts** | < 60 sec, vertical | Hook, hashtags, thumbnail |
|
||||
| **Playlist** | Grouped videos | Naming, ordering, descriptions |
|
||||
|
||||
---
|
||||
|
||||
## Audit Checklist
|
||||
|
||||
### 1. Metadata Audit (메타데이터 감사)
|
||||
|
||||
#### Title (제목)
|
||||
|
||||
| Criteria | Standard | Check |
|
||||
|----------|----------|-------|
|
||||
| Length | 60-70 characters (Korean ~30자) | [ ] |
|
||||
| Primary keyword | First 40 characters | [ ] |
|
||||
| Brand mention | "제이미성형외과" included | [ ] |
|
||||
| Click appeal | Clear benefit/curiosity | [ ] |
|
||||
| No clickbait | Accurate to content | [ ] |
|
||||
|
||||
**Title Formula for Jamie**:
|
||||
```
|
||||
[시술명] + [핵심 베네핏] + 제이미성형외과
|
||||
예: "내시경 이마거상술 | 자연스러운 동안 효과의 비밀 | 제이미성형외과"
|
||||
```
|
||||
|
||||
#### Description (설명)
|
||||
|
||||
| Section | Required Content | Position |
|
||||
|---------|------------------|----------|
|
||||
| Hook | 핵심 내용 요약 (2-3줄) | 첫 150자 |
|
||||
| Timestamps | 챕터 목차 | 상단 |
|
||||
| Main content | 상세 설명, 키워드 자연 배치 | 중간 |
|
||||
| CTA | 상담 예약 링크, 채널 구독 | 하단 |
|
||||
| Links | 웹사이트, SNS, 관련 영상 | 하단 |
|
||||
| Hashtags | 3-5개 관련 해시태그 | 최하단 |
|
||||
|
||||
**Description Template**:
|
||||
```
|
||||
[첫 2줄 - 영상 핵심 내용]
|
||||
[시술명]에 대해 알아보세요. 제이미성형외과 정기호 원장이 설명합니다.
|
||||
|
||||
⏱️ 타임스탬프
|
||||
00:00 인트로
|
||||
01:23 [주제1]
|
||||
03:45 [주제2]
|
||||
...
|
||||
|
||||
📋 영상 내용
|
||||
[상세 설명 - 키워드 자연 포함]
|
||||
|
||||
🏥 제이미성형외과
|
||||
📍 주소: 서울시 강남구 압구정로...
|
||||
📞 상담예약: 02-XXX-XXXX
|
||||
🔗 홈페이지: https://...
|
||||
📱 카카오톡: ...
|
||||
|
||||
#제이미성형외과 #[시술명] #압구정성형외과
|
||||
```
|
||||
|
||||
#### Tags (태그)
|
||||
|
||||
| Category | Examples | Count |
|
||||
|----------|----------|-------|
|
||||
| Brand | 제이미성형외과, Jamie Plastic Surgery | 2-3 |
|
||||
| Procedure | 이마거상술, 내시경이마거상, forehead lift | 5-8 |
|
||||
| General | 성형외과, 압구정, 동안성형 | 3-5 |
|
||||
| Long-tail | 이마주름개선, 눈썹처짐교정 | 3-5 |
|
||||
|
||||
**Total**: 15-20 tags (max 500 characters)
|
||||
|
||||
### 2. Chapter Timestamps (챕터 타임스탬프)
|
||||
|
||||
#### Requirements
|
||||
|
||||
| Criteria | Standard |
|
||||
|----------|----------|
|
||||
| Minimum chapters | 3+ for videos > 5 min |
|
||||
| First timestamp | Must start at 00:00 |
|
||||
| Format | MM:SS or HH:MM:SS |
|
||||
| Placement | Description (visible area) |
|
||||
| Labels | Clear, descriptive Korean |
|
||||
|
||||
#### Chapter Best Practices
|
||||
|
||||
```
|
||||
✅ Good Example:
|
||||
00:00 인트로
|
||||
00:45 이마거상술이란?
|
||||
02:30 수술 과정 설명
|
||||
05:15 회복 기간 및 주의사항
|
||||
08:00 자주 묻는 질문
|
||||
10:30 마무리 및 상담 안내
|
||||
|
||||
❌ Bad Example:
|
||||
00:00 시작
|
||||
02:00 본론
|
||||
08:00 끝
|
||||
```
|
||||
|
||||
#### Auto-Chapter Detection
|
||||
|
||||
If timestamps missing, suggest based on:
|
||||
- Topic transitions in transcript
|
||||
- Visual scene changes (if accessible)
|
||||
- Standard medical video structure
|
||||
|
||||
### 3. Transcript Review (자막/스크립트 검토)
|
||||
|
||||
#### Auto-Generated Caption Audit
|
||||
|
||||
| Check | Action |
|
||||
|-------|--------|
|
||||
| Medical terms accuracy | 의학 용어 오타 수정 |
|
||||
| Brand name spelling | "제이미성형외과" 정확히 |
|
||||
| Procedure names | 시술명 정확성 확인 |
|
||||
| Numbers/dates | 숫자 표기 확인 |
|
||||
| Speaker labels | 화자 구분 (필요시) |
|
||||
|
||||
#### Transcript Enhancement
|
||||
|
||||
Priority corrections for Jamie content:
|
||||
```
|
||||
Common errors to fix:
|
||||
- "이마 거상" → "이마거상술"
|
||||
- "제이미" 누락/오타
|
||||
- 의학 용어 띄어쓰기
|
||||
- 영어 의학 용어 정확성
|
||||
```
|
||||
|
||||
### 4. Schema Validation (스키마 검증)
|
||||
|
||||
#### VideoObject Schema Requirements
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "VideoObject",
|
||||
"name": "[Video Title]",
|
||||
"description": "[Video Description]",
|
||||
"thumbnailUrl": "[Thumbnail URL]",
|
||||
"uploadDate": "YYYY-MM-DD",
|
||||
"duration": "PT[X]M[Y]S",
|
||||
"contentUrl": "[Video URL]",
|
||||
"embedUrl": "[Embed URL]",
|
||||
"interactionStatistic": {
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/WatchAction",
|
||||
"userInteractionCount": [view count]
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "제이미성형외과",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "[Logo URL]"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Medical Video Extensions
|
||||
|
||||
For medical content, recommend adding:
|
||||
```json
|
||||
{
|
||||
"@type": ["VideoObject", "MedicalWebPage"],
|
||||
"specialty": "PlasticSurgery",
|
||||
"medicalAudience": {
|
||||
"@type": "PatientAudience"
|
||||
},
|
||||
"lastReviewed": "YYYY-MM-DD"
|
||||
}
|
||||
```
|
||||
|
||||
#### Schema Checklist
|
||||
|
||||
| Property | Required | Jamie Standard |
|
||||
|----------|----------|----------------|
|
||||
| name | Yes | Match title exactly |
|
||||
| description | Yes | First 160 chars meaningful |
|
||||
| thumbnailUrl | Yes | High-res, branded |
|
||||
| uploadDate | Yes | ISO 8601 format |
|
||||
| duration | Yes | ISO 8601 duration |
|
||||
| publisher | Recommended | 제이미성형외과 info |
|
||||
| hasPart (Clips) | For chapters | Match timestamps |
|
||||
|
||||
### 5. Internationalization (다국어/국제화)
|
||||
|
||||
#### Language Settings
|
||||
|
||||
| Setting | Recommendation |
|
||||
|---------|----------------|
|
||||
| Primary language | Korean (ko) |
|
||||
| Default audio | Korean |
|
||||
| Title/Description | Korean primary |
|
||||
| Subtitles | Korean (manual), English (auto+edit) |
|
||||
|
||||
#### Subtitle Priority
|
||||
|
||||
| Language | Priority | Target Audience |
|
||||
|----------|----------|-----------------|
|
||||
| Korean (ko) | Required | 내국인 |
|
||||
| English (en) | High | Medical tourism |
|
||||
| Japanese (ja) | Medium | 일본 의료관광객 |
|
||||
| Chinese (zh) | Medium | 중국 의료관광객 |
|
||||
|
||||
#### Localized Metadata
|
||||
|
||||
For key videos, consider:
|
||||
```
|
||||
Title (English): Endoscopic Forehead Lift | Natural Rejuvenation | Jamie Plastic Surgery
|
||||
Title (Japanese): 内視鏡額リフト | 自然な若返り | ジェイミー整形外科
|
||||
```
|
||||
|
||||
#### i18n Checklist
|
||||
|
||||
| Item | Check |
|
||||
|------|-------|
|
||||
| Korean subtitles (manual) | [ ] |
|
||||
| English subtitles | [ ] |
|
||||
| Subtitle timing accuracy | [ ] |
|
||||
| Localized title (EN) | [ ] |
|
||||
| Localized description (EN) | [ ] |
|
||||
| End screen localization | [ ] |
|
||||
|
||||
### 6. Shorts-Specific Audit (쇼츠 전용)
|
||||
|
||||
| Element | Standard |
|
||||
|---------|----------|
|
||||
| Duration | < 60 seconds |
|
||||
| Aspect ratio | 9:16 vertical |
|
||||
| Hook | First 1-3 seconds captivating |
|
||||
| Text overlay | Readable, on-brand |
|
||||
| Hashtags | #Shorts + 2-3 relevant |
|
||||
| Music/Sound | Trending or original |
|
||||
| CTA | Subscribe/Follow prompt |
|
||||
|
||||
### 7. Playlist Audit (재생목록 감사)
|
||||
|
||||
| Element | Checklist |
|
||||
|---------|-----------|
|
||||
| Playlist title | Descriptive, keyword-rich |
|
||||
| Playlist description | 200+ characters, links |
|
||||
| Video order | Logical sequence |
|
||||
| Thumbnail consistency | Visual brand cohesion |
|
||||
| Missing videos | Gap analysis |
|
||||
| Duplicate content | Remove redundancy |
|
||||
|
||||
**Recommended Playlists for Jamie**:
|
||||
```
|
||||
1. 눈성형 시리즈 (Eye Surgery Series)
|
||||
2. 이마/리프팅 시리즈 (Forehead/Lifting Series)
|
||||
3. 자주 묻는 질문 FAQ
|
||||
4. 원장 토크 (Director's Talk)
|
||||
5. 수술 후 관리 (Post-Op Care)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO Enhancement Recommendations
|
||||
|
||||
### Quick Wins
|
||||
|
||||
1. **Add timestamps** to all videos > 3 min
|
||||
2. **Optimize first 150 chars** of description
|
||||
3. **Include brand** in every title
|
||||
4. **Add Korean captions** (manual review)
|
||||
5. **Create consistent thumbnails**
|
||||
|
||||
### Advanced Optimization
|
||||
|
||||
1. **VideoObject schema** on website embeds
|
||||
2. **Clip schema** for key moments
|
||||
3. **Playlist** strategic grouping
|
||||
4. **End screens** linking related videos
|
||||
5. **Cards** for CTA and related content
|
||||
|
||||
### Content Gap Analysis
|
||||
|
||||
Compare against competitors:
|
||||
- Missing procedure topics
|
||||
- FAQ not addressed
|
||||
- Trending formats not utilized
|
||||
- Shorts opportunities
|
||||
|
||||
---
|
||||
|
||||
## Audit Report Template
|
||||
|
||||
### Video Audit Summary
|
||||
|
||||
```
|
||||
📹 Video: [Title]
|
||||
🔗 URL: [YouTube URL]
|
||||
📅 Audit Date: YYYY-MM-DD
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📊 OVERALL SCORE: [X]/100
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
✅ PASSED (X items)
|
||||
- [Item 1]
|
||||
- [Item 2]
|
||||
|
||||
⚠️ NEEDS IMPROVEMENT (X items)
|
||||
- [Item 1]: [Recommendation]
|
||||
- [Item 2]: [Recommendation]
|
||||
|
||||
❌ MISSING (X items)
|
||||
- [Item 1]: [How to fix]
|
||||
- [Item 2]: [How to fix]
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📋 DETAILED CHECKLIST
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
[Metadata]
|
||||
☑️ Title optimized
|
||||
☐ Description needs timestamps
|
||||
☑️ Tags complete
|
||||
|
||||
[Chapters]
|
||||
☐ No timestamps found
|
||||
→ Suggested chapters: [list]
|
||||
|
||||
[Transcript]
|
||||
☑️ Auto-captions available
|
||||
☐ Manual Korean captions missing
|
||||
|
||||
[Schema]
|
||||
☐ VideoObject not detected
|
||||
→ Provide schema template
|
||||
|
||||
[i18n]
|
||||
☐ English subtitles missing
|
||||
☐ No localized metadata
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🎯 TOP 3 PRIORITIES
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
1. [Highest impact action]
|
||||
2. [Second priority]
|
||||
3. [Third priority]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Single Video Audit
|
||||
```
|
||||
"이 유튜브 영상 SEO 감사해줘: [URL]"
|
||||
"유튜브 영상 메타데이터 최적화 확인해줘"
|
||||
```
|
||||
|
||||
### Playlist Audit
|
||||
```
|
||||
"이 재생목록 검토해줘: [Playlist URL]"
|
||||
"플레이리스트 구성 개선안 줘"
|
||||
```
|
||||
|
||||
### Shorts Review
|
||||
```
|
||||
"쇼츠 영상들 점검해줘"
|
||||
"Shorts SEO 체크리스트 확인"
|
||||
```
|
||||
|
||||
### Bulk Audit
|
||||
```
|
||||
"제이미 유튜브 채널 전체 감사"
|
||||
"최근 10개 영상 SEO 상태 확인"
|
||||
```
|
||||
|
||||
### Schema Generation
|
||||
```
|
||||
"이 영상의 VideoObject 스키마 만들어줘"
|
||||
"구조화된 데이터 추천해줘"
|
||||
```
|
||||
|
||||
### i18n Setup
|
||||
```
|
||||
"영어 자막/메타데이터 추가 가이드"
|
||||
"다국어 설정 최적화"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Medical Advertising Compliance
|
||||
|
||||
### YouTube Content Rules (의료광고법)
|
||||
|
||||
**Allowed**:
|
||||
- Educational procedure explanations
|
||||
- General recovery information
|
||||
- Doctor credentials and expertise
|
||||
- Facility tours
|
||||
|
||||
**Prohibited**:
|
||||
- Patient testimonials
|
||||
- Before/After without disclaimers
|
||||
- Guaranteed results claims
|
||||
- Price comparisons
|
||||
|
||||
### Required Disclaimer
|
||||
|
||||
Include in description:
|
||||
```
|
||||
※ 본 영상은 정보 제공 목적이며, 개인에 따라 결과가 다를 수 있습니다.
|
||||
수술 전 반드시 전문의와 상담하시기 바랍니다.
|
||||
부작용: 출혈, 감염, 염증 등이 발생할 수 있습니다.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brand Integration
|
||||
|
||||
| Element | YouTube Standard |
|
||||
|---------|------------------|
|
||||
| Channel name | 제이미성형외과 |
|
||||
| Handle | @jamie-plasticsurgery (recommended) |
|
||||
| Logo | Consistent across all videos |
|
||||
| Thumbnails | Branded template, faces visible |
|
||||
| Intro/Outro | 3-5 sec branded bumper |
|
||||
| Tone | Professional yet approachable |
|
||||
|
||||
---
|
||||
|
||||
## Available Resources
|
||||
|
||||
```
|
||||
references/
|
||||
├── youtube_seo_checklist.md # Complete audit checklist
|
||||
├── video_schema_templates.md # JSON-LD schema templates
|
||||
├── description_templates.md # Description copy templates
|
||||
└── shorts_optimization_guide.md # Shorts-specific guidelines
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Version 1.0.0 | 2025-12-22 | Claude Desktop Skill*
|
||||
244
custom-skills/44-jamie-youtube-subtitle-checker/SKILL.md
Normal file
244
custom-skills/44-jamie-youtube-subtitle-checker/SKILL.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
name: jamie-youtube-subtitle-checker
|
||||
description: |
|
||||
SBV subtitle file typo corrector and YouTube metadata generator for Jamie Clinic.
|
||||
Triggers: check subtitles, subtitle QA, SBV correction, 자막 교정.
|
||||
---
|
||||
|
||||
# Jamie YouTube Subtitle Editor Skill
|
||||
|
||||
> **Purpose**: SBV 자막 파일 오타 교정 및 YouTube 메타데이터 생성
|
||||
> **Input**: YouTube에서 다운로드한 SBV 포맷 자막 파일
|
||||
> **Output**: 교정된 SBV 파일 + YouTube 메타데이터 패키지
|
||||
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
[Input: SBV 자막 파일]
|
||||
↓
|
||||
[1. SBV 파싱 및 텍스트 추출]
|
||||
↓
|
||||
[2. 오타 자동 교정 (typo_dictionary 적용)]
|
||||
↓
|
||||
[3. 의학 용어 표준화]
|
||||
↓
|
||||
[4. 챕터 타임스탬프 추출]
|
||||
↓
|
||||
[5. YouTube 메타데이터 생성]
|
||||
↓
|
||||
[Output: 교정된 SBV + 메타데이터 패키지]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SBV Format Specification
|
||||
|
||||
### SBV 구조
|
||||
```
|
||||
[시작시간],[종료시간]
|
||||
자막 텍스트 (1줄 또는 여러 줄)
|
||||
|
||||
[시작시간],[종료시간]
|
||||
자막 텍스트
|
||||
...
|
||||
```
|
||||
|
||||
### 시간 형식
|
||||
- `H:MM:SS.sss` 또는 `M:SS.sss`
|
||||
- 예: `0:00:05.120,0:00:08.450`
|
||||
|
||||
### 파싱 규칙
|
||||
1. 빈 줄로 자막 블록 구분
|
||||
2. 첫 줄: 타임스탬프 (콤마로 시작/종료 구분)
|
||||
3. 나머지 줄: 자막 텍스트
|
||||
|
||||
---
|
||||
|
||||
## 오타 교정 시스템
|
||||
|
||||
### 1. 브랜드명 오타
|
||||
|
||||
| 오타 패턴 | 정정 |
|
||||
|----------|------|
|
||||
| 데이미, 재이미, 제의미 | 제이미 |
|
||||
| 성액과, 성형과, 성현외과 | 성형외과 |
|
||||
| 제이미성과 | 제이미 성형외과 |
|
||||
|
||||
### 2. 시술명 오타
|
||||
|
||||
| 오타 패턴 | 정정 |
|
||||
|----------|------|
|
||||
| 쌍거풀, 쌍거플, 쌍커풀 | 쌍꺼풀 |
|
||||
| 매물법, 매몰밥, 메몰법 | 매몰법 |
|
||||
| 눈매교정, 눈메교정 | 눈매교정술 |
|
||||
| 안검하수, 안겁하수 | 안검하수 |
|
||||
| 이마거상, 이마 거상 | 이마거상술 |
|
||||
| 눈썹거상, 눈섭거상 | 눈썹거상술 |
|
||||
| 지방재배치, 지방 재배치 | 지방재배치술 |
|
||||
| 스마스, SMAS | 스마스 |
|
||||
| 하이푸, 하이프 | 하이푸 |
|
||||
| 듀얼트임, 듀얼 트임 | 듀얼 트임 수술 |
|
||||
|
||||
### 3. 일반 의학 용어 오타
|
||||
|
||||
| 오타 패턴 | 정정 |
|
||||
|----------|------|
|
||||
| 요분의 | 여분의 |
|
||||
| 수면마취, 수면 마취 | 수면마취 |
|
||||
| 국소마취, 국소 마취 | 국소마취 |
|
||||
| 절계, 절게 | 절개 |
|
||||
| 봉합, 봉헙 | 봉합 |
|
||||
| 회복기간, 회복 기간 | 회복 기간 |
|
||||
|
||||
---
|
||||
|
||||
## 공식 진료과목 명칭
|
||||
|
||||
### 눈 성형
|
||||
- 퀵 매몰법
|
||||
- 하이브리드 쌍커풀
|
||||
- 안검하수 눈매교정술
|
||||
- 눈밑지방 재배치
|
||||
- 듀얼 트임 수술
|
||||
- 눈썹밑 피부절개술
|
||||
- 눈 재수술
|
||||
|
||||
### 이마 성형
|
||||
- 내시경 이마 거상술
|
||||
- 내시경 눈썹 거상술
|
||||
|
||||
### 동안 성형
|
||||
- 앞광대 리프팅
|
||||
- 스마스 리프팅
|
||||
- 자가 지방이식
|
||||
- 실 리프팅
|
||||
- 하이푸 리프팅
|
||||
|
||||
---
|
||||
|
||||
## 챕터 추출 로직
|
||||
|
||||
### 영상 구조 패턴 (정기호 원장 스타일)
|
||||
|
||||
| 섹션 | 일반적 시작 시간 | 키워드 |
|
||||
|------|-----------------|--------|
|
||||
| 인트로 | 0:00 | "안녕하세요", "제이미성형외과" |
|
||||
| 문제 제기 | 0:15~0:30 | "고민", "걱정", "불편" |
|
||||
| 시술 소개 | 0:30~1:00 | "[시술명]이란", "방법", "특징" |
|
||||
| 장점/효과 | 중반부 | "장점", "효과", "결과" |
|
||||
| 회복/주의사항 | 후반부 | "회복", "주의", "관리" |
|
||||
| 마무리/CTA | 마지막 20초 | "상담", "문의", "감사" |
|
||||
|
||||
### 챕터 포맷
|
||||
|
||||
```
|
||||
0:00 인트로
|
||||
0:17 [주제1 - 문제 제기]
|
||||
0:33 [주제2 - 시술 설명]
|
||||
0:50 [주제3 - 장점/특징]
|
||||
1:10 [주제4 - 회복/관리]
|
||||
1:25 마무리
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output Specifications
|
||||
|
||||
### 1. 교정된 SBV 파일
|
||||
|
||||
**파일명**: `{원본파일명}_corrected.sbv`
|
||||
|
||||
### 2. YouTube 메타데이터 패키지
|
||||
|
||||
**파일명**: `youtube_video_info.md`
|
||||
|
||||
**내용 구성**:
|
||||
```markdown
|
||||
# YouTube 영상 정보
|
||||
|
||||
## 추천 제목
|
||||
[시술명] | [핵심 키워드] | 제이미성형외과
|
||||
|
||||
## 챕터 (Chapters)
|
||||
0:00 인트로
|
||||
...
|
||||
|
||||
## 영상 설명 (Description)
|
||||
[첫 2줄 요약]
|
||||
|
||||
⏱️ 타임스탬프
|
||||
[챕터 목록]
|
||||
|
||||
🏥 제이미성형외과
|
||||
📍 서울시 강남구 압구정로 136 EHL빌딩 3층
|
||||
📞 02-542-2399
|
||||
🌐 https://jamie.clinic
|
||||
|
||||
#제이미성형외과 #[시술명] #압구정성형외과 #[관련태그]
|
||||
|
||||
## 오타 수정 내역
|
||||
| 위치 | 원본 | 수정 |
|
||||
|------|------|------|
|
||||
| 0:05 | 성액과 | 성형외과 |
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 기본 사용
|
||||
```
|
||||
"이 SBV 자막 파일 오타 교정해줘"
|
||||
"유튜브 자막 수정하고 챕터 추출해줘"
|
||||
```
|
||||
|
||||
### 파일 업로드 후
|
||||
```
|
||||
"자막 파일 교정하고 YouTube 메타데이터 만들어줘"
|
||||
"SBV 오타 수정 + 영상 정보 패키지 생성"
|
||||
```
|
||||
|
||||
### 특정 요청
|
||||
```
|
||||
"챕터 타임스탬프만 추출해줘"
|
||||
"오타 수정 내역 리포트만 줘"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
### 교정 완료 확인
|
||||
- [ ] 브랜드명 "제이미성형외과" 정확히 표기
|
||||
- [ ] 시술명 공식 명칭으로 통일
|
||||
- [ ] 의학 용어 맞춤법 확인
|
||||
- [ ] 띄어쓰기 일관성
|
||||
|
||||
### 챕터 확인
|
||||
- [ ] 0:00 인트로 포함
|
||||
- [ ] 주요 전환점 반영
|
||||
- [ ] 시간 형식 통일 (M:SS)
|
||||
|
||||
### 메타데이터 확인
|
||||
- [ ] 제목에 시술명 + 브랜드 포함
|
||||
- [ ] 해시태그 3-5개
|
||||
- [ ] 병원 연락처 정확
|
||||
|
||||
---
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `references/typo_dictionary.json` - 오타 교정 사전
|
||||
- `references/chapter_patterns.md` - 챕터 추출 패턴
|
||||
- `references/youtube_metadata.md` - 메타데이터 템플릿
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **의료광고법 고지문**: 자막 파일에는 포함하지 않음 (YouTube 설명란에만 삽입)
|
||||
- **SBV 전용**: YouTube 다운로드 기본 포맷인 SBV만 지원
|
||||
- **한국어 특화**: 제이미성형외과 콘텐츠 전용 오타 사전
|
||||
312
custom-skills/45-jamie-instagram-manager/SKILL.md
Normal file
312
custom-skills/45-jamie-instagram-manager/SKILL.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
name: jamie-instagram-manager
|
||||
description: |
|
||||
Jamie Clinic Instagram account manager for engagement, content planning, and boost strategy.
|
||||
Triggers: Instagram management, 제이미 인스타그램, IG strategy, social media.
|
||||
---
|
||||
|
||||
# Jamie Instagram Manager Skill
|
||||
|
||||
> **Purpose**: Dedicated Instagram Account Manager for Jamie Plastic Surgery Clinic
|
||||
> **Platform**: Claude Desktop with Instagram Graph API MCP
|
||||
> **Language**: Korean Primary (한국어 우선), occasional English
|
||||
> **Posting Target**: 피드 2-3x + 릴스 1-2x weekly + 스토리 daily
|
||||
|
||||
---
|
||||
|
||||
## Role Definition
|
||||
|
||||
| This Skill | Related Skills |
|
||||
|------------|----------------|
|
||||
| Instagram account management | `jamie-brand-editor`: Content generation |
|
||||
| Engagement & replies | `jamie-brand-guardian`: Compliance review |
|
||||
| Posting calendar | - |
|
||||
| Boost strategies | - |
|
||||
|
||||
**Workflow**: Use this skill for Instagram-specific tasks → Generate content with `jamie-brand-editor` → Review with `jamie-brand-guardian` before posting.
|
||||
|
||||
---
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### 1. Account Status Analysis (계정 상태 분석)
|
||||
|
||||
Use Instagram MCP tools to retrieve and analyze:
|
||||
|
||||
```
|
||||
Required Data Points:
|
||||
- Follower count & growth trend
|
||||
- Engagement rate (likes, comments, saves, shares)
|
||||
- Reach vs. Impressions
|
||||
- Profile visits & website clicks
|
||||
- Top performing posts (last 30 days)
|
||||
- Audience demographics
|
||||
```
|
||||
|
||||
**Analysis Framework**:
|
||||
| Metric | Benchmark | Action Trigger |
|
||||
|--------|-----------|----------------|
|
||||
| Engagement Rate | ≥3% (확정 목표) | < 2% requires content review |
|
||||
| Follower Growth | +1-2% monthly | Negative = engagement issue |
|
||||
| Save Rate | 2-5% | High = educational content working |
|
||||
| Comment Ratio | 0.1-0.5% | Low = CTA weakness |
|
||||
|
||||
### 2. Follower Engagement (팔로워 소통)
|
||||
|
||||
#### Comment Reply Guidelines
|
||||
|
||||
**Response Tone** (인스타그램 톤):
|
||||
- More casual than blog, but still professional
|
||||
- Warm and appreciative
|
||||
- Use soft emoji sparingly (max 1-2 per reply)
|
||||
- Always end with invitation to consult
|
||||
|
||||
**Reply Templates by Type**:
|
||||
|
||||
| Comment Type | Response Pattern | Example |
|
||||
|--------------|------------------|---------|
|
||||
| 시술 문의 | 감사 + 간단 설명 + 상담 유도 | "관심 가져주셔서 감사합니다. [시술명]은 자연스러운 개선을 목표로 합니다. 자세한 상담은 프로필 링크에서 예약 가능해요!" |
|
||||
| 칭찬/응원 | 진심 감사 + 약속 | "따뜻한 말씀 감사드려요. 앞으로도 안전하고 자연스러운 결과를 위해 최선을 다하겠습니다." |
|
||||
| 비용 문의 | 상담 유도 (가격 직접 언급 X) | "문의 감사합니다! 정확한 안내는 개인별 상태에 따라 달라 상담을 통해 안내드리고 있어요. 프로필 링크로 예약해주세요." |
|
||||
| 회복 문의 | 일반적 정보 + 개인차 언급 | "보통 [기간] 정도면 일상 복귀 가능하시지만, 개인차가 있어 상담 시 자세히 안내드릴게요!" |
|
||||
| 부정적 피드백 | 공감 + DM 유도 | "불편을 드려 죄송합니다. DM으로 자세한 내용 보내주시면 확인 후 연락드리겠습니다." |
|
||||
|
||||
**DM Response Priority**:
|
||||
1. **긴급**: 수술 후 이상 증상 → 즉시 연락처 안내
|
||||
2. **높음**: 예약/상담 문의 → 24시간 내 응답
|
||||
3. **보통**: 일반 문의 → 48시간 내 응답
|
||||
4. **낮음**: 감사/인사 → 72시간 내 응답
|
||||
|
||||
**DM 전환 퍼널 (확정)**:
|
||||
Instagram 콘텐츠 → DM 문의 → 카카오톡 채널 전환 → 카카오톡 상담 → 예약 확정
|
||||
KPI: DM→카카오→예약 전환율 30% 목표
|
||||
|
||||
### 3. Content Planning (콘텐츠 기획)
|
||||
|
||||
#### Weekly Posting Schedule (확정)
|
||||
|
||||
피드: 주 2-3회 (1080x1350px)
|
||||
스토리: 매일 1-2회
|
||||
릴스: 주 1-2회 (30-60초, 세로 9:16)
|
||||
최적 게시 시간: 오전 9시 / 오후 12시 / 오후 7시
|
||||
|
||||
| 요일 | 콘텐츠 유형 | 포맷 |
|
||||
|------|------------|------|
|
||||
| 월 | 주간 인사 / 일상 | 스토리 + 피드 |
|
||||
| 화 | Q&A 릴스 | 릴스 |
|
||||
| 수 | 점심/간식 일상 | 스토리 |
|
||||
| 목 | 카드뉴스 (시술 정보) | 피드 (캐러셀) |
|
||||
| 금 | 주말 인사 / TMI | 스토리 + 피드 |
|
||||
| 주말 | 릴스 또는 예약 안내 | 릴스/스토리 |
|
||||
|
||||
#### Content Pillar Framework
|
||||
|
||||
```
|
||||
제이미 인스타그램 콘텐츠 4 Pillar (확정):
|
||||
|
||||
1. Q&A 릴스 (40%)
|
||||
- 정기호 원장님 YouTube Q&A → 30-60초 릴스 편집
|
||||
- YouTube Shorts 크로스포스팅
|
||||
- 빈도: 주 1-2회 (릴스)
|
||||
- 디자인: Dark 테마 기본, Q=Gold / A=White
|
||||
|
||||
2. 일상 콘텐츠 (30%)
|
||||
- 병원 분위기, 스태프 일상, 원장님 출근길, 점심 메뉴
|
||||
- 빈도: 주 2회 (피드 + 스토리)
|
||||
- 디자인: 사진 중심, 웜톤 보정, 하단 오버레이
|
||||
|
||||
3. 카드뉴스 (20%)
|
||||
- 시술 정보 요약, Ghost/Naver 블로그 AI 요약 → 카드뉴스 변환
|
||||
- 빈도: 주 1회 (피드 캐러셀)
|
||||
- 디자인: Light 테마, Floating Circle, 5-7장 구성
|
||||
|
||||
4. 환자 에피소드 (10%)
|
||||
- 리얼모델/동의 환자 기반 경험담
|
||||
- 빈도: 월 2-3회 (피드/릴스)
|
||||
- 디자인: Soft 테마, Gold 카테고리 뱃지, 면책 고지문 필수
|
||||
```
|
||||
|
||||
#### Hashtag Strategy
|
||||
|
||||
**Core Hashtags (항상 포함)**:
|
||||
```
|
||||
#제이미성형외과 #압구정성형외과 #자연스러운성형
|
||||
```
|
||||
|
||||
**Category Hashtags (콘텐츠별)**:
|
||||
| Category | Hashtags |
|
||||
|----------|----------|
|
||||
| 눈성형 | #쌍꺼풀 #눈성형 #눈매교정 #자연스러운쌍꺼풀 |
|
||||
| 이마성형 | #이마거상술 #내시경이마거상 #동안성형 |
|
||||
| 리프팅 | #얼굴리프팅 #스마스리프팅 #동안 |
|
||||
|
||||
**Volume Rule**: 15-20 hashtags total (5 core + 10-15 category)
|
||||
|
||||
### 4. Storytelling Ideas (스토리텔링 아이디어)
|
||||
|
||||
#### Story Formats
|
||||
|
||||
| Format | Frequency | Content |
|
||||
|--------|-----------|---------|
|
||||
| Behind-the-scenes | 주 2회 | 상담실, 회복실, 수술 준비 |
|
||||
| Poll/Quiz | 주 1회 | "어떤 눈매가 자연스러워 보이나요?" |
|
||||
| Q&A Box | 주 1회 | 팔로워 질문 답변 |
|
||||
| Countdown | 이벤트 시 | 상담 이벤트 등 |
|
||||
|
||||
#### Reels Ideas (릴스 아이디어)
|
||||
|
||||
1. **교육 시리즈**: "1분만에 알아보는 [시술명]"
|
||||
2. **Before/After**: 고지문 포함, 개인정보 블러 처리
|
||||
3. **원장 토크**: 시술 철학, 자주 묻는 질문
|
||||
4. **회복 브이로그**: 타임라인 형식 (환자 동의 필수)
|
||||
5. **트렌드 활용**: 인기 오디오 + 제이미 메시지
|
||||
|
||||
### 5. Boost & Promotion Strategy (부스트 전략)
|
||||
|
||||
#### Organic vs. Paid Decision Matrix
|
||||
|
||||
| Metric | Organic First | Consider Boosting |
|
||||
|--------|---------------|-------------------|
|
||||
| Engagement | > 4% | < 2% |
|
||||
| Reach | Growing | Declining 2주+ |
|
||||
| Saves | > 3% | < 1% |
|
||||
| Comments | Active | Low |
|
||||
|
||||
#### Boost Budget Allocation
|
||||
|
||||
```
|
||||
월 예산 배분 가이드:
|
||||
- 교육 콘텐츠: 40% (신규 팔로워 유입)
|
||||
- 브랜드 스토리: 30% (신뢰 구축)
|
||||
- 이벤트/프로모션: 30% (전환)
|
||||
```
|
||||
|
||||
#### Target Audience for Ads
|
||||
|
||||
| Audience | Age | Interests | Use For |
|
||||
|----------|-----|-----------|---------|
|
||||
| 핵심 타겟 | 25-45 | 뷰티, 성형, K-beauty | 시술 교육 |
|
||||
| 확장 타겟 | 35-55 | 동안, 안티에이징 | 리프팅 콘텐츠 |
|
||||
| 리타겟팅 | All | 웹사이트 방문자 | 전환 |
|
||||
|
||||
---
|
||||
|
||||
## Medical Advertising Compliance (의료광고법)
|
||||
|
||||
### Instagram Specific Rules
|
||||
|
||||
**허용**:
|
||||
- 시술 과정 일반 설명
|
||||
- 고지문 포함된 Before/After
|
||||
- 원장 프로필 및 자격
|
||||
- 병원 시설 소개
|
||||
|
||||
**금지**:
|
||||
- 효과 보장 문구 ("100% 만족", "반드시")
|
||||
- 가격 직접 노출
|
||||
- 환자 후기 직접 인용
|
||||
- 타 병원 비교
|
||||
- 고지문 없는 Before/After
|
||||
|
||||
### Required Disclaimers
|
||||
|
||||
```
|
||||
[Before/After 포스트 필수 고지]
|
||||
"본 게시물은 개인에 따라 결과가 다를 수 있으며,
|
||||
부작용(출혈, 감염, 염증 등)이 있을 수 있습니다.
|
||||
의료진과 충분한 상담 후 결정하시기 바랍니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Instagram MCP Tools Reference
|
||||
|
||||
### Expected MCP Functions
|
||||
|
||||
```
|
||||
// Account Analysis
|
||||
instagram.getAccountInsights(period: "30d")
|
||||
instagram.getFollowerDemographics()
|
||||
instagram.getTopPosts(limit: 10)
|
||||
|
||||
// Content Management
|
||||
instagram.getComments(postId: string)
|
||||
instagram.replyToComment(commentId: string, text: string)
|
||||
instagram.getDMs(filter: "unread")
|
||||
|
||||
// Publishing
|
||||
instagram.createPost(media: string, caption: string, hashtags: string[])
|
||||
instagram.schedulePost(media: string, caption: string, scheduledTime: Date)
|
||||
|
||||
// Analytics
|
||||
instagram.getPostInsights(postId: string)
|
||||
instagram.getReachAnalytics(period: "7d")
|
||||
```
|
||||
|
||||
### Fallback Without MCP
|
||||
|
||||
If Instagram MCP unavailable, request user to:
|
||||
1. Export Instagram Insights from app
|
||||
2. Copy-paste comments/DMs for reply suggestions
|
||||
3. Share screenshots for analysis
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 계정 상태 분석
|
||||
```
|
||||
"제이미 인스타그램 계정 상태 분석해줘"
|
||||
"이번 달 인스타그램 성과 리포트 만들어줘"
|
||||
```
|
||||
|
||||
### 댓글/DM 응대
|
||||
```
|
||||
"이 댓글에 어떻게 답변할까?" [댓글 내용]
|
||||
"DM 답변 초안 작성해줘"
|
||||
```
|
||||
|
||||
### 콘텐츠 기획
|
||||
```
|
||||
"다음 주 인스타그램 포스팅 계획 세워줘"
|
||||
"퀵매몰법 관련 릴스 아이디어 줘"
|
||||
"12월 콘텐츠 캘린더 만들어줘"
|
||||
```
|
||||
|
||||
### 부스트 전략
|
||||
```
|
||||
"이 포스트 부스트할지 판단해줘"
|
||||
"이번 달 광고 예산 배분 추천해줘"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brand Integration
|
||||
|
||||
This skill follows Jamie's brand guidelines from `40-jamie-brand-editor`:
|
||||
|
||||
| Element | Instagram Adaptation |
|
||||
|---------|---------------------|
|
||||
| 톤앤매너 | 해요체 60% + 습니다체 40% — 친근하지만 절제된 톤 |
|
||||
| 호칭 | "여러분" 권장 (자연스러운 톤) |
|
||||
| 주의 | "제이미 언니네" 같은 표현은 한정적으로만 사용 |
|
||||
| 슬로건 | "티안나게 수술하고, 티나게 예뻐지는" 활용 |
|
||||
| 핵심가치 | 자연스러움, 안전, 투명성 강조 |
|
||||
|
||||
**After generating Instagram content, review with `jamie-brand-guardian` for compliance.**
|
||||
|
||||
---
|
||||
|
||||
## Available Resources
|
||||
|
||||
```
|
||||
references/
|
||||
├── instagram_content_calendar_template.md # 월간 캘린더 템플릿
|
||||
├── hashtag_database.md # 승인된 해시태그 DB
|
||||
├── reply_templates.md # 댓글 응대 템플릿
|
||||
└── instagram_design_guide.md # 디자인 가이드라인 v2.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Version 2.0.0 | 2026-02-15 | Claude Desktop Skill*
|
||||
476
custom-skills/46-jamie-journal-editor/SKILL.md
Normal file
476
custom-skills/46-jamie-journal-editor/SKILL.md
Normal file
@@ -0,0 +1,476 @@
|
||||
---
|
||||
name: jamie-journal-editor
|
||||
description: |
|
||||
Jamie Clinic journal/blog content editor for "정기호의 성형외과 진료실 이야기" (journal.jamie.clinic).
|
||||
Creates educational medical blog posts in Dr. Jung's authentic voice with Korean medical ad compliance.
|
||||
Supports both full drafting (research → outline → article) and editing existing manuscripts.
|
||||
Triggers: Jamie journal, 제이미 저널, 진료실 이야기, journal blog, Jamie blog post, 블로그 콘텐츠.
|
||||
license: Internal-use Only
|
||||
---
|
||||
|
||||
# Jamie Journal Editor Skill
|
||||
|
||||
> **Purpose**: Create and edit educational blog content for Jamie Clinic's journal channel
|
||||
> **Channel**: "정기호의 성형외과 진료실 이야기" (https://journal.jamie.clinic)
|
||||
> **CMS**: Ghost (journal.jamie.clinic)
|
||||
> **Partner Skills**: `jamie-brand-audit` (compliance review), `jamie-brand-editor` (general branded content)
|
||||
|
||||
---
|
||||
|
||||
## Role Definition
|
||||
|
||||
| This Skill | Brand Editor (40) | Brand Audit (41) |
|
||||
|------------|-------------------|-------------------|
|
||||
| Journal/blog articles | All marketing content | Content review |
|
||||
| Educational, long-form | Multi-channel content | Compliance checking |
|
||||
| Dr. Jung's personal voice | Brand voice (general) | Feedback/corrections |
|
||||
|
||||
**This skill specializes in journal-style educational articles written from Dr. Jung's perspective.**
|
||||
|
||||
---
|
||||
|
||||
## Workflow Modes
|
||||
|
||||
### Mode A: Full Drafting (no manuscript)
|
||||
1. Receive topic/brief with target audience
|
||||
2. Deep research on the given topic
|
||||
3. Develop outline or memo for user approval
|
||||
4. Draft article using brand voice and content structure
|
||||
5. Generate SEO metadata, image specs, and schema data
|
||||
6. Run compliance review
|
||||
7. Submit to `jamie-brand-audit` for final review
|
||||
|
||||
### Mode B: Manuscript Editing (원고 편집)
|
||||
1. Receive manuscript (초안) from Dr. Jung
|
||||
2. Preserve original voice, analogies, and expressions
|
||||
3. Restructure for web readability and SEO
|
||||
4. Add SEO metadata, image specs, and schema data
|
||||
5. Run compliance review
|
||||
6. Submit to `jamie-brand-audit` for final review
|
||||
|
||||
---
|
||||
|
||||
## Brand Voice (Dr. Jung's Journal Voice)
|
||||
|
||||
### Brand Personality (5 Traits)
|
||||
|
||||
| Trait | Expression Example |
|
||||
|-------|-------------------|
|
||||
| Trustworthy Expert | "2008년부터 눈 성형을 전문적으로 시행하고 있고" |
|
||||
| Warm Explainer | "나무 옮겨 심는 거랑 똑같다고 하거든요" |
|
||||
| Honest Advisor | "100% 성공률을 가진 의사는 없어요" |
|
||||
| Patient-Centered | "환자분들이 말씀하시는 졸린 눈은..." |
|
||||
| Humble Confidence | "저희들이 시행하고 있습니다" |
|
||||
|
||||
### Sentence Ending Ratios
|
||||
|
||||
| Type | Ratio | Example |
|
||||
|------|-------|---------|
|
||||
| Formal (~습니다/~입니다) | 90% | "진행됩니다", "있습니다" |
|
||||
| Service (~드립니다) | 6% | "보장해 드립니다" |
|
||||
| Soft (~거든요/~해요) | 4% | "드물거든요" (Q&A only) |
|
||||
|
||||
### Honorific Guide
|
||||
|
||||
| Context | Honorific | Usage |
|
||||
|---------|-----------|-------|
|
||||
| Medical explanation | 환자분, 환자분들 | 61% |
|
||||
| Service guidance | 고객님 | 22% |
|
||||
| General address | 여러분 | 17% |
|
||||
|
||||
### Standard Opening
|
||||
```
|
||||
"안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Structure (Journal Article)
|
||||
|
||||
### Opening
|
||||
```markdown
|
||||
안녕하세요. 제이미성형외과 정기호 원장입니다.
|
||||
|
||||
오늘은 [타겟 고객/고민]을 위한 [시술명]에 대해 [말씀드리겠습니다/소개해 드리겠습니다].
|
||||
|
||||
[주제에 대한 일반적인 오해나 필요성 언급]
|
||||
```
|
||||
|
||||
> 시리즈 후속편의 경우 "지난 글에서~"로 대체 가능.
|
||||
|
||||
### Body (5-Step Structure)
|
||||
1. **Problem Statement** (Empathy) - Patient concerns/symptoms
|
||||
2. **Cause Explanation** (Education) - Why this problem occurs
|
||||
3. **Solution** (Jamie's Method) - Procedure introduction
|
||||
4. **Advantages** (Differentiation) - Recovery, scars, pain, anesthesia
|
||||
5. **Expected Results** (Vision) - Post-surgery outcomes
|
||||
|
||||
### Closing
|
||||
```markdown
|
||||
[Key bullet point summary]
|
||||
|
||||
[고민]이시라면 지금 바로 제이미성형외과의 [시술명] 상담을 추천드립니다.
|
||||
언제든지 편안한 마음으로 상담해 주시면 감사하겠습니다.
|
||||
```
|
||||
|
||||
### Required Disclaimer (#알립니다#)
|
||||
```
|
||||
※ 본 콘텐츠는 의학 정보 제공을 목적으로 작성되었으며,
|
||||
개인의 상태에 따라 적합한 치료 방법은 다를 수 있습니다.
|
||||
|
||||
※ 모든 수술 및 시술은 개인에 따라 결과가 다를 수 있으며,
|
||||
출혈, 감염, 붓기, 비대칭 등의 부작용이 발생할 수 있습니다.
|
||||
반드시 전문의와 충분한 상담 후 결정하시기 바랍니다.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO Metadata
|
||||
|
||||
각 포스트에 다음 SEO 메타데이터를 생성합니다:
|
||||
|
||||
| 항목 | 형식 |
|
||||
|------|------|
|
||||
| 메타 제목 | [주제] — [핵심키워드] \| 제이미성형외과 (60자 이내) |
|
||||
| 메타 디스크립션 | 120-160자, 핵심 키워드 포함, CTA 암시 |
|
||||
| Slug | 영문 소문자, 하이픈 구분, 핵심 키워드 3-5개 |
|
||||
| 태그 | 핵심 키워드 5-10개 (한글) |
|
||||
|
||||
---
|
||||
|
||||
## Analogy Dictionary (Dr. Jung's Signature Metaphors)
|
||||
|
||||
| Topic | Metaphor |
|
||||
|-------|----------|
|
||||
| Fat graft survival | "나무 옮겨 심는 거랑 똑같다고 하거든요. 한 번 옮겨 심은 나무는 그 자리에서 계속 자라는 거예요." |
|
||||
| 3-point fixation | "인형극 실 비유 - 실이 두 줄인 거랑 세 줄 네 줄인 거랑은 움직임의 자연스러움이 차이가 있겠죠" |
|
||||
| Revision surgery | "깨끗한 도화지에 그림을 그리면 화가의 실력이 100% 발휘가 될 텐데, 재수술은 낙서가 있는 도화지에 덧칠을 하는 것" |
|
||||
| Endotine | "똑딱이 단추와 같은 나사라고 생각하셔도 되겠습니다" |
|
||||
|
||||
---
|
||||
|
||||
## Medical Terminology Pattern
|
||||
|
||||
본문 첫 등장 시 다음 형식을 사용합니다:
|
||||
```
|
||||
안검하수(眼瞼下垂, Ptosis)는 눈을 뜨는 근육의 힘이 약해져 눈꺼풀이 처지는 현상을 말합니다.
|
||||
```
|
||||
|
||||
**기본 형식**: 국문(English) — 예: 안검외반(Ectropion)
|
||||
|
||||
| 위치 | 영문 병기 | 예시 |
|
||||
|------|----------|------|
|
||||
| 본문 텍스트 | 첫 등장 시 적용 | "연부조직(Soft Tissue)이 거상(Lifting)되고" |
|
||||
| 섹션 타이틀/H2 | 선택적 — 핵심 시술명에만 | "## 안검외반에 대하여" (영문 생략 가능) |
|
||||
| 그래픽 이미지 | 핵심 해부학 용어에만 | 도식 라벨에 주요 해부학 용어만 병기 |
|
||||
| Featured Image | 사용하지 않음 | 국문만 사용 — 예외 없음 |
|
||||
|
||||
---
|
||||
|
||||
## Procedure Copy Reference
|
||||
|
||||
### Eye Surgery
|
||||
| Procedure | Key Expression |
|
||||
|-----------|---------------|
|
||||
| Quick Burial | "티 안 나게 예뻐지는", "휴가를 내지 않고도" |
|
||||
| Hybrid Double Eyelid | "절개법과 매몰법의 장점만을 모은" |
|
||||
| Ptosis Correction | "졸리고 답답한 눈매를 또렷하고 시원하게" |
|
||||
| Under-eye Fat | "어둡고 칙칙한 눈밑을 환하게" |
|
||||
|
||||
### Forehead Surgery
|
||||
| Procedure | Key Expression |
|
||||
|-----------|---------------|
|
||||
| Endoscopic Forehead Lift | "3점 고정", "흡수성 봉합사 주문 제작" |
|
||||
| Endoscopic Brow Lift | "눈썹을 이상적인 위치로 리프팅" |
|
||||
| Sub-brow Excision | "티 안 나게 눈꺼풀 처짐을 개선" |
|
||||
|
||||
### Anti-aging
|
||||
| Procedure | Key Expression |
|
||||
|-----------|---------------|
|
||||
| SMAS Lifting | "표정 근막층부터 근본적으로" |
|
||||
| Fat Grafting | "반영구적 유지", "나무 옮겨 심는 것처럼" |
|
||||
|
||||
---
|
||||
|
||||
## Numeric Expression Guide
|
||||
|
||||
| Item | Expression |
|
||||
|------|-----------|
|
||||
| Surgery time | "10~15분", "1시간 정도", "4시간 정도" |
|
||||
| Recovery | "다음 날부터", "4~5일", "일주일 정도" |
|
||||
| AS period | "5년간 AS 보장" |
|
||||
| Management | "1년간 무료 리프팅 관리" |
|
||||
| Survival rate | "30% 정도, 많게는 40%까지" |
|
||||
| Duration | "5년 이상", "반영구적" |
|
||||
|
||||
---
|
||||
|
||||
## Do's & Don'ts
|
||||
|
||||
### Do's
|
||||
- Start with patient empathy: "~로 고민하시는 분들이 많습니다"
|
||||
- Use Dr. Jung's signature analogies
|
||||
- Include specific numbers: "5년간 AS 보장", "1시간 내외"
|
||||
- Set realistic expectations: "개선에 한계가 있을 수 있습니다"
|
||||
- 원고 편집 시 원문의 독특한 표현·비유·직설적 경고는 최대한 보존
|
||||
|
||||
### Don'ts
|
||||
| Prohibited | Replacement |
|
||||
|-----------|-------------|
|
||||
| "100% 성공" | "대부분의 경우 좋은 결과를 기대할 수 있습니다" |
|
||||
| "부작용 없음" | "부작용은 극히 드뭅니다" |
|
||||
| "반드시 좋아집니다" | "개선을 기대할 수 있겠습니다" |
|
||||
| "전문/전문병원" | "중점진료", "풍부한 경험" |
|
||||
| "완벽/최고/최상" | 사용 불가 |
|
||||
| "무통/완치/해결" | "개선", "지향" |
|
||||
| Competitor comparison | "저희만의 방법으로..." |
|
||||
| Casual tone | Standard formal speech |
|
||||
|
||||
---
|
||||
|
||||
## Image Specifications
|
||||
|
||||
### Visual DNA & Color System
|
||||
|
||||
모든 저널 그래픽의 3가지 원칙:
|
||||
1. **사실적 리얼리티** — Featured Image는 실사 모델 사진 기반. 삽화·AI 캐릭터 지양.
|
||||
2. **정보의 미니멀리즘** — 필요한 텍스트만 배치, 여백으로 신뢰감 표현.
|
||||
3. **의학적 정확성** — 해부학 도식은 실제 수술 부위 기준으로 정확히 표현.
|
||||
|
||||
**컬러 팔레트**:
|
||||
|
||||
| 역할 | HEX | 용도 |
|
||||
|------|-----|------|
|
||||
| Background | `#E0E5EB` | 모든 이미지 배경 — flat only, 그라디언트 금지 |
|
||||
| Primary Text | `#333333` | 본문 라벨, 제목, 설명 |
|
||||
| Accent / Arrow | `#6B8FAF` | 화살표, 활성 노드, 강조 선 |
|
||||
| Secondary | `#8B9BA8` | 보조 라벨, 비활성 단계, 윤곽선 |
|
||||
| Jamie Main Green | `#6d7856` | JAMIE 텍스트 로고 전용 |
|
||||
|
||||
### Image Placement (본문 내 위치)
|
||||
|
||||
> Featured Image는 Ghost Post Settings에서 설정하며, 본문에 삽입하지 않습니다. 본문 이미지는 2번부터 시작합니다.
|
||||
|
||||
| Position | Placement | Content |
|
||||
|----------|-----------|---------|
|
||||
| Featured | Ghost Post Settings (본문 밖) | 실사 모델 + 국문 카피 (1200×675px, 16:9) |
|
||||
| 2 | After definition/cause | 해부학 도식 (1200×600px) |
|
||||
| 3 | After procedure explanation | 프로세스 인포그래픽 |
|
||||
| 4 | After advantages/effects | 비교 차트 또는 요약 |
|
||||
| 5 | Before closing (optional) | CTA 이미지 |
|
||||
|
||||
### Featured Image Rules
|
||||
|
||||
| 규칙 | 상세 |
|
||||
|------|------|
|
||||
| 실사 모델 필수 | 자연스러운 메이크업, 정면 또는 3/4 측면, 균일한 조명 |
|
||||
| 카피 규칙 | 포스트 제목 그대로 사용 금지. 핵심 정보를 전달하는 표현으로 작성 |
|
||||
| 영문 절대 금지 | 국문만 사용 — 예외 없음 |
|
||||
| JAMIE 워터마크 | 이미지 생성 앱의 자체 옵션으로 처리 (우하단, `#6d7856`, 반투명) |
|
||||
|
||||
**카피라이팅 예시**:
|
||||
|
||||
| 포스트 주제 | Featured Image 카피 |
|
||||
|------------|-------------------|
|
||||
| 앞광대리프팅 회복과정과 부작용 | "앞광대 리프팅, 회복 과정과 주의할 부작용" |
|
||||
| 내시경 이마거상술의 원리 | "내시경 이마거상술, 처진 이마와 눈썹을 올리는 원리" |
|
||||
| 눈 수술 다음날 세안 | "눈 수술 후 세안, 언제부터 어떻게 해야 할까" |
|
||||
|
||||
### Image File Naming
|
||||
|
||||
형식: `[topic]-[detail]-[type]-[number].png`
|
||||
|
||||
| Type keyword | 의미 |
|
||||
|-------------|------|
|
||||
| `featured` | 대표 이미지 |
|
||||
| `recovery-timeline` | 회복 타임라인 |
|
||||
| `comparison` | 비교 도식 |
|
||||
| `anatomy` | 해부학 도식 |
|
||||
| `infographic` | 요약 인포그래픽 |
|
||||
| `flowchart` | 플로우차트 |
|
||||
|
||||
### Prohibited Elements (모든 이미지 공통)
|
||||
|
||||
- 실제 수술 장면 (혈액, 절개 과정)
|
||||
- "Before" / "After" 라벨 직접 표기한 전후 비교 사진
|
||||
- 수술 도구의 사실적 사진 (간결한 일러스트는 허용)
|
||||
- 3D 효과, 그림자, 그라디언트 배경
|
||||
- 장식적 테두리, 프레임, 스톡 클립아트, 이모지
|
||||
- 경쟁 병원, 타 브랜드 노출
|
||||
|
||||
---
|
||||
|
||||
## Gemini Image Prompt Templates
|
||||
|
||||
각 이미지 구성안 아래에 Gemini 프롬프트를 코드 스니펫으로 제시합니다.
|
||||
|
||||
**작성 원칙**:
|
||||
- `[PRIMARY]`와 `[SECONDARY]` 블록으로 시각적 위계 구조화
|
||||
- 컬러 코드는 HEX로 명시
|
||||
- 의료 고지문은 프롬프트에 포함하지 않음
|
||||
- 구성 의도가 불명확할 때 임의 해석하지 말고 사용자에게 확인
|
||||
|
||||
### Template 1 — Featured Image
|
||||
```
|
||||
Generate a realistic photograph of an Asian woman [나이대 및 표정 묘사].
|
||||
Studio background: flat light blue-gray (#E0E5EB). Natural lighting, soft left-side.
|
||||
|
||||
[PRIMARY — must be visually dominant]
|
||||
Korean text overlay: "[featured image 카피 — 국문만]"
|
||||
Font: bold Korean gothic, color #333333, lower-third or center-left alignment
|
||||
|
||||
[SECONDARY — subtle supporting]
|
||||
Clean negative space for text breathing room
|
||||
|
||||
Aspect ratio: 16:9, size: 1200x675px
|
||||
Do NOT include: English copy text, decorative borders, gradients
|
||||
```
|
||||
|
||||
### Template 2 — Timeline (회복 과정)
|
||||
```
|
||||
Create a clean medical timeline infographic.
|
||||
Background: flat #E0E5EB, no gradients, no texture.
|
||||
|
||||
[PRIMARY — visually dominant]
|
||||
Horizontal timeline with [N] milestone nodes:
|
||||
- [Day 0] "[라벨]" (node color #6B8FAF, active)
|
||||
- [Day N] "[라벨]" (node color #8B9BA8, transitional)
|
||||
Connecting line: solid #6B8FAF
|
||||
|
||||
[SECONDARY — supporting]
|
||||
Below each node: one-line Korean description, #333333, clean gothic font
|
||||
|
||||
Size: 1200x600px
|
||||
Do NOT include: photographs, 3D effects, decorative borders, unnecessary English labels
|
||||
```
|
||||
|
||||
### Template 3 — Comparison (비교 도식)
|
||||
```
|
||||
Create a clean medical comparison diagram.
|
||||
Background: flat #E0E5EB, no gradients.
|
||||
|
||||
[PRIMARY — visually dominant]
|
||||
Two-column comparison layout:
|
||||
Left: "[옵션 A]" — [주요 특징들]
|
||||
Right: "[옵션 B]" — [주요 특징들]
|
||||
Dividing element: thin line, color #8B9BA8
|
||||
|
||||
[SECONDARY — supporting]
|
||||
Section headers: bold #333333, Accent arrows: #6B8FAF
|
||||
|
||||
Size: 1200x600px
|
||||
Do NOT include: 3D effects, stock clip-art, English labels (except key medical terms in parentheses)
|
||||
```
|
||||
|
||||
### Template 4 — Anatomy (해부학 도식)
|
||||
```
|
||||
Create a clean medical anatomy diagram in minimal line-art style.
|
||||
Background: flat #E0E5EB.
|
||||
|
||||
[PRIMARY — visually dominant]
|
||||
[해부학 구조 묘사 — 위치, 방향, 화살표]
|
||||
Directional arrows: #6B8FAF (bold, clear)
|
||||
Key anatomical labels in Korean with English in parentheses for primary terms only
|
||||
|
||||
[SECONDARY — supporting]
|
||||
Secondary labels: #8B9BA8
|
||||
Body outline or cross-section: thin lines, #8B9BA8
|
||||
|
||||
Size: 1200x600px
|
||||
Do NOT include: realistic tissue rendering, gore, graphic surgical scenes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema Data (Ghost CMS)
|
||||
|
||||
Ghost CMS는 Article Schema를 자동 생성하므로 **FAQPage + BreadcrumbList**만 수동 추가합니다.
|
||||
두 스키마를 하나의 `<script>` 태그 안에 `@graph` 배열로 통합합니다.
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "질문 1",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "답변 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{ "@type": "ListItem", "position": 1, "name": "정기호의 진료실 이야기", "item": "https://journal.jamie.clinic/" },
|
||||
{ "@type": "ListItem", "position": 2, "name": "[카테고리/태그명]", "item": "https://journal.jamie.clinic/tag/[태그슬러그]/" },
|
||||
{ "@type": "ListItem", "position": 3, "name": "[포스트 제목]", "item": "https://journal.jamie.clinic/[slug]/" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
> 삽입 위치: Ghost Post Settings → Code injection → **Post Header**
|
||||
|
||||
---
|
||||
|
||||
## Output Format — 3-파일 1세트
|
||||
|
||||
최종 출력물은 3개 파일을 1세트로 생성합니다:
|
||||
|
||||
### File 1: 종합 초안 (`jamie-journal_[topic-slug]_comprehensive-draft.md`)
|
||||
- SEO 메타데이터
|
||||
- 블로그 본문 초안 (마크다운, 이미지 위치 표시)
|
||||
- 이미지 사양 (각 이미지별 구성안 + Gemini 프롬프트)
|
||||
- Schema 데이터
|
||||
- 브랜드 보이스 / 의료광고 심의 체크리스트
|
||||
- 편집자 노트 (원문 편집 시)
|
||||
|
||||
### File 2: Ghost 본문 (`ghost_[topic-slug]_body.md`)
|
||||
Ghost 에디터에 바로 붙여넣기 가능한 클린 마크다운. Featured Image 자리표시 없음 (본문 밖). 이미지 2번부터 자리표시. #알립니다# 포함.
|
||||
|
||||
### File 3: Ghost Code Injection (`ghost_[topic-slug]_code-injection.html`)
|
||||
FAQPage + BreadcrumbList 통합 스크립트. 상단 HTML 주석으로 포스트 제목, slug, 삽입 위치 안내.
|
||||
|
||||
---
|
||||
|
||||
## Available Resources
|
||||
|
||||
```
|
||||
desktop/references/
|
||||
├── brand-voice.md # Dr. Jung's detailed voice guide
|
||||
├── content-patterns.md # Content structure patterns & examples
|
||||
└── medical-compliance.md # Medical ad compliance rules
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Blog Post (Full Drafting)
|
||||
```
|
||||
"내시경 이마거상술에 대한 저널 포스트 작성해줘.
|
||||
타겟: 30-50대 여성, 이마 주름과 눈썹 처짐 고민"
|
||||
```
|
||||
|
||||
### Manuscript Editing
|
||||
```
|
||||
"원장님 초안 편집해줘. 앞광대리프팅 회복과정과 부작용 주제.
|
||||
원문 톤은 최대한 살려줘."
|
||||
```
|
||||
|
||||
### Educational Article
|
||||
```
|
||||
"안검하수 눈매교정에 대한 교육적 콘텐츠 작성.
|
||||
환자 고민 중심으로 정기호 원장 말투로."
|
||||
```
|
||||
|
||||
**After generating, use `jamie-brand-audit` to review compliance.**
|
||||
182
custom-skills/47-jamie-marketing-editor/SKILL.md
Normal file
182
custom-skills/47-jamie-marketing-editor/SKILL.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: jamie-marketing-editor
|
||||
description: |
|
||||
Jamie Clinic marketing content editor for digital channels, ads, communications, and internal docs.
|
||||
Creates compliant marketing copy for website, blog, SNS, ads, and patient communications.
|
||||
Triggers: Jamie marketing, 제이미 마케팅, ad copy, Jamie ads, 광고 카피, SNS 콘텐츠, 마케팅 콘텐츠.
|
||||
license: Internal-use Only
|
||||
---
|
||||
|
||||
# Jamie Marketing Editor Skill
|
||||
|
||||
> **Purpose**: Create and edit marketing content across all Jamie Clinic digital channels
|
||||
> **Scope**: Website, blog, SNS, ads, email, KakaoTalk, patient communications
|
||||
> **Partner Skills**: `jamie-brand-editor` (general branded content), `jamie-brand-audit` (compliance review)
|
||||
|
||||
---
|
||||
|
||||
## Role Definition
|
||||
|
||||
| This Skill (47) | Brand Editor (40) | Journal Editor (46) |
|
||||
|------------------|-------------------|---------------------|
|
||||
| Multi-channel marketing | General branded content | Journal/blog articles |
|
||||
| Ad copy, SNS, email | Blog posts, procedure pages | Educational long-form |
|
||||
| Channel-optimized tone | Brand voice (standard) | Dr. Jung's personal voice |
|
||||
|
||||
**This skill focuses on marketing-optimized content across all digital channels.**
|
||||
|
||||
---
|
||||
|
||||
## Core Brand Pillars
|
||||
|
||||
| Pillar | Description |
|
||||
|--------|-------------|
|
||||
| **Safety** | Patient safety first: certified facilities, thorough evaluation |
|
||||
| **Naturalness** | Natural-looking results that enhance, not transform |
|
||||
| **Transparency** | Honest consultations, realistic expectations |
|
||||
| **Quality Assurance** | 5-year AS coverage, 1-year monitoring |
|
||||
|
||||
---
|
||||
|
||||
## Communication Style
|
||||
|
||||
**Tone**: Professional medical authority + approachable family-like warmth
|
||||
**Character**: "답정남" (decisive gentleman) — clear, logical, honest assessments
|
||||
**Trust Markers**: 5-year AS, director's personal care, 1:1 recovery rooms, honest recommendations
|
||||
|
||||
### Key Differentiators to Emphasize
|
||||
- Director personally performs all surgeries and post-op care
|
||||
- 3-point fixation endoscopic forehead lift (proprietary)
|
||||
- Revision surgery expertise
|
||||
- One-person recovery rooms
|
||||
- Honest recommendations (declines unsuitable cases)
|
||||
|
||||
---
|
||||
|
||||
## Channel-Specific Guidelines
|
||||
|
||||
### Website
|
||||
- **Tone**: Professional, educational, trust-building
|
||||
- **Structure**: Clear information hierarchy, thorough explanations
|
||||
- **Template**: Page header (25 chars) > Procedure name (12 chars) > Introduction > Jamie's Distinction > Benefits > Candidates > Info table > FAQ > Glossary > Post-care > CTA > Disclaimer
|
||||
|
||||
### Blog (Naver/Website)
|
||||
- **Tone**: Educational, accessible, friendly
|
||||
- **Focus**: SEO-optimized, keyword-rich, step-by-step explanations
|
||||
- No patient testimonials or experience stories
|
||||
|
||||
### Instagram
|
||||
- **Tone**: Sensory, concise, trendy
|
||||
- **Format**: Core message + relevant hashtags (no superlatives in tags)
|
||||
- Avoid before/after gallery-style comparisons
|
||||
|
||||
### YouTube
|
||||
- **Tone**: Professional yet easy to understand
|
||||
- **Format**: Step-by-step explanations with visual aids
|
||||
- No patient interviews or dramatic before/after videos
|
||||
|
||||
### KakaoTalk Channel
|
||||
- **Tone**: Friendly yet professional, helpful
|
||||
- **Format**: Brief info delivery, action-oriented
|
||||
- Quick consultation booking guidance
|
||||
|
||||
### Search Ads (Naver/Google/Meta)
|
||||
- No exaggerated claims or effect guarantees
|
||||
- No comparative superiority ("최고", "1위")
|
||||
- Allowed: "○○ 성형외과 - 전문의 상담", "압구정 위치 - 예약 문의"
|
||||
|
||||
---
|
||||
|
||||
## Content Structure (Procedure Page Template)
|
||||
|
||||
1. **Page Header**: Lead-in (25 chars) + Procedure name (12 chars) + Headline
|
||||
2. **Introduction**: What the procedure is, common patient concerns
|
||||
3. **Jamie's Distinction**: Unique approach, techniques, expertise
|
||||
4. **Expected Benefits**: Aesthetic and functional improvements
|
||||
5. **Recommended Candidates**: Specific patient profiles
|
||||
6. **Procedure Info Table**: Anesthesia, duration, visits, suture removal, recovery, pain level
|
||||
7. **FAQ Section**: Common questions from consultation data
|
||||
8. **Technical Glossary**: Medical terms in accessible language
|
||||
9. **Post-Surgery Care & AS**: Aftercare programs, warranty
|
||||
10. **CTA**: Consultation booking guidance
|
||||
11. **Compliance Disclaimer**: Required side-effect disclosure
|
||||
|
||||
---
|
||||
|
||||
## Customer Segment Messaging
|
||||
|
||||
| Segment | Strategy |
|
||||
|---------|----------|
|
||||
| First-time patients | Emphasize safety, expertise, 5-year AS, educational content |
|
||||
| Revision surgery | Empathy, problem-solving ability, detailed evaluation process |
|
||||
| Referred patients | Maintain trust, personalized care, gratitude |
|
||||
|
||||
---
|
||||
|
||||
## Compliance Rules (의료법 제56조)
|
||||
|
||||
### Prohibited
|
||||
- Patient testimonials or treatment experience stories
|
||||
- Before/after photos without required disclaimers
|
||||
- Effect guarantees ("100% 만족", "완벽한 결과", "보장")
|
||||
- Comparative claims ("최고", "1위", "타병원보다")
|
||||
- Safety guarantees ("부작용 없음", "완전 안전")
|
||||
- Unverified statistics or certifications
|
||||
|
||||
### Required
|
||||
- Individual variation disclosure: "결과는 개인에 따라 다를 수 있습니다"
|
||||
- Side-effect notice: "붓기, 멍, 염증 등의 부작용이 발생할 수 있습니다"
|
||||
- Consultation recommendation: "반드시 전문의와 상담 후 결정하시기 바랍니다"
|
||||
|
||||
### Expression Substitutions
|
||||
| Prohibited | Compliant Alternative |
|
||||
|-----------|----------------------|
|
||||
| "완벽한 결과를 보장" | "개인에 따라 결과가 다를 수 있습니다" |
|
||||
| "반드시 개선됩니다" | "개선에 도움을 줄 수 있습니다" |
|
||||
| "압구정 최고" | "압구정에 위치한" |
|
||||
| "타병원보다 우수한" | "독자적인 기법을 사용하는" |
|
||||
|
||||
---
|
||||
|
||||
## SEO with Compliance
|
||||
|
||||
- Integrate keywords naturally without violating ad prohibitions
|
||||
- Apply schema markup for medical procedures and local business
|
||||
- Optimize for Apgujeong/Gangnam location-based searches
|
||||
- Sufficient information density without exaggeration
|
||||
|
||||
---
|
||||
|
||||
## Available Resources
|
||||
|
||||
```
|
||||
desktop/
|
||||
├── brand_guidelines/
|
||||
│ └── brand_voice_guide_korean.md # Korean brand voice guide
|
||||
└── regulations/
|
||||
└── medical_advertising_law_summary_korean.md # Medical ad law summary
|
||||
|
||||
code/scripts/
|
||||
└── compliance_checker.py # Automated compliance scanning
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Procedure Page
|
||||
```
|
||||
"내시경 이마거상술 시술 소개 페이지 작성. 타겟: 30-50대 여성."
|
||||
```
|
||||
|
||||
### Instagram Series
|
||||
```
|
||||
"자연스러운 눈 성형 관련 인스타그램 5개 시리즈 작성"
|
||||
```
|
||||
|
||||
### Ad Copy
|
||||
```
|
||||
"퀵매몰법 네이버 검색 광고 카피 3개 버전 작성"
|
||||
```
|
||||
|
||||
**After generating, use `jamie-brand-audit` to review compliance.**
|
||||
100
custom-skills/48-jamie-copy-trimmer/SKILL.md
Normal file
100
custom-skills/48-jamie-copy-trimmer/SKILL.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: jamie-copy-trimmer
|
||||
description: >-
|
||||
Trims and sharpens Korean plastic-surgery / aesthetic marketing copy — headlines, body,
|
||||
CTAs, names, slogans. Removes clichés and medical-ad compliance risks, suggests trendy,
|
||||
catchy alternatives within 의료광고 심의 limits, and re-scores them. Use when copy feels
|
||||
awkward or old, or to refine, name, or compliance-check any copy. Triggers: 카피 다듬어,
|
||||
카피 트리밍, 네이밍 검토, 슬로건 다듬어, 심의 안전하게, copy trim, make it catchier, 제이미 카피.
|
||||
For Jamie work, use with jamie-brand-guardian.
|
||||
license: Proprietary (OurDigital internal)
|
||||
---
|
||||
|
||||
# Jamie Copy Trimmer
|
||||
|
||||
Trim and sharpen Korean plastic-surgery / aesthetic-medical marketing copy against an industry expression corpus, within medical-advertising limits.
|
||||
|
||||
**Working language.** These directives are in English, but the copy you evaluate and produce is Korean — the language of the target market. Write all deliverable copy, alternatives, and the final report in Korean unless the user asks otherwise.
|
||||
|
||||
## Core philosophy (internalize this)
|
||||
|
||||
1. **The corpus is a map for AVOIDING clichés, not a library to COPY.** Frequently used means probably already stale. Use it to spot what everyone says and go elsewhere. Copying corpus phrases defeats the purpose, which is differentiation.
|
||||
2. **The ceiling on wit is set by 의료광고 심의.** Korean plastic-surgery copy is constrained by rules against exaggeration, superlatives, and patient inducement. Raise the appeal, but anything past the compliance line is void. Compliance is a **gate, not a score** — if a single 🔴 risk expression remains, that option fails outright.
|
||||
3. **Trim first, dazzle second.** Cutting redundancy, clichés, and risk is the primary job. Add restrained flair into the space you cleared. "Say less, land harder."
|
||||
4. **Don't guess — mark [확인].** If procedure facts, target, channel, or brand tone are missing, ask the user or leave a `[확인]` note and move on. Never fill gaps with invention.
|
||||
|
||||
## Before you start (inputs)
|
||||
|
||||
Ask, or mark `[확인]`, if any of these are missing:
|
||||
- **Copy text** and its type (headline / body / CTA / name / slogan)
|
||||
- **Channel** (카카오플러스친구 / Instagram / blog / Naver / in-clinic POP / search ad) — tone and 심의 strictness differ by channel
|
||||
- **Procedure & target** (standard procedure name, patient concern)
|
||||
- **Brand tone** — for a specific brand (e.g., Jamie), that brand's guide (`jamie-brand-guardian`) overrides this skill's taste defaults
|
||||
|
||||
## Workflow (5 steps)
|
||||
|
||||
### 1) Diagnose — tag the original
|
||||
Tag each phrase/expression. When unsure, read the matching corpus file.
|
||||
|
||||
| Tag | Meaning | Action |
|
||||
|-----|---------|--------|
|
||||
| 🟢 effective | Works, resonates | Keep |
|
||||
| 🟡 cliché | Industry-stale, tired | Trim / replace (→ `corpus_cliche.md`) |
|
||||
| 🔴 compliance risk | Exaggeration, superlative, inducement, comparison | Must remove/replace (→ `corpus_compliance_risk.md`) |
|
||||
| ⚪ flat | Not wrong, but no hook | Sharpen (→ `witty_within_limits.md`) |
|
||||
| 🟦 brand asset | Brand-owned phrase | Do not misjudge as cliché (e.g., Jamie's "티 안 나게") |
|
||||
|
||||
### 2) Trim
|
||||
Remove all 🔴, replace 🟡, delete redundancy. Output of this step is a "safe, plain" version.
|
||||
|
||||
### 3) Elevate — add appeal within 심의 limits
|
||||
Using techniques in `witty_within_limits.md`, give **2–3 alternatives per element**, each with a rationale (why it lands harder), matched to the channel's tone. Consult `corpus_examples.md` for technique coordinates — never copy the examples verbatim.
|
||||
|
||||
### 4) Re-score — 5-axis rubric
|
||||
Score original vs. improved with `evaluation_rubric.md`. 심의 is a PASS/FAIL gate. In the brand-fit axis, state in one line **which brand attribute / association / asset** the copy strengthens (keeps qualitative judgment concrete rather than vague).
|
||||
|
||||
### 5) Recursive Improvement — evolve the corpus
|
||||
Per `recursive_protocol.md` (Recursive Improvement Protocol), propose feeding this session's adopted/rejected expressions back into the corpus. Demote once-effective phrases that have become common to the cliché list.
|
||||
|
||||
## Output format (produce the report in Korean, using this structure)
|
||||
|
||||
```
|
||||
## 카피 트리밍 결과 — [대상/채널]
|
||||
|
||||
### 1) 진단
|
||||
- "원문 문구" → 🔴/🟡/⚪/🟢/🟦 사유
|
||||
|
||||
### 2) 트리밍 (담백한 안)
|
||||
[군더더기·위험 제거 버전]
|
||||
|
||||
### 3) 대안 (심의 한도 내 감각안)
|
||||
| 요소 | 원문 | 문제 | 개선안 2~3개 | 근거 |
|
||||
|
||||
### 4) 재평가 (5축, 1~5점 · 심의=게이트)
|
||||
| 축 | 원문 | 추천안 | 코멘트 |
|
||||
| 감각 / 차별성 / 브랜드적합성 / 심의(P·F) / 명료성 |
|
||||
→ 강화되는 브랜드 자산: [속성/연상 명시]
|
||||
|
||||
### 5) 추천안 + 이유
|
||||
|
||||
### 6) 준비 점검 사항
|
||||
- [확인] 심의 필요 / 정보 부재 / 사실 검증 필요 항목을 여기 모아 정리
|
||||
```
|
||||
|
||||
Keep risk items and open questions out of the body; collect them at the end under **준비 점검 사항** (brevity principle).
|
||||
|
||||
## Reference files (read as needed)
|
||||
|
||||
- `references/corpus_compliance_risk.md` — medical-ad risk expressions + safe replacements (**most important**)
|
||||
- `references/corpus_cliche.md` — stale/old expressions to avoid (avoidance coordinates)
|
||||
- `references/corpus_effective.md` — working patterns (application coordinates; don't copy verbatim)
|
||||
- `references/corpus_examples.md` — analyzed real-world exemplar copy (technique coordinates; no imitation)
|
||||
- `references/witty_within_limits.md` — making copy trendy/catchy inside 심의 limits, and the boundaries
|
||||
- `references/evaluation_rubric.md` — the 5-axis re-scoring rubric and gate rules
|
||||
- `references/recursive_protocol.md` — Recursive Improvement Protocol and corpus tagging schema
|
||||
|
||||
## Reminders
|
||||
|
||||
- Compliance judgment here is guidance, not legal advice. Always leave a `[확인]` recommending pre-publication 의료광고 자율심의.
|
||||
- For a specific brand, that brand's tone guide wins over this skill's defaults.
|
||||
- The corpus is a living document — even effective phrases go stale. Refresh it periodically via `recursive_protocol.md`.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Cliché / Stale Expressions (🟡) — Avoidance Coordinates
|
||||
|
||||
> **This list is for avoiding, not copying.** Frequently used = already tired.
|
||||
> If a listed expression shows up in the copy, consider replacing it. The "refresh direction" is a lead for finding an alternative, not a ready-made line.
|
||||
|
||||
## A. Beauty/result show-off (stale)
|
||||
| Cliché (Korean) | Why it's old | Refresh direction |
|
||||
|-----------------|--------------|-------------------|
|
||||
| 여신, 인형 같은, 리즈 갱신, 인생 리즈 | Ranks looks, unrealistic, 10-years-ago tone | Replace with a concrete everyday-change scene |
|
||||
| 물광, 꿀광, 동안 미모 | Spent beauty buzzwords | Use the customer's concern language, not glow words |
|
||||
| 확 달라진, 몰라보게, 환골탈태 | Exaggeration; borders on 심의 risk | Restrain and specify the degree of change |
|
||||
|
||||
## B. Self-flattering filler (empty)
|
||||
| Cliché | Why it's old | Refresh direction |
|
||||
|--------|--------------|-------------------|
|
||||
| 특별한 당신을 위한, 당신만의 | Said to everyone = says nothing | Name a truly specific concern/situation |
|
||||
| 아름다움을 완성하다, 당신의 아름다움 | Abstract, boilerplate | Make it concrete with verbs/scenes |
|
||||
| 프리미엄, 명품, 하이엔드 | Unbacked elevation words | Replace with facts (technique, track record) |
|
||||
|
||||
## C. Pressure/urgency (also inducement risk)
|
||||
| Cliché | Why it's old | Refresh direction |
|
||||
|--------|--------------|-------------------|
|
||||
| 지금 바로, 서두르세요, 놓치지 마세요 | Pressure cliché, lowers trust | Informational CTA ("편하게 문의해 주세요") |
|
||||
| 마감 임박, 선착순, 단 O명 | Inducement/pressure, 심의 risk | Keep any time-limited notice plain, strip inducement |
|
||||
| 후회 없는 선택, 결심만 하세요 | Decision-coercion tone | Leave the decision with the customer |
|
||||
|
||||
## D. Region/class clichés
|
||||
| Cliché | Why it's old | Refresh direction |
|
||||
|--------|--------------|-------------------|
|
||||
| 강남 언니, 청담동 스타일, 강남 미인 | Regional cliché, fatigue | Replace region with specific result/experience |
|
||||
|
||||
## E. Over-used "safe" words (the irony)
|
||||
| Expression | Caution | Refresh direction |
|
||||
|------------|---------|-------------------|
|
||||
| 자연스러운 / 자연스럽게 | The whole industry overuses it → loses distinctiveness | Keep the concept, vary the expression (scene/metaphor). ※ If a brand owns it as a slogan, treat as 🟦 |
|
||||
| 1:1 맞춤, 나만의 디자인 | Now a standard phrase | Make the "what" of the customization concrete |
|
||||
|
||||
## F. Form habits (hurt readability)
|
||||
- Exclamation spam (!!!), emoji overload, question-mark spam → one breath per sentence, restrained emoji
|
||||
- All-caps / all-bold → emphasize in one place only
|
||||
|
||||
---
|
||||
*Update rule: when you spot a newly-stale expression, add it here per `recursive_protocol.md`, and demote over-common "effective" phrases into this file.*
|
||||
@@ -0,0 +1,57 @@
|
||||
# Compliance Risk Dictionary (🔴) + Safe Replacements
|
||||
|
||||
> Based on Korean medical law and 의료광고 심의 (medical-ad review) standards. **This is not legal advice** — recommend pre-publication self-regulatory review. If any 🔴 remains, the option FAILS the rubric gate.
|
||||
|
||||
## Contents
|
||||
1. Absolute / superlative expressions
|
||||
2. Effect-guarantee / definitive expressions
|
||||
3. Patient-inducement expressions
|
||||
4. Comparative / exclusivity expressions
|
||||
5. Testimonials / reviews / before-after photos
|
||||
6. Neologisms / non-standard procedure names
|
||||
7. Missing mandatory disclosures
|
||||
8. Safe-replacement technique
|
||||
|
||||
---
|
||||
|
||||
## 1. Absolute / superlative (prohibited)
|
||||
Overstating or misleading beyond objective fact is prohibited.
|
||||
- Risk terms: `100%`, `완벽`, `완전`, `전혀`, `모든`, `유일`, `최고`, `국내 1위`, `NO.1`, `확실히`, `반드시`, `무조건` (and English `best`, `only`, `perfect`)
|
||||
- Replace with: "대부분의 경우", "개인에 따라 차이가 있습니다", "~를 기대할 수 있습니다"; use numbers only with a cited basis.
|
||||
|
||||
## 2. Effect-guarantee / definitive (prohibited / caution)
|
||||
- Risk terms: `부작용 없는`, `안전한` (as an absolute), `반영구`, `평생`, `재발 없음`, `100% 자연스러움`, `반드시 예뻐지는`
|
||||
- Replace with: "부작용은 극히 드뭅니다", "오래 유지되는 편입니다", "자연스러운 결과를 기대할 수 있습니다" + **attach a side-effect disclosure**.
|
||||
|
||||
## 3. Patient inducement (Medical Act Art. 27(3) risk)
|
||||
Offering economic benefit to induce/solicit patients carries strong illegality risk.
|
||||
- Risk terms: `무료`, `공짜`, `1+1`, `이벤트가`, `할인 이벤트`, `선착순 할인`, `○○원` (price front-loaded as a lure), `후기 작성 시 할인/적립/사은품`, `친구 소개하면`
|
||||
- Replace with: quote price only as "상담 시 안내"; keep any discount **decoupled** from reviews/referrals as a plain time-limited notice; request reviews only voluntarily with no compensation.
|
||||
- Note: even phrasing like "얼마일까" has been flagged as inducement.
|
||||
|
||||
## 4. Comparative / exclusivity (prohibited)
|
||||
- Risk terms: `타 병원보다`, `다른 곳과 다르게` (disparaging tone), `국내 유일`, `업계 최초` (unverified), any direct/indirect disparagement of competitors
|
||||
- Replace with: "저희만의 방법으로"; state own strengths factually (uniqueness instead of comparison).
|
||||
|
||||
## 5. Testimonials / reviews / before-after photos (review-subject, caution)
|
||||
- Risk: using patient reviews or before/after photos in ads; implying effect via testimonial
|
||||
- Handle: advertising use is subject to 심의; before/after must meet rules (same conditions, no misleading) → `[확인]` for review.
|
||||
|
||||
## 6. Neologisms / non-standard procedure names (caution)
|
||||
Neologisms and gimmicky phrasing are a top cause of 심의 rejection.
|
||||
- Risk: unverified proprietary procedure names, trendy slang, jargon
|
||||
- Replace with: standard procedure names first; pair a brand procedure name with its standard name.
|
||||
|
||||
## 7. Missing mandatory disclosures (formal requirement)
|
||||
- Required: **advertiser (clinic name)**, **side-effect / caution disclosure**, and on large online platforms the **review-approval number and validity period**
|
||||
- Missing these risks unreviewed-ad / labeling violations → confirm with a checklist.
|
||||
|
||||
## 8. Safe-replacement technique
|
||||
- Absolute → qualified ("대부분", "개인차")
|
||||
- Guarantee → expectation ("기대할 수 있습니다")
|
||||
- Inducement → information ("상담 시 안내")
|
||||
- Comparison → uniqueness ("저희만의")
|
||||
- Always pair with a side-effect disclosure and the advertiser name.
|
||||
|
||||
---
|
||||
*Sources: 대한의사협회 의료광고 심의기준; 의료광고 심의 금지 문구 안내; 강남언니 광고 가이드; related legal columns. Confirm the latest standard with the self-regulatory body.*
|
||||
@@ -0,0 +1,41 @@
|
||||
# Effective Patterns (🟢) — Application Coordinates
|
||||
|
||||
> **These are patterns, not finished lines.** Adapt the structure to the situation; pasting an example verbatim quickly turns it into a cliché.
|
||||
|
||||
## 1. Customer's own words (mirror the concern)
|
||||
Naming the concern in the words a customer actually uses beats abstract praise.
|
||||
- Pattern: [specific moment] + [discomfort]
|
||||
- Ex: "웃을 때 잡히는 이마 주름", "눈뜨는 게 무거운 아침", "화장이 자꾸 접히는 눈가"
|
||||
- Why: people who recognize themselves stop and read.
|
||||
|
||||
## 2. Scene & verb driven (restrain adjectives)
|
||||
A scene or verb that reveals the result beats an adjective like "아름다운."
|
||||
- Ex: "거울 앞에 서는 시간이 짧아졌다", "사진 찍을 때 앞줄에 선다"
|
||||
- Why: show, don't tell.
|
||||
|
||||
## 3. Restrained numbers & facts (trust)
|
||||
Verifiable facts are 심의-safe and persuasive, unlike exaggeration.
|
||||
- Ex: "2008년부터", "회복 3일", "제가 직접 집도합니다"
|
||||
- Caution: do not slide into superlatives/guarantees.
|
||||
|
||||
## 4. Honesty/humility as differentiation
|
||||
When the category is full of hype, candor stands out.
|
||||
- Ex: "안 되는 것도 말씀드립니다", "개선에 한계가 있을 수 있습니다"
|
||||
- Why: trust is the hook.
|
||||
|
||||
## 5. Question hook (not misleading)
|
||||
- Ex: "표정 주름, 한 번으로 될까요?", "왜 4개월마다일까요?"
|
||||
- Caution: 심의 risk if the answer becomes an exaggeration/guarantee.
|
||||
|
||||
## 6. Rhythm / parallelism
|
||||
- Ex (brand-asset example): "티 안 나게 수술하고, 티 나게 예뻐지는" — parallel/rhyme sticks in memory.
|
||||
- Why: it must read cleanly aloud, with no stumble.
|
||||
|
||||
## 7. Channel-fit tone
|
||||
- 카플친 / 알림톡: warm, thank-you tone, short
|
||||
- Instagram: first-line hook, sensory and concise
|
||||
- Blog / homepage: educational, evidence-led
|
||||
- In-clinic POP: one line understood at a glance + a support line
|
||||
|
||||
---
|
||||
*These coordinates only tell you which direction to aim. The actual line must be forged fresh each time.*
|
||||
@@ -0,0 +1,100 @@
|
||||
# Analyzed Exemplar Copy — Technique Coordinates (No Imitation)
|
||||
|
||||
> A distilled bank of **real** Korean (and a few global) plastic-surgery / aesthetic copy examples, curated from web research and filtered by this skill's rubric (fresh · compliance-safe · catchy).
|
||||
> **Use these to learn the technique and then write something new.** Do not paste them into client copy — several are brand-owned, and copying defeats differentiation.
|
||||
> Each entry: the line · the technique to steal · a compliance note.
|
||||
|
||||
## How to read this file
|
||||
Organized by **technique** (not by clinic), because the transferable asset is the technique. Compliance notes flag where a line would be risky if used directly in a Korean medical ad (superlatives, effect-guarantees, inducement, comparison). "🌐" marks a non-Korean example kept for inspiration only.
|
||||
|
||||
---
|
||||
|
||||
## 1. Metaphor + brand-name fusion
|
||||
Fuse the brand name with a metaphor so the name itself carries meaning. Highest-value, low-risk technique.
|
||||
- **"예쁨이 자란다, 나무성형외과"** — brand name (나무/tree) + growth metaphor + rhythm. Compliance: safe (metaphor, no guarantee).
|
||||
- **"I am Detailist" (바노바기)** — first-person identity claim ("detail = us"); differentiates on *attitude*, not effect. Safe.
|
||||
- **"진화하는 피부질환, 연구하는 피부주치의" (차앤박)** — clean parallelism + "주치의 (personal doctor)" metaphor = expertise + intimacy. Safe.
|
||||
- **"REWRITE YOUR STORY" (리쥬란)** — regeneration reframed as authoring your own story; strong emotion, no effect claim. Safe.
|
||||
- **Steal this:** let the brand/procedure name do double duty; anchor an abstract benefit to a concrete metaphor.
|
||||
|
||||
## 2. Contrarian reframe (flip the category default)
|
||||
Turn an industry assumption on its head.
|
||||
- **"Slow Banobagi / 느린 만큼 더 안전한 성형" (바노바기)** — flips "fast & flashy"; the slowness *is* the trust. Compliance: safe (but "더" leans comparative — keep it about self, not others).
|
||||
- **"줄였다는 느낌 말고, 맞춘 느낌!" (아이디병원, 콧볼)** — "not A, but B" contrast + rhythm; conveys philosophy without superlatives. Relatively safe.
|
||||
- **"누군가를 위해 예뻐지지 않아" (낫포유)** — value flip toward self-determination; fresh MZ tone. Safe.
|
||||
- **Steal this:** name the default the category shouts, then stake the opposite ground.
|
||||
|
||||
## 3. Everyday scene / customer language
|
||||
Make the need self-evident with a concrete life moment in the customer's own words.
|
||||
- **"오늘부로 보정 어플 삭제" (아이디병원, 윤곽)** — everyday detail (photo-retouch app) implies the result. Compliance: caution — it edges toward a result claim; soften.
|
||||
- **"무더운 여름에 반팔티 한장만 입자" (아이디병원, 여유증)** — season + scene generate the need naturally. Relatively safe.
|
||||
- **"심술보, 불독살, 처진볼살" (아이디병원, 중안부)** — lists the concern in exact customer words. Relatively safe (avoid shaming register).
|
||||
- **Steal this:** open on the mirror/photo/clothing moment where the concern actually bites.
|
||||
|
||||
## 4. Subjecthood / self-determination
|
||||
Make the customer the subject; the clinic is the helper.
|
||||
- **"예쁘게 나답게" (AB성형외과)** — parallel + self-acceptance. Safe.
|
||||
- 🌐 **"You... redefined." (Centra)** — ellipsis whitespace + "redefined" in one word; ultra-concise, adapts well to Korean. Safe.
|
||||
- 🌐 **"Own Your Look" (BOTOX/Allergan)** — imperative agency. Safe in spirit.
|
||||
- **Steal this:** put 당신/나 as the grammatical subject; frame surgery as the customer's decision, not the clinic's promise.
|
||||
|
||||
## 5. Indirect emotion (result → feeling; dodges effect-claims)
|
||||
Point at how life feels afterward, not at the physical result — the safest way to be moving.
|
||||
- **"세상이 나에게 친절해졌다" (본 아이템)** — change framed as how the world *treats* you. Safe, long resonance.
|
||||
- **"당신의 피부에 자신감을" (RNME, 슈링크)** — abstract value (confidence). Safe.
|
||||
- 🌐 **"Recapture the beauty of self-confidence."** — beauty = confidence; avoids appearance claims, matches Korea's 2026 trust-first shift. Safe.
|
||||
- **Steal this:** shift the object from the face to the feeling/relationship; this both moves people and clears 심의.
|
||||
|
||||
## 6. Wordplay / rhyme mnemonic
|
||||
Sound-based memorability tied to the brand.
|
||||
- **"예쁘면 DA야! / 잘생기면 DA야!" (디에이)** — brand name + interjection pun, gendered variants for reach. Relatively safe (rhyme-led).
|
||||
- **"당신의 뷰티메이트" (Beauty+Medical+Mate, 리앤영)** — coined word compresses a "companion" position. Safe.
|
||||
- **Steal this:** find the pun that lives inside the brand name; make it repeatable.
|
||||
|
||||
## 7. Location / positioning anchor
|
||||
Compress positioning into a place or association.
|
||||
- **"신사역에 있는 쥬얼리 / 가슴 성형을 잘 하는 쥬얼리" (쥬얼리)** — place anchor + drives associated search. Compliance: caution — "잘 하는" implies superiority; soften.
|
||||
- **Steal this:** anchor to a place/association the customer already navigates by.
|
||||
|
||||
## 8. Question hook / curiosity
|
||||
Open a loop the reader wants closed.
|
||||
- **"한 장의 시트가 피부를 얼마나 바꿀 수 있을까" (더우주)** — curiosity question, easy to transplant to procedure content openers. Safe.
|
||||
- **Steal this:** ask the exact question the hesitant customer is already asking — but don't answer it with a guarantee.
|
||||
|
||||
## 9. Contrast / triad structure
|
||||
Structural rhythm carries the message.
|
||||
- 🌐 **"Look Better. Breathe Better. Sleep Better."** — triad; sells *function* (e.g., rhinoplasty) — functional benefit is comparatively 심의-safe. Safe.
|
||||
- **"다시, 원인을 정확히 분석하다" (아이디병원, 재수술)** — process/trust framing for anxious re-op customers; fits the 2026 trend. Safe.
|
||||
- **Steal this:** triads and "process over promise" reassure without claiming results.
|
||||
|
||||
---
|
||||
|
||||
## AVOID cluster — frequent clichés that overlap with 심의 risk
|
||||
Seen repeatedly in the wild; low distinctiveness and usually risky. Details in `corpus_cliche.md` / `corpus_compliance_risk.md`.
|
||||
- Price/inducement: 특가 · 파격 · 초특가 · "최대 OO% 할인" · 선착순 O명 · 후기 작성 시 할인
|
||||
- Superlative/exclusivity: 최고 · 1위 · 유일 · 국내최초 · (EN) best / only / perfect
|
||||
- Effect-guarantee: 예뻐진다(단정) · 흉터/부작용 없는 · 책임진료
|
||||
- Shaming/objectifying: 넙데데 · 코끼리 다리 · "SIZE MATTERS" (전형적 성상품화 논란)
|
||||
- Tired beauty words: 여신 · 리즈 갱신 · 인형 같은
|
||||
|
||||
## Market context (why the above matters)
|
||||
Korean aesthetic marketing is shifting from **price competition → trust competition**: event/discount lines are shrinking, and story-led, indirect emotional appeals ("변화보다 자신감") both pass 심의 and perform better. English-market lines built on `best / only / results / perfect` should **not** be translated literally — they become superlative/effect-guarantee risks in Korea.
|
||||
|
||||
## Distilled principle
|
||||
The safest *and* catchiest formula = **(a) metaphor/brand-name fusion + (b) subjecthood framing + (c) process/emotion instead of result claims** — and never superlatives, discount inducement, or comparison.
|
||||
|
||||
---
|
||||
|
||||
## Sources (traceability)
|
||||
- 바노바기 공식 — https://www.banobagi.com/page/sub07_00
|
||||
- 쥬얼리성형외과 인터뷰(채널톡) — https://channel.io/ko/blog/articles/cs-case-jewerly-e75ca530
|
||||
- 리쥬란(나무위키) — https://namu.wiki/w/%EB%A6%AC%EC%A5%AC%EB%9E%80
|
||||
- 차앤박(CNP) 브랜드스토리 — https://www.cnpskin.com/pc/cnp/about-us/brand-story.html
|
||||
- 나무성형외과 공모전(위비티) — https://www.wevity.com/index_university.php?c=find&s=_university&gbn=viewok&gp=71&ix=55321
|
||||
- 아이디병원 프로모션 — https://www.idhospital.com/promotion/onsale
|
||||
- 디에이성형외과 — https://daprs.com/board/event/list
|
||||
- 카피 모음(채널톡) — https://channel.io/ko/blog/articles/copy222-ffa64ebe
|
||||
- 성형외과 광고 인사이트(신뢰 중심 전환, AMPM) — https://inside.ampm.co.kr/insight/13055
|
||||
- 강남언니 광고 가이드 — https://blog.gangnamunni.com/post/ads-guide
|
||||
- 글로벌 클리닉 슬로건 DB — http://www.textart.ru/advertising/slogans/plastic-surgery.html
|
||||
- BOTOX "The One & Only" (AbbVie) — https://news.abbvie.com/2025-09-09-BOTOX-R-Cosmetic-onabotulinumtoxinA-Unveils-The-One-Only-Campaign
|
||||
@@ -0,0 +1,40 @@
|
||||
# 5-Axis Re-scoring Rubric
|
||||
|
||||
Score the original and the improved copy on the same basis to make "did it actually get better?" objective.
|
||||
|
||||
## Axes and scale (1–5 each; compliance is a gate)
|
||||
|
||||
| Axis | Question | 1 | 5 |
|
||||
|------|----------|---|---|
|
||||
| **Freshness / Hook** | Non-stale and eye-catching? | tired, boilerplate | makes you stop and read |
|
||||
| **Distinctiveness** | Avoids the clichés everyone uses? | seen-it-before | only this clinic |
|
||||
| **Brand fit** | Matches tone & brand assets? | off-tone | reinforces brand-ness |
|
||||
| **Compliance (심의)** | Free of risk expressions? | — | — (PASS/FAIL gate) |
|
||||
| **Clarity** | Understood instantly? | what does it say | one-pass clear |
|
||||
|
||||
## Compliance gate rule
|
||||
- If any 🔴 (risk expression) exists → **FAIL** → that option cannot be adopted, no matter how high the other scores.
|
||||
- Missing mandatory disclosure (advertiser / side-effects / review number) is also a FAIL → then `[확인]`.
|
||||
|
||||
## Brand-asset line (required)
|
||||
When scoring brand fit, avoid stopping at a vague "feel." Write one line:
|
||||
> Brand **attribute / association / asset** this copy strengthens: (e.g., a "naturalness" association, an "honest expert" attribute, a slogan asset)
|
||||
|
||||
This anchors qualitative judgment in a measurable direction instead of vague sentiment.
|
||||
|
||||
## Judgment / improvement priority
|
||||
1. Compliance gate first (fix immediately if FAIL)
|
||||
2. Clarity (if it doesn't read, appeal is moot)
|
||||
3. Distinctiveness & freshness (remove clichés → strengthen hook)
|
||||
4. Brand-fit fine-tuning
|
||||
|
||||
## Scoring example
|
||||
```
|
||||
| Axis | Original | Recommended |
|
||||
| Freshness | 2 | 4 |
|
||||
| Distinctiveness | 2 | 4 |
|
||||
| Brand fit | 3 | 5 |
|
||||
| Compliance | PASS | PASS |
|
||||
| Clarity | 4 | 4 |
|
||||
→ Strengthened asset: "표정 습관" reframe reinforces the "honest educator" attribute
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Recursive Improvement Protocol — Evolving the Corpus
|
||||
|
||||
This skill's taste standard is not fixed; it **learns every campaign**. That keeps pace with staleness and keeps the differentiation coordinates current.
|
||||
|
||||
## After each trimming session
|
||||
1. **Adopted expressions** → if a newly-working pattern, add to `corpus_effective.md` (generalize the structure/principle; do not store the whole line).
|
||||
2. **Rejected / trimmed expressions** → if stale, add to `corpus_cliche.md`; if a compliance risk, add to `corpus_compliance_risk.md`.
|
||||
3. **Demotion**: move once-effective but now-common expressions from `corpus_effective` → `corpus_cliche`.
|
||||
|
||||
## Tagging schema (record on each addition)
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| expression | expression/pattern (prefer a generalized form over a single line) |
|
||||
| class | effective / cliché / risk |
|
||||
| reason | why this class (one line) |
|
||||
| channel | channel it was mainly used in |
|
||||
| date | added/updated date |
|
||||
| source | campaign / client source |
|
||||
|
||||
Example:
|
||||
```
|
||||
| "headline it with the customer's own concern" | effective | instant empathy, stops the scroll | Instagram/blog | 2026-07 | Jamie 표정케어 |
|
||||
| "인생 리즈 갱신" | cliché | 10-yr-old beauty buzzword, no distinctiveness | all | 2026-07 | — |
|
||||
```
|
||||
|
||||
## Periodic cleanup (quarterly recommended)
|
||||
- Merge duplicates, delete dead entries
|
||||
- If `corpus_effective` grows too large, review candidates for demotion
|
||||
- Reflect changes in 심의 standards (check the self-regulatory body's notices → `[확인]`)
|
||||
|
||||
## Principle recap
|
||||
- The corpus is for **avoidance / coordinates, not imitation**. Even `effective` entries mean "borrow the structure, write anew," not "reuse."
|
||||
- Always confirm currency of compliance items. Judgments here are guidance, not legal advice.
|
||||
|
||||
## Optional user feedback loop
|
||||
After a campaign ends, if the user shares which copy performed well/poorly (reactions, conversions, reviews), use that signal to re-tag effective/cliché. Performance data is the corpus's final arbiter.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Making Copy Trendy / Catchy Within 심의 Limits
|
||||
|
||||
> Premise: **the ceiling on wit is set by 의료광고 심의.** If the fun relies on exaggeration, superlatives, inducement, or comparison, it is void.
|
||||
> Goal: restrained appeal that still catches the eye inside the rules.
|
||||
|
||||
## Six techniques that work
|
||||
|
||||
### 1. Win with specificity
|
||||
Drop abstractions ("아름다움") for a concrete scene or object — the concreteness itself feels fresh.
|
||||
- Flat: "자신감을 드립니다" → Sharp: "거울 보는 시간이 즐거워집니다"
|
||||
|
||||
### 2. Borrow the customer's words
|
||||
Use the exact phrase the target types into a search box or community post as the headline.
|
||||
- Ex: "표정 관리, 매번 큰맘 먹지 않아도"
|
||||
|
||||
### 3. Unexpected frame (reframe/twist)
|
||||
Bend a familiar idea slightly, without creating a misunderstanding.
|
||||
- Ex: "주름은 나이가 아니라 표정 습관" (educational reframe)
|
||||
|
||||
### 4. Rhythm / parallelism / rhyme
|
||||
Make it stick through the pleasure of sound: parallel structure, triads, alliteration.
|
||||
- Caution: never sacrifice meaning for rhyme.
|
||||
|
||||
### 5. Restrained humor (keep dignity)
|
||||
Light but not cheap; never at the cost of the clinic's trust.
|
||||
- Safe: situational empathy humor ("월요일 아침 눈꺼풀처럼 무거운")
|
||||
- Risky: appearance-shaming, anxiety-baiting, self-deprecation
|
||||
|
||||
### 6. Whitespace and brevity
|
||||
Don't say everything. Stop at one line and leave the rest to consultation.
|
||||
|
||||
## Boundaries — wit that does NOT work
|
||||
- Appearance-shaming / anxiety-baiting ("이대로 괜찮으세요?" pressure)
|
||||
- Superlatives dressed as humor ("완벽 변신 실화")
|
||||
- Inducement-as-fun ("친구 데려오면 개이득") → 심의 / inducement violation
|
||||
- Neologism / meme overuse → ages fast and is a top 심의-rejection cause
|
||||
- Dignity-damaging or provocative gags
|
||||
|
||||
## Wit intensity by channel
|
||||
| Channel | Wit allowance | Note |
|
||||
|---------|---------------|------|
|
||||
| Instagram | High (still 심의) | Focus on the first-line hook |
|
||||
| Blog / homepage | Medium | Educational tone first; wit at the subhead level |
|
||||
| 카플친 / 알림톡 | Low | Warm, thank-you tone; no heavy gags |
|
||||
| In-clinic POP | Medium | One catchy line, understood instantly |
|
||||
| Search ad | Low | Clarity and compliance first |
|
||||
|
||||
## Self-check questions
|
||||
- Does this joke create a misunderstanding (effect / safety)?
|
||||
- Is any inducement (economic-benefit emphasis) mixed in?
|
||||
- Will it still be fresh in 6 months (not meme-dependent)?
|
||||
- Does it read cleanly aloud, with no stumble?
|
||||
118
custom-skills/50-notebooklm-agent/SKILL.md
Normal file
118
custom-skills/50-notebooklm-agent/SKILL.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: notebooklm-agent
|
||||
description: |
|
||||
Q&A agent for NotebookLM notebooks. Ask questions and get grounded, citation-backed answers from your sources.
|
||||
Triggers: ask NotebookLM, query notebook, research question, 노트북 질문, NotebookLM 에이전트.
|
||||
---
|
||||
|
||||
# NotebookLM Agent
|
||||
|
||||
Q&A agent that answers questions using NotebookLM's Gemini-powered analysis. Returns grounded responses with source citations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
NotebookLM CLI must be installed and authenticated:
|
||||
|
||||
```bash
|
||||
pip install notebooklm-py
|
||||
playwright install chromium
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
## When This Skill Activates
|
||||
|
||||
- User asks "ask NotebookLM about X"
|
||||
- User wants to "query my notebook"
|
||||
- User needs "research answers from sources"
|
||||
- Korean: "노트북LM에서 찾아줘", "노트북 질문"
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| List notebooks | `notebooklm list` |
|
||||
| Set active notebook | `notebooklm use <id>` |
|
||||
| Ask question | `notebooklm ask "question"` |
|
||||
| New conversation | `notebooklm ask "question" --new` |
|
||||
| With citations | `notebooklm ask "question" --json` |
|
||||
| Specific sources | `notebooklm ask "q" -s src1 -s src2` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Select Notebook
|
||||
|
||||
```bash
|
||||
# List available notebooks
|
||||
notebooklm list
|
||||
|
||||
# Set context (use partial ID)
|
||||
notebooklm use abc123
|
||||
```
|
||||
|
||||
### 2. Ask Questions
|
||||
|
||||
```bash
|
||||
# Simple question
|
||||
notebooklm ask "What are the main findings?"
|
||||
|
||||
# Follow-up (continues conversation)
|
||||
notebooklm ask "Can you elaborate on point 2?"
|
||||
|
||||
# New conversation
|
||||
notebooklm ask "Different topic" --new
|
||||
|
||||
# Query specific sources only
|
||||
notebooklm ask "Compare these two" -s source1_id -s source2_id
|
||||
```
|
||||
|
||||
### 3. Get Structured Output
|
||||
|
||||
For citations and references, use `--json`:
|
||||
|
||||
```bash
|
||||
notebooklm ask "Summarize the methodology" --json
|
||||
```
|
||||
|
||||
Returns:
|
||||
```json
|
||||
{
|
||||
"answer": "The methodology involves... [1] [2]",
|
||||
"references": [
|
||||
{"source_id": "abc...", "citation_number": 1, "cited_text": "..."},
|
||||
{"source_id": "def...", "citation_number": 2, "cited_text": "..."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Autonomy Rules
|
||||
|
||||
**Run automatically:**
|
||||
- `notebooklm list` - view notebooks
|
||||
- `notebooklm status` - check context
|
||||
- `notebooklm source list` - view sources
|
||||
- `notebooklm ask "..."` - answer questions
|
||||
|
||||
**Ask before running:**
|
||||
- `notebooklm delete` - destructive operations
|
||||
- `notebooklm source add` - modifies notebook
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Set context first**: Always `use` a notebook before asking
|
||||
2. **Use --json for citations**: Get structured references for research
|
||||
3. **Continue conversations**: Omit `--new` for follow-up questions
|
||||
4. **Filter sources**: Use `-s` to query specific documents only
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| "No notebook context" | Run `notebooklm use <id>` |
|
||||
| Auth error | Run `notebooklm login` |
|
||||
| Source not found | Check `notebooklm source list` |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **notebooklm-automation**: Full notebook management
|
||||
- **notebooklm-studio**: Generate podcasts, videos, quizzes
|
||||
- **notebooklm-research**: Add sources and research workflows
|
||||
104
custom-skills/51-notebooklm-automation/SKILL.md
Normal file
104
custom-skills/51-notebooklm-automation/SKILL.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: notebooklm-automation
|
||||
description: |
|
||||
Complete NotebookLM automation for notebooks, sources, and artifacts management.
|
||||
Triggers: manage NotebookLM, create notebook, add sources, 노트북 관리, NotebookLM 자동화.
|
||||
---
|
||||
|
||||
# NotebookLM Automation
|
||||
|
||||
Complete programmatic control over NotebookLM notebooks, sources, and artifacts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
pip install notebooklm-py
|
||||
playwright install chromium
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
## When This Skill Activates
|
||||
|
||||
- "Create a NotebookLM notebook"
|
||||
- "Add sources to NotebookLM"
|
||||
- "Manage my notebooks"
|
||||
- Korean: "노트북 만들어줘", "소스 추가"
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Notebook Operations
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| List all | `notebooklm list` |
|
||||
| List (JSON) | `notebooklm list --json` |
|
||||
| Create | `notebooklm create "Title"` |
|
||||
| Rename | `notebooklm rename <id> "New"` |
|
||||
| Delete | `notebooklm delete <id>` |
|
||||
| Set context | `notebooklm use <id>` |
|
||||
|
||||
### Source Operations
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Add URL | `notebooklm source add "https://..."` |
|
||||
| Add file | `notebooklm source add ./file.pdf` |
|
||||
| Add YouTube | `notebooklm source add "youtube.com/..."` |
|
||||
| List sources | `notebooklm source list` |
|
||||
| Delete source | `notebooklm source delete <id>` |
|
||||
| Wait for ready | `notebooklm source wait <id>` |
|
||||
|
||||
### Artifact Operations
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| List artifacts | `notebooklm artifact list` |
|
||||
| Wait for completion | `notebooklm artifact wait <id>` |
|
||||
| Delete artifact | `notebooklm artifact delete <id>` |
|
||||
|
||||
## Workflows
|
||||
|
||||
### Bulk Import Sources
|
||||
|
||||
```bash
|
||||
notebooklm create "Research Collection"
|
||||
notebooklm source add "https://url1.com"
|
||||
notebooklm source add "https://url2.com"
|
||||
notebooklm source add ./local.pdf
|
||||
notebooklm source list
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Use `--json` for machine-readable output:
|
||||
|
||||
```bash
|
||||
# Create and capture ID
|
||||
NOTEBOOK_ID=$(notebooklm create "Docs" --json | jq -r '.id')
|
||||
|
||||
# Add sources
|
||||
notebooklm source add "https://docs.example.com" --json
|
||||
|
||||
# Export for downstream processing
|
||||
notebooklm list --json > notebooks.json
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `NOTEBOOKLM_HOME` | Custom config directory |
|
||||
| `NOTEBOOKLM_AUTH_JSON` | Inline auth (CI/CD) |
|
||||
|
||||
## Autonomy Rules
|
||||
|
||||
**Auto-run:** `list`, `status`, `source list`, `artifact list`, `create`, `use`
|
||||
**Ask first:** `delete`, `rename`
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| Auth error | `notebooklm login` |
|
||||
| No context | `notebooklm use <id>` |
|
||||
| Rate limit | Wait 5-10 min, retry |
|
||||
138
custom-skills/52-notebooklm-studio/SKILL.md
Normal file
138
custom-skills/52-notebooklm-studio/SKILL.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: notebooklm-studio
|
||||
description: |
|
||||
Content generation for NotebookLM Studio artifacts - podcasts, videos, quizzes, flashcards, and more.
|
||||
Triggers: create podcast, generate video, make quiz, 팟캐스트 만들기, 퀴즈 생성, NotebookLM 스튜디오.
|
||||
---
|
||||
|
||||
# NotebookLM Studio
|
||||
|
||||
Generate all NotebookLM Studio content types: audio, video, quizzes, flashcards, slide decks, infographics, mind maps, and data tables.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
pip install notebooklm-py
|
||||
playwright install chromium
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
## When This Skill Activates
|
||||
|
||||
- "Create a podcast about my sources"
|
||||
- "Generate a video explainer"
|
||||
- "Make flashcards for studying"
|
||||
- "Turn this into a quiz"
|
||||
- Korean: "팟캐스트 만들어줘", "비디오 생성", "퀴즈 만들기"
|
||||
|
||||
## Content Types
|
||||
|
||||
| Type | Command | Options | Output |
|
||||
|------|---------|---------|--------|
|
||||
| **Audio** | `generate audio` | `--format`, `--length`, `--language` | MP3 |
|
||||
| **Video** | `generate video` | `--style`, `--format` | MP4 |
|
||||
| **Quiz** | `generate quiz` | `--difficulty`, `--quantity` | JSON/MD/HTML |
|
||||
| **Flashcards** | `generate flashcards` | `--difficulty`, `--quantity` | JSON/MD/HTML |
|
||||
| **Slide Deck** | `generate slide-deck` | `--format`, `--length` | PDF |
|
||||
| **Infographic** | `generate infographic` | `--orientation`, `--detail` | PNG |
|
||||
| **Mind Map** | `generate mind-map` | (instant) | JSON |
|
||||
| **Data Table** | `generate data-table` | description required | CSV |
|
||||
| **Report** | `generate report` | `--format` | Markdown |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Generate Content
|
||||
|
||||
```bash
|
||||
# Audio (podcast)
|
||||
notebooklm generate audio "Focus on key findings"
|
||||
notebooklm generate audio --format debate --length longer
|
||||
|
||||
# Video
|
||||
notebooklm generate video --style whiteboard
|
||||
notebooklm generate video --style anime "Make it fun"
|
||||
|
||||
# Quiz & Flashcards
|
||||
notebooklm generate quiz --difficulty hard --quantity more
|
||||
notebooklm generate flashcards --quantity standard
|
||||
|
||||
# Visual content
|
||||
notebooklm generate slide-deck --format detailed
|
||||
notebooklm generate infographic --orientation portrait
|
||||
notebooklm generate mind-map
|
||||
|
||||
# Data extraction
|
||||
notebooklm generate data-table "Compare all methods mentioned"
|
||||
notebooklm generate report --format study_guide
|
||||
```
|
||||
|
||||
### Download Artifacts
|
||||
|
||||
```bash
|
||||
# Check status first
|
||||
notebooklm artifact list
|
||||
|
||||
# Download when ready
|
||||
notebooklm download audio ./podcast.mp3
|
||||
notebooklm download video ./overview.mp4
|
||||
notebooklm download quiz --format markdown ./quiz.md
|
||||
notebooklm download flashcards --format json ./cards.json
|
||||
notebooklm download slide-deck ./slides.pdf
|
||||
notebooklm download infographic ./infographic.png
|
||||
notebooklm download mind-map ./mindmap.json
|
||||
notebooklm download data-table ./data.csv
|
||||
```
|
||||
|
||||
## Video Styles
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
| `classic` | Standard presentation |
|
||||
| `whiteboard` | Hand-drawn whiteboard |
|
||||
| `kawaii` | Cute animated style |
|
||||
| `anime` | Japanese animation |
|
||||
| `pixel` | 8-bit pixel art |
|
||||
| `watercolor` | Painted aesthetic |
|
||||
| `neon` | Glowing neon effects |
|
||||
| `paper` | Paper cutout animation |
|
||||
| `sketch` | Pencil sketch style |
|
||||
|
||||
## Audio Formats
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `deep-dive` | Comprehensive exploration |
|
||||
| `brief` | Quick summary |
|
||||
| `critique` | Critical analysis |
|
||||
| `debate` | Two-sided discussion |
|
||||
|
||||
## Processing Times
|
||||
|
||||
| Type | Typical Time | Timeout |
|
||||
|------|--------------|---------|
|
||||
| Mind map | Instant | - |
|
||||
| Quiz/Flashcards | 5-15 min | 900s |
|
||||
| Audio | 10-20 min | 1200s |
|
||||
| Video | 15-45 min | 2700s |
|
||||
|
||||
## Autonomy Rules
|
||||
|
||||
**Auto-run:** `artifact list`, `artifact wait` (in subagent)
|
||||
**Ask first:** `generate *`, `download *`
|
||||
|
||||
## Language Settings
|
||||
|
||||
```bash
|
||||
notebooklm language list # Show 80+ languages
|
||||
notebooklm language set ja # Japanese
|
||||
notebooklm language set ko # Korean
|
||||
notebooklm language set zh_Hans # Simplified Chinese
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| Rate limited | Wait 5-10 min, retry |
|
||||
| Generation failed | Check `artifact list`, retry later |
|
||||
| Download fails | Ensure artifact status is `completed` |
|
||||
144
custom-skills/53-notebooklm-research/SKILL.md
Normal file
144
custom-skills/53-notebooklm-research/SKILL.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
name: notebooklm-research
|
||||
description: |
|
||||
Research and source discovery for NotebookLM. Web/Drive research, auto-import, and source text extraction.
|
||||
Triggers: research topic, find sources, web research, 리서치, 자료 조사, NotebookLM 연구.
|
||||
---
|
||||
|
||||
# NotebookLM Research
|
||||
|
||||
Research workflows for NotebookLM: web research, Drive search, auto-import, and source content extraction.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
pip install notebooklm-py
|
||||
playwright install chromium
|
||||
notebooklm login
|
||||
```
|
||||
|
||||
## When This Skill Activates
|
||||
|
||||
- "Research [topic] in NotebookLM"
|
||||
- "Find sources about X"
|
||||
- "Do web research on Y"
|
||||
- "Search my Drive for documents"
|
||||
- Korean: "리서치 해줘", "자료 찾아줘", "웹 검색"
|
||||
|
||||
## Research Modes
|
||||
|
||||
| Mode | Sources Found | Time | Use Case |
|
||||
|------|---------------|------|----------|
|
||||
| `fast` | 5-10 | seconds | Quick overview |
|
||||
| `deep` | 20+ | 2-5 min | Comprehensive research |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Web Research
|
||||
|
||||
```bash
|
||||
# Fast research (default)
|
||||
notebooklm source add-research "artificial intelligence trends"
|
||||
|
||||
# Deep research with auto-import
|
||||
notebooklm source add-research "climate change policy" --mode deep --import-all
|
||||
|
||||
# Deep research (non-blocking, wait separately)
|
||||
notebooklm source add-research "topic" --mode deep --no-wait
|
||||
notebooklm research wait --import-all
|
||||
```
|
||||
|
||||
### Drive Research
|
||||
|
||||
```bash
|
||||
# Search Google Drive
|
||||
notebooklm source add-research "quarterly report" --from drive
|
||||
|
||||
# Deep Drive search
|
||||
notebooklm source add-research "project docs" --from drive --mode deep
|
||||
```
|
||||
|
||||
### Research Status
|
||||
|
||||
```bash
|
||||
# Check ongoing research
|
||||
notebooklm research status
|
||||
|
||||
# Wait for completion
|
||||
notebooklm research wait
|
||||
notebooklm research wait --import-all # Auto-import found sources
|
||||
```
|
||||
|
||||
## Source Content Extraction
|
||||
|
||||
```bash
|
||||
# Get indexed fulltext
|
||||
notebooklm source fulltext <source_id>
|
||||
notebooklm source fulltext <source_id> --json
|
||||
|
||||
# Get AI-generated guide
|
||||
notebooklm source guide <source_id>
|
||||
```
|
||||
|
||||
## Workflow: Research to Analysis
|
||||
|
||||
```bash
|
||||
# 1. Create notebook
|
||||
notebooklm create "AI Research Project"
|
||||
|
||||
# 2. Run deep research
|
||||
notebooklm source add-research "large language models 2024" --mode deep --no-wait
|
||||
|
||||
# 3. Wait and import (can spawn subagent for this)
|
||||
notebooklm research wait --import-all
|
||||
|
||||
# 4. Verify sources
|
||||
notebooklm source list
|
||||
|
||||
# 5. Start analysis
|
||||
notebooklm ask "What are the key trends?"
|
||||
```
|
||||
|
||||
## Subagent Pattern for Deep Research
|
||||
|
||||
For non-blocking deep research:
|
||||
|
||||
```python
|
||||
# Main conversation
|
||||
notebooklm source add-research "topic" --mode deep --no-wait
|
||||
|
||||
# Spawn subagent to wait
|
||||
Task(
|
||||
prompt="Wait for research in notebook {id} and import sources.
|
||||
Use: notebooklm research wait -n {id} --import-all --timeout 300
|
||||
Report how many sources were imported.",
|
||||
subagent_type="general-purpose"
|
||||
)
|
||||
```
|
||||
|
||||
## Autonomy Rules
|
||||
|
||||
**Auto-run:**
|
||||
- `notebooklm research status`
|
||||
- `notebooklm source list`
|
||||
- `notebooklm source fulltext`
|
||||
- `notebooklm source guide`
|
||||
|
||||
**Ask first:**
|
||||
- `notebooklm source add-research` (modifies notebook)
|
||||
- `notebooklm research wait --import-all` (long-running)
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use deep mode** for comprehensive research
|
||||
2. **Use --no-wait** for non-blocking operations
|
||||
3. **Spawn subagent** for long waits
|
||||
4. **Check research status** before importing
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| No results | Try different keywords |
|
||||
| Timeout | Extend timeout or check status |
|
||||
| Rate limit | Wait and retry |
|
||||
290
custom-skills/60-gtm-audit/SKILL.md
Normal file
290
custom-skills/60-gtm-audit/SKILL.md
Normal file
@@ -0,0 +1,290 @@
|
||||
---
|
||||
name: gtm-audit
|
||||
description: |
|
||||
GTM container audit using Chrome DevTools and DTM Agent for tag verification.
|
||||
Triggers: audit GTM, GTM analysis, tag debugging, dataLayer inspection.
|
||||
---
|
||||
|
||||
# GTM Audit Skill
|
||||
|
||||
Automated audit workflow for GTM containers using **Chrome DevTools MCP** for browser inspection and **DTM Agent MCP** for GTM API operations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Chrome GTM Debug Profile (Required)
|
||||
|
||||
Before starting any GTM audit, launch the dedicated Chrome GTM Debug profile:
|
||||
|
||||
```bash
|
||||
chrome-gtm
|
||||
```
|
||||
|
||||
This launches Chrome with remote debugging enabled on port 9222, which is required for the chrome-devtools MCP server to connect.
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| **Profile Location** | `~/Library/Application Support/Chrome-GTM-Debug` |
|
||||
| **Debug Port** | 9222 |
|
||||
| **Launch Script** | `~/Utilities/chrome-gtm-debug.sh` |
|
||||
|
||||
**Note**: This is a separate Chrome instance from your regular browser. Install GTM-related extensions (Tag Assistant, etc.) in this profile for debugging work.
|
||||
|
||||
## Required MCP Servers
|
||||
|
||||
This skill requires the following MCP servers to be configured:
|
||||
|
||||
| MCP Server | Purpose | Tools Used |
|
||||
|------------|---------|------------|
|
||||
| **chrome-devtools** | Browser debugging & inspection | `navigate_page`, `evaluate_script`, `list_network_requests`, `list_console_messages`, `click`, `hover`, `take_screenshot`, `performance_start_trace` |
|
||||
| **dtm-agent** | GTM API operations | `dtm_status`, `dtm_list_tags`, `dtm_get_tag`, `dtm_list_triggers`, `dtm_get_trigger`, `dtm_list_variables`, `dtm_debug_performance`, `dtm_debug_preview` |
|
||||
|
||||
**Important**: The chrome-devtools MCP server requires Chrome to be running with `--remote-debugging-port=9222`. Always run `chrome-gtm` first before attempting to use chrome-devtools tools.
|
||||
|
||||
## Critical Workflow Rule
|
||||
|
||||
**ALWAYS use Chrome DevTools MCP FIRST before making GTM configuration changes.**
|
||||
|
||||
GTM triggering and parameter capturing issues are highly dependent on:
|
||||
- DOM structure and CSS selectors
|
||||
- Dynamic element loading and timing
|
||||
- DataLayer event sequences
|
||||
- JavaScript execution order
|
||||
- Network request timing
|
||||
|
||||
## Standard Debugging Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 0: SETUP │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Run `chrome-gtm` to launch Chrome GTM Debug profile │
|
||||
│ Verify: curl http://127.0.0.1:9222/json/version │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 1: INSPECT (Chrome DevTools MCP) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 1. navigate_page → Load target URL │
|
||||
│ 2. evaluate_script → Check window.dataLayer state │
|
||||
│ 3. list_network_requests → Identify GTM/tag requests │
|
||||
│ 4. list_console_messages → Check for JS errors │
|
||||
│ 5. click/hover → Simulate user interactions │
|
||||
│ 6. take_screenshot → Document current state │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 2: DIAGNOSE │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ • Identify DOM/timing/selector issues from browser state │
|
||||
│ • Compare expected vs actual dataLayer events │
|
||||
│ • Check network request payloads for missing parameters │
|
||||
│ • Review console for GTM/tag errors │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 3: CONFIGURE (DTM Agent MCP) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 1. dtm_status → Verify authentication │
|
||||
│ 2. dtm_list_tags → Review current tag configuration │
|
||||
│ 3. dtm_get_trigger → Inspect trigger conditions │
|
||||
│ 4. dtm_list_variables → Check variable mappings │
|
||||
│ 5. dtm_update_tag/trigger → Make configuration changes │
|
||||
│ 6. dtm_create_version → Create version for testing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 4: VERIFY (Chrome DevTools MCP) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ • Repeat Phase 1 inspection steps │
|
||||
│ • Confirm issue is resolved │
|
||||
│ • Document before/after comparison │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Chrome DevTools MCP Commands
|
||||
|
||||
### Essential Inspection Commands
|
||||
|
||||
```javascript
|
||||
// 1. Check dataLayer state
|
||||
evaluate_script: "JSON.stringify(window.dataLayer || [], null, 2)"
|
||||
|
||||
// 2. Get GTM container info
|
||||
evaluate_script: "Object.keys(window.google_tag_manager || {})"
|
||||
|
||||
// 3. Monitor dataLayer pushes (inject listener)
|
||||
evaluate_script: `
|
||||
window.__gtmPushLog = [];
|
||||
const originalPush = window.dataLayer.push;
|
||||
window.dataLayer.push = function(...args) {
|
||||
window.__gtmPushLog.push({timestamp: Date.now(), data: args[0]});
|
||||
return originalPush.apply(this, args);
|
||||
};
|
||||
`
|
||||
|
||||
// 4. Get captured pushes
|
||||
evaluate_script: "JSON.stringify(window.__gtmPushLog || [], null, 2)"
|
||||
|
||||
// 5. Check for GTM debug mode
|
||||
evaluate_script: "window.google_tag_manager?.['GTM-XXXXXX']?.preview?.active"
|
||||
```
|
||||
|
||||
### Network Request Patterns
|
||||
|
||||
| Tag Type | URL Pattern to Monitor |
|
||||
|----------|------------------------|
|
||||
| GA4 | `google-analytics.com/g/collect` |
|
||||
| Google Ads | `googleads.g.doubleclick.net` |
|
||||
| Meta Pixel | `facebook.com/tr` |
|
||||
| LinkedIn | `px.ads.linkedin.com` |
|
||||
| TikTok | `analytics.tiktok.com` |
|
||||
| Kakao | `track.tiara.kakao.com` |
|
||||
| Naver | `wcs.naver.net` |
|
||||
|
||||
## DTM Agent MCP Commands
|
||||
|
||||
### Container Analysis
|
||||
|
||||
```bash
|
||||
# Check authentication and active container
|
||||
dtm_status
|
||||
|
||||
# List all tags with optional filter
|
||||
dtm_list_tags --name_filter "GA4"
|
||||
|
||||
# Get specific tag details
|
||||
dtm_get_tag --tag_id "123"
|
||||
|
||||
# List triggers and their conditions
|
||||
dtm_list_triggers
|
||||
|
||||
# Container performance analysis
|
||||
dtm_debug_performance
|
||||
|
||||
# Validate container configuration
|
||||
dtm_debug_preview
|
||||
```
|
||||
|
||||
### Configuration Changes
|
||||
|
||||
```bash
|
||||
# Update tag configuration
|
||||
dtm_update_tag --tag_id "123" --config {...}
|
||||
|
||||
# Create new trigger
|
||||
dtm_create_trigger --name "Button Click" --type "CLICK"
|
||||
|
||||
# Create version for testing
|
||||
dtm_create_version --name "Debug Session 2025-01-03"
|
||||
```
|
||||
|
||||
## Audit Checklist
|
||||
|
||||
### Container Health
|
||||
- [ ] GTM script loads without errors (Chrome DevTools: console)
|
||||
- [ ] Container ID matches expected (Chrome DevTools: evaluate_script)
|
||||
- [ ] No duplicate containers (Chrome DevTools: network requests)
|
||||
- [ ] Script in correct position (Chrome DevTools: DOM inspection)
|
||||
|
||||
### DataLayer Quality
|
||||
- [ ] dataLayer initialized before GTM (Chrome DevTools: evaluate_script)
|
||||
- [ ] Event names follow conventions (Chrome DevTools: dataLayer inspection)
|
||||
- [ ] Required parameters present (DTM Agent: dtm_get_tag to check expected params)
|
||||
- [ ] Data types correct (Chrome DevTools: typeof checks)
|
||||
- [ ] E-commerce objects cleared before new push
|
||||
- [ ] No duplicate events firing (Chrome DevTools: network request count)
|
||||
|
||||
### Tag Configuration
|
||||
- [ ] Tags properly configured (DTM Agent: dtm_list_tags)
|
||||
- [ ] Triggers have correct conditions (DTM Agent: dtm_get_trigger)
|
||||
- [ ] Variables mapped correctly (DTM Agent: dtm_list_variables)
|
||||
- [ ] Firing sequence appropriate (DTM Agent: dtm_debug_performance)
|
||||
|
||||
### Tag Firing Verification
|
||||
- [ ] Pageview fires on all pages (Chrome DevTools: network requests)
|
||||
- [ ] Events fire on correct triggers (Chrome DevTools: click + network)
|
||||
- [ ] E-commerce events have required params (Chrome DevTools: request payload)
|
||||
- [ ] Conversion tags fire once (Chrome DevTools: request count)
|
||||
|
||||
## Common Issue Patterns
|
||||
|
||||
### Issue: Tag Not Firing
|
||||
|
||||
**Diagnosis (Chrome DevTools MCP first)**:
|
||||
1. `navigate_page` → Load the page
|
||||
2. `list_network_requests` → Check if ANY request to tag endpoint
|
||||
3. `evaluate_script` → Verify dataLayer event exists
|
||||
4. `list_console_messages` → Check for errors
|
||||
|
||||
**Fix (DTM Agent MCP)**:
|
||||
1. `dtm_get_trigger` → Review trigger conditions
|
||||
2. `dtm_update_trigger` → Adjust selector/event name
|
||||
3. `dtm_debug_preview` → Validate configuration
|
||||
|
||||
### Issue: Missing Parameters
|
||||
|
||||
**Diagnosis (Chrome DevTools MCP first)**:
|
||||
1. `list_network_requests` → Capture the actual request
|
||||
2. Parse request payload to see what's sent
|
||||
3. `evaluate_script` → Check dataLayer for expected values
|
||||
|
||||
**Fix (DTM Agent MCP)**:
|
||||
1. `dtm_list_variables` → Check variable configuration
|
||||
2. `dtm_update_tag` → Fix parameter mappings
|
||||
|
||||
### Issue: Duplicate Firing
|
||||
|
||||
**Diagnosis (Chrome DevTools MCP first)**:
|
||||
1. `list_network_requests` → Count requests to same endpoint
|
||||
2. `evaluate_script` → Check dataLayer for duplicate pushes
|
||||
|
||||
**Fix (DTM Agent MCP)**:
|
||||
1. `dtm_list_triggers` → Check for overlapping triggers
|
||||
2. `dtm_get_tag` → Review tag firing options
|
||||
|
||||
## Performance Analysis
|
||||
|
||||
Use Chrome DevTools MCP for performance:
|
||||
```
|
||||
performance_start_trace → Run page load → Analyze trace
|
||||
```
|
||||
|
||||
Compare with DTM Agent static analysis:
|
||||
```
|
||||
dtm_debug_performance → Get container complexity grade
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Audit generates structured report:
|
||||
|
||||
```json
|
||||
{
|
||||
"audit_metadata": {
|
||||
"url": "https://example.com",
|
||||
"timestamp": "2025-01-03T10:30:00Z",
|
||||
"container_id": "GTM-XXXXXX",
|
||||
"tools_used": ["chrome-devtools", "dtm-agent"]
|
||||
},
|
||||
"browser_inspection": {
|
||||
"dataLayer_events": [...],
|
||||
"network_requests": [...],
|
||||
"console_errors": [...],
|
||||
"performance_metrics": {...}
|
||||
},
|
||||
"gtm_configuration": {
|
||||
"tags": [...],
|
||||
"triggers": [...],
|
||||
"variables": [...],
|
||||
"container_grade": "B"
|
||||
},
|
||||
"issues": [...],
|
||||
"recommendations": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- **seo-core-web-vitals**: Use performance traces for Core Web Vitals impact analysis
|
||||
- **notion-writer**: Export audit reports to Notion database
|
||||
- **seo-schema-validator**: Validate e-commerce structured data alongside GTM
|
||||
294
custom-skills/61-gtm-editor/SKILL.md
Normal file
294
custom-skills/61-gtm-editor/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: gtm-editor
|
||||
description: >
|
||||
GTM implementation toolkit. Creates, updates, and modifies GTM tags, triggers,
|
||||
variables via API. Generates Custom HTML with ES5 compliance. Handles workspace
|
||||
lifecycle, DOM analysis for trigger design, and dataLayer code generation.
|
||||
Triggers on "create GTM tag", "generate dataLayer", "modify trigger", "update variable",
|
||||
"inject tracking code", "write custom HTML", "manage GTM", "design tags",
|
||||
"create conversion tag", "set up tracking".
|
||||
NOT for auditing (use gtm-audit) or validation/QA (use gtm-validator).
|
||||
---
|
||||
|
||||
# GTM Editor Skill
|
||||
|
||||
Create, modify, and deploy GTM configurations via API. Generates ES5-compliant Custom HTML tags.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### GTM Container Management (DTM Agent MCP)
|
||||
- `dtm_status` — Check auth and active account/container
|
||||
- `dtm_set_account` / `dtm_set_container` — Switch context
|
||||
- `dtm_list_tags` / `dtm_get_tag` / `dtm_create_tag` / `dtm_update_tag` / `dtm_delete_tag`
|
||||
- `dtm_list_triggers` / `dtm_get_trigger` / `dtm_create_trigger` / `dtm_delete_trigger`
|
||||
- `dtm_list_variables` / `dtm_get_variable` / `dtm_create_variable` / `dtm_delete_variable`
|
||||
- `dtm_list_folders` / `dtm_list_workspaces` / `dtm_get_workspace_status`
|
||||
- `dtm_list_versions` / `dtm_get_live_version` / `dtm_create_version`
|
||||
|
||||
### Browser Analysis (Chrome DevTools MCP)
|
||||
- `navigate_page` — Load page for DOM analysis
|
||||
- `evaluate_script` — Inspect forms, buttons, links, CSS selectors
|
||||
- `take_snapshot` — Get page structure for trigger design
|
||||
- `list_network_requests` — Verify tags fire after changes
|
||||
|
||||
### Reporting (Notion MCP)
|
||||
- Write implementation plans and tag configurations to Notion
|
||||
|
||||
## Tagging Workflow: dataLayer First (MANDATORY)
|
||||
|
||||
**Always push complexity into the dataLayer, not GTM triggers.**
|
||||
|
||||
### Step 1: Design the dataLayer push FIRST
|
||||
|
||||
Before creating any GTM configuration, design the `dataLayer.push()` that the website should implement. Present this to the user as a code snippet matching their tech stack:
|
||||
|
||||
**For vanilla JS / static HTML (ES5):**
|
||||
```javascript
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
'event': 'generate_lead',
|
||||
'lead_type': document.getElementById('requestType').value,
|
||||
'form_id': 'contact-form'
|
||||
});
|
||||
```
|
||||
|
||||
**For React / Next.js / TypeScript:**
|
||||
```typescript
|
||||
declare global { interface Window { dataLayer: Record<string, any>[]; } }
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
event: 'generate_lead',
|
||||
lead_type: selectedType,
|
||||
form_id: 'contact-form',
|
||||
});
|
||||
```
|
||||
|
||||
**For Vue:**
|
||||
```javascript
|
||||
window.dataLayer?.push({
|
||||
event: 'generate_lead',
|
||||
lead_type: this.selectedType,
|
||||
form_id: 'contact-form',
|
||||
});
|
||||
```
|
||||
|
||||
**For PHP / WordPress:**
|
||||
```php
|
||||
add_action('wp_footer', function() { ?>
|
||||
<script>
|
||||
document.getElementById('contact-form').addEventListener('submit', function() {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
'event': 'generate_lead',
|
||||
'lead_type': document.getElementById('requestType').value
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php });
|
||||
```
|
||||
|
||||
**ASK the user:** "Here's the dataLayer code for your developers. Can they add this to the page? What's your tech stack?"
|
||||
|
||||
### Step 2: Create simple GTM config
|
||||
|
||||
Once dataLayer push is agreed:
|
||||
- **Trigger:** Custom Event `{{_event}}` equals `generate_lead` (simple, robust)
|
||||
- **Variables:** DataLayer Variable for each parameter (e.g., `dlv - lead_type`)
|
||||
- **Tag:** GA4 Event with `measurementIdOverride` + DLV parameters
|
||||
|
||||
### Step 3: cHTML fallback (LAST RESORT)
|
||||
|
||||
Only if user confirms they CANNOT modify the website code:
|
||||
1. Create a Custom HTML tag that listens for DOM events
|
||||
2. The cHTML pushes to dataLayer internally
|
||||
3. A Custom Event trigger picks it up (same clean pattern)
|
||||
|
||||
**All cHTML must be ES5-compatible** (see ES5 section below).
|
||||
|
||||
### Decision Tree
|
||||
```
|
||||
Track user action?
|
||||
├─ Can devs add dataLayer.push()? → YES → Simple CE trigger ✅
|
||||
├─ Can't modify code, element has ID? → Use Click ID trigger ✅
|
||||
├─ No ID? → Ask user to add one first
|
||||
└─ Nothing possible? → cHTML + CE trigger (last resort)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### 1. Tag Creation via GTM API
|
||||
|
||||
Create tags directly in GTM — no manual pasting. The API handles workspace lifecycle automatically (auto-creates new workspace if current one is published).
|
||||
|
||||
**GA4 Event Tags:**
|
||||
```
|
||||
GA4 event tags use type "gaawe" with:
|
||||
- eventName: the GA4 event name (snake_case)
|
||||
- measurementIdOverride: {{GA4 Measurement ID variable}} (NOT measurementId)
|
||||
- eventParameters: list of name/value maps
|
||||
```
|
||||
|
||||
**CRITICAL: measurementIdOverride, NOT measurementId**
|
||||
GA4 event tags inherit from config — use `measurementIdOverride` to reference the measurement ID variable. The API rejects `measurementId`.
|
||||
|
||||
### 2. Trigger Design from DOM Analysis
|
||||
|
||||
Analyze page DOM to design precise triggers:
|
||||
|
||||
```javascript
|
||||
// Extract forms
|
||||
(function(){var forms=document.querySelectorAll('form');return JSON.stringify(Array.from(forms).map(function(f){return{id:f.id,action:f.action,fields:Array.from(f.querySelectorAll('input,textarea,select')).map(function(el){return{name:el.name,type:el.type,id:el.id}})}}));})()
|
||||
|
||||
// Extract CTAs and buttons
|
||||
(function(){return JSON.stringify(Array.from(document.querySelectorAll('a[class*="btn"],button,[role="button"]')).map(function(el){return{text:(el.textContent||'').trim().substring(0,50),href:el.href||'',classes:el.className,id:el.id}}));})()
|
||||
```
|
||||
|
||||
**Trigger types:**
|
||||
| Type | Use For | Key Parameter |
|
||||
|------|---------|---------------|
|
||||
| `linkClick` | `<a>` elements | `autoEventFilter` with CSS selector |
|
||||
| `click` | Any element | `filter` on Click ID/Classes |
|
||||
| `formSubmission` | Form submit | `filter` on Form ID |
|
||||
| `customEvent` | dataLayer events | `custom_event_filter` on `{{_event}}` |
|
||||
| `scrollDepth` | Scroll tracking | `verticalThresholdsPercent` |
|
||||
| `timer` | Time on page | `interval`, `limit` |
|
||||
| `domReady` | DOM loaded | No filter needed |
|
||||
|
||||
### 3. Custom HTML Generation (ES5 MANDATORY)
|
||||
|
||||
GTM Custom HTML uses a JavaScript compiler that does NOT support ES2020+.
|
||||
|
||||
**FORBIDDEN:**
|
||||
```javascript
|
||||
// NO: optional chaining, arrow functions, const/let, template literals,
|
||||
// destructuring, spread, async/await
|
||||
```
|
||||
|
||||
**REQUIRED:**
|
||||
```javascript
|
||||
// YES: var, function(){}, string concatenation, explicit property access
|
||||
(function() {
|
||||
var form = document.getElementById('contact-form');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function() {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
'event': 'generate_lead',
|
||||
'lead_type': form.querySelector('#requestType').value
|
||||
});
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
### 4. Workspace Lifecycle Management
|
||||
|
||||
GTM workspaces become **read-only after publishing**. The DTM Agent auto-handles this:
|
||||
- If create/update fails with "Workspace is already submitted"
|
||||
- Auto-creates a new workspace
|
||||
- Retries the operation
|
||||
- Caches the workspace ID for subsequent calls
|
||||
|
||||
**You don't need to manage workspaces manually.** Just call `dtm_create_tag` etc. and it works.
|
||||
|
||||
### 5. DataLayer Code Generation
|
||||
|
||||
Generate dataLayer push code for common tracking scenarios:
|
||||
|
||||
**E-commerce:**
|
||||
```javascript
|
||||
// purchase event
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({ ecommerce: null }); // Clear previous
|
||||
window.dataLayer.push({
|
||||
'event': 'purchase',
|
||||
'ecommerce': {
|
||||
'transaction_id': /* order ID */,
|
||||
'currency': 'KRW',
|
||||
'value': /* total */,
|
||||
'items': [{ item_id: '', item_name: '', price: 0, quantity: 1 }]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Forms/Leads:**
|
||||
```javascript
|
||||
window.dataLayer.push({
|
||||
'event': 'generate_lead',
|
||||
'lead_type': /* form type */,
|
||||
'form_id': /* form element ID */
|
||||
});
|
||||
```
|
||||
|
||||
## Korean Market Patterns
|
||||
|
||||
| Context | Pattern |
|
||||
|---------|---------|
|
||||
| Currency | KRW (no decimals) |
|
||||
| Payment buttons | 장바구니, 결제하기, 주문하기, 문의하기 |
|
||||
| Payment methods | 카카오페이, 네이버페이, 토스, 신용카드 |
|
||||
| Platforms | Kakao Pixel (`track.kakao.com`), Naver Analytics (`wcs.naver.net`) |
|
||||
|
||||
## Supported Tag Types
|
||||
|
||||
| GTM Type | Name | Platform |
|
||||
|----------|------|----------|
|
||||
| `googtag` | Google Tag | GA4 config |
|
||||
| `gaawe` | GA4 Event | GA4 events |
|
||||
| `gclidw` | Conversion Linker | Google Ads |
|
||||
| `awct` | Ads Conversion | Google Ads |
|
||||
| `html` | Custom HTML | Any (Meta, PostHog, Kakao, etc.) |
|
||||
| `cvt_MQDKZ` | Clarity | Microsoft Clarity |
|
||||
| `cvt_WF3R3` | Amplitude | Amplitude |
|
||||
|
||||
## FB Conversions API Integration
|
||||
|
||||
When creating tags that should trigger FB CAPI:
|
||||
1. Check existing FBEventName mapping table (Regex Table variable)
|
||||
2. If your custom event isn't mapped, add a new rule: `event_name → FB_Standard_Event`
|
||||
3. Key mappings: `generate_lead → Lead`, `purchase → Purchase`, `page_view → PageView`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Analyze**: Use Chrome DevTools to inspect the page (or receive from gtm-audit Mode D)
|
||||
2. **Design**: Determine events, triggers, variables needed
|
||||
3. **Create**: Use DTM Agent MCP tools to create in GTM
|
||||
4. **Verify**: Hand off to gtm-validator for QA
|
||||
5. **Document**: Write implementation details to Notion
|
||||
|
||||
## Trigger Design: IDs First (MANDATORY)
|
||||
|
||||
**Always prefer element `id` over CSS selectors.** IDs survive redesigns; CSS selectors break.
|
||||
|
||||
**Before creating ANY click/form trigger:**
|
||||
1. Check if the target element has an `id` attribute via Chrome DevTools `evaluate_script`
|
||||
2. **If it has an ID** → use `Click ID` or `Form ID` filter
|
||||
3. **If it does NOT have an ID** → **ASK the user** before falling back to CSS:
|
||||
> "This element doesn't have an ID. Can you add one to the website/app code?
|
||||
> Suggested: `id="[section]-[element]-[action]"` e.g., `id="hero-btn-consult"`"
|
||||
4. Only use CSS selectors when the user explicitly confirms they cannot modify the HTML
|
||||
|
||||
**Priority order:**
|
||||
1. Element ID (`Click ID equals "hero-btn-consult"`)
|
||||
2. Data attribute (`Click Element matches CSS [data-track="consult"]`)
|
||||
3. Form ID (`Form ID equals "contact-form"`)
|
||||
4. CSS selector (last resort — document which selectors are used)
|
||||
|
||||
**Suggested ID naming:** `[section]-[element]-[action]`
|
||||
- Navigation: `nav-about`, `nav-services`, `nav-insights`
|
||||
- CTAs: `hero-btn-consult`, `cta-btn-booking`
|
||||
- Forms: `contact-form`, `newsletter-form`
|
||||
- Footer: `footer-social-linkedin`, `footer-email`
|
||||
|
||||
## Rules
|
||||
|
||||
- Always use `dtm_status` first to verify auth and active container
|
||||
- Always use `measurementIdOverride` for GA4 event tags (NOT `measurementId`)
|
||||
- **Always prefer element IDs for triggers — ask user to add IDs before using CSS selectors**
|
||||
- All Custom HTML must be ES5-compatible
|
||||
- Use `--dry-run` conceptually — verify trigger selectors via Chrome DevTools before creating
|
||||
- Rate limit: space API calls 1 second apart in batch operations
|
||||
- Never delete tags without explicit user confirmation — move to Archive folder instead
|
||||
- After creating tags, suggest user run gtm-validator to verify
|
||||
225
custom-skills/62-gtm-validator/SKILL.md
Normal file
225
custom-skills/62-gtm-validator/SKILL.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: gtm-validator
|
||||
description: >
|
||||
GTM QA and validation toolkit. Verifies tags fire correctly on live pages,
|
||||
tests trigger conditions against actual DOM, validates dataLayer schemas,
|
||||
checks naming conventions, compares cross-platform events, and runs QA checklists.
|
||||
Uses Chrome DevTools MCP for live page testing.
|
||||
Triggers on "validate tags", "check triggers", "verify tracking", "QA GTM",
|
||||
"test events", "debug GTM", "troubleshoot", "naming conventions",
|
||||
"GTM question", "GTM best practice", "compare versions".
|
||||
NOT for auditing (use gtm-audit) or creating/modifying (use gtm-editor).
|
||||
---
|
||||
|
||||
# GTM Validator Skill
|
||||
|
||||
Verify GTM implementations work correctly on live pages. Test triggers, validate dataLayer, check naming conventions.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Browser Testing (Chrome DevTools MCP)
|
||||
- `navigate_page` — Load page to test
|
||||
- `evaluate_script` — Check dataLayer state, test selectors, verify globals
|
||||
- `list_network_requests` — Verify tag firing after interactions
|
||||
- `click` / `hover` — Simulate user actions to trigger events
|
||||
- `list_console_messages` — Check for JS errors
|
||||
- `take_screenshot` — Document test results
|
||||
|
||||
### GTM Container (DTM Agent MCP)
|
||||
- `dtm_status` — Check active container
|
||||
- `dtm_list_tags` / `dtm_get_tag` — Get tag configurations
|
||||
- `dtm_list_triggers` / `dtm_get_trigger` — Get trigger conditions
|
||||
- `dtm_list_variables` / `dtm_get_variable` — Get variable definitions
|
||||
- `dtm_list_versions` / `dtm_get_live_version` — Compare versions
|
||||
|
||||
## Validation Modes
|
||||
|
||||
### 1. Tag Firing Verification
|
||||
|
||||
Test that specific tags fire on the correct pages and interactions.
|
||||
|
||||
**Steps:**
|
||||
1. `dtm_list_tags` — get all active tags with their triggers
|
||||
2. `navigate_page` to target URL
|
||||
3. `list_network_requests` — capture initial tag firings
|
||||
4. For click triggers: `click` the element, then check `list_network_requests` again
|
||||
5. Compare: expected tags (from GTM config) vs actual network requests
|
||||
|
||||
**Verification script for specific tag:**
|
||||
```javascript
|
||||
// Check if GA4 event fired
|
||||
(function(){
|
||||
var dl = window.dataLayer || [];
|
||||
var events = dl.filter(function(e){return e.event && e.event !== 'gtm.js' && e.event !== 'gtm.dom' && e.event !== 'gtm.load';});
|
||||
return JSON.stringify(events.map(function(e){return {event: e.event, params: Object.keys(e).filter(function(k){return k !== 'event' && !k.startsWith('gtm.')})}}));
|
||||
})()
|
||||
```
|
||||
|
||||
### 2. Trigger Condition Testing
|
||||
|
||||
Verify trigger CSS selectors and conditions match the actual page DOM.
|
||||
|
||||
**Steps:**
|
||||
1. `dtm_get_trigger` — get trigger filter/autoEventFilter
|
||||
2. Extract CSS selectors from trigger config
|
||||
3. `evaluate_script` — test if selectors match elements on page:
|
||||
|
||||
```javascript
|
||||
// Test CSS selector
|
||||
(function(selector){
|
||||
var els = document.querySelectorAll(selector);
|
||||
return JSON.stringify({
|
||||
selector: selector,
|
||||
matchCount: els.length,
|
||||
elements: Array.from(els).slice(0,5).map(function(el){
|
||||
return {tag: el.tagName, id: el.id, classes: el.className, text: (el.textContent||'').trim().substring(0,30)};
|
||||
})
|
||||
});
|
||||
})('.hero a.btn')
|
||||
```
|
||||
|
||||
4. If selector matches 0 elements → trigger will NEVER fire → **CRITICAL issue**
|
||||
5. If selector matches too many elements → trigger may fire incorrectly → **WARNING**
|
||||
|
||||
### 3. DataLayer Schema Validation
|
||||
|
||||
Verify dataLayer pushes contain required fields with correct types.
|
||||
|
||||
**GA4 Recommended Event Schemas:**
|
||||
|
||||
| Event | Required Fields |
|
||||
|-------|----------------|
|
||||
| `page_view` | (automatic) |
|
||||
| `generate_lead` | — (value, currency optional) |
|
||||
| `purchase` | transaction_id, value, currency, items[] |
|
||||
| `add_to_cart` | items[] (item_id, item_name, price, quantity) |
|
||||
| `form_start` | form_id, form_name |
|
||||
| `form_submit` | form_id, form_name |
|
||||
|
||||
**Validation script:**
|
||||
```javascript
|
||||
// Check dataLayer for specific event schema
|
||||
(function(eventName){
|
||||
var dl = window.dataLayer || [];
|
||||
var events = dl.filter(function(e){return e.event === eventName;});
|
||||
if(events.length === 0) return JSON.stringify({found: false, event: eventName});
|
||||
var e = events[events.length-1];
|
||||
return JSON.stringify({found: true, event: eventName, keys: Object.keys(e), data: e});
|
||||
})('generate_lead')
|
||||
```
|
||||
|
||||
### 4. Naming Convention Check
|
||||
|
||||
Verify all GTM resources follow D.intelligence naming standards.
|
||||
|
||||
**Tag naming:**
|
||||
```
|
||||
[Platform] - [event_name] [context]
|
||||
Examples:
|
||||
GA4 - generate_lead (contact form)
|
||||
sGTM - purchase
|
||||
cHTML - Contact Form dataLayer Events
|
||||
FB_CONVERSIONS_API-PIXEL_ID-Web-Tag-GA4_Event
|
||||
```
|
||||
|
||||
**Trigger naming:**
|
||||
```
|
||||
[Type] - [description] Trigger
|
||||
Examples:
|
||||
Click - Hero CTA (consult)
|
||||
CE - contact_form_submit
|
||||
Form Submit - contact-form
|
||||
Scroll - 50pct Depth
|
||||
PV - consult booking Trigger
|
||||
```
|
||||
|
||||
**Variable naming:**
|
||||
```
|
||||
[prefix] - [description]
|
||||
Prefixes: dlv (dataLayer), cjs (Custom JS), aev (Auto-Event),
|
||||
jsv (JS Variable), URL query, cookie, c (Constant)
|
||||
Examples:
|
||||
dlv - contact_form.request_type
|
||||
cjs - Social platform
|
||||
aev - click url hostname
|
||||
```
|
||||
|
||||
**Validation steps:**
|
||||
1. `dtm_list_tags` / `dtm_list_triggers` / `dtm_list_variables`
|
||||
2. Check each name against patterns above
|
||||
3. Flag violations: missing prefix, wrong case, unclear description
|
||||
|
||||
### 5. Cross-Platform Event Mapping Verification
|
||||
|
||||
Verify the same user action sends consistent events to all platforms.
|
||||
|
||||
| User Action | GA4 | Meta (FB) | Google Ads | Kakao | Naver |
|
||||
|------------|-----|-----------|------------|-------|-------|
|
||||
| Page view | page_view | PageView | — | pageView | — |
|
||||
| Product view | view_item | ViewContent | — | viewContent | — |
|
||||
| Add to cart | add_to_cart | AddToCart | — | addToCart | — |
|
||||
| Purchase | purchase | Purchase | purchase | purchase | — |
|
||||
| Lead form | generate_lead | Lead | submit_lead_form | participation | — |
|
||||
|
||||
**Steps:**
|
||||
1. Trigger a user action (click, form submit)
|
||||
2. `list_network_requests` — capture all resulting requests
|
||||
3. Check each platform received the correct event:
|
||||
- GA4: `google-analytics.com/g/collect?en=...`
|
||||
- Meta: `facebook.com/tr/?ev=...`
|
||||
- Google Ads: `googleads.g.doubleclick.net/pagead/...`
|
||||
|
||||
### 6. QA Checklist Execution
|
||||
|
||||
Run through a standard QA checklist for GTM implementation:
|
||||
|
||||
- [ ] GTM container snippet in `<head>` (not `<body>`)
|
||||
- [ ] GTM noscript fallback in `<body>`
|
||||
- [ ] No duplicate GTM containers on same page
|
||||
- [ ] dataLayer initialized before GTM snippet
|
||||
- [ ] All P1 tags fire on page load (GA4 config, Consent Mode)
|
||||
- [ ] Click triggers use specific selectors (not generic classes)
|
||||
- [ ] Form triggers match form IDs that exist on page
|
||||
- [ ] Custom HTML tags use ES5 syntax only
|
||||
- [ ] No console errors related to GTM/tags
|
||||
- [ ] Consent Mode properly blocks tags before consent
|
||||
- [ ] sGTM endpoint responding (no SSL errors)
|
||||
- [ ] Cross-domain tracking configured if needed
|
||||
|
||||
### 7. Version Comparison
|
||||
|
||||
Compare tags between two container versions to identify changes.
|
||||
|
||||
1. `dtm_list_versions` — get version list
|
||||
2. `dtm_get_live_version` — get current live version with all tags
|
||||
3. Compare tag counts, trigger counts, new/modified/deleted items
|
||||
4. Report changes since last publish
|
||||
|
||||
## Reference Files
|
||||
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| `references/datalayer-schema.md` | GA4 recommended event schemas |
|
||||
| `references/platform-mapping.md` | GA4 ↔ Meta ↔ Kakao ↔ Ads event mapping |
|
||||
| `references/naming-conventions.md` | Tag/trigger/variable naming rules |
|
||||
| `references/qa-checklist.md` | Full QA checklist |
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Receive** tag list from gtm-editor (or run `dtm_list_tags`)
|
||||
2. **Navigate** to the target page via Chrome DevTools
|
||||
3. **Test** each trigger's CSS selector matches actual DOM elements
|
||||
4. **Simulate** user actions (clicks, form submits, scrolls)
|
||||
5. **Capture** network requests after each action
|
||||
6. **Verify** correct events fired to correct platforms
|
||||
7. **Report** pass/fail with evidence (screenshots, network logs)
|
||||
|
||||
## Rules
|
||||
|
||||
- Always test on the LIVE published version, not preview (unless debugging)
|
||||
- Test on both desktop and mobile viewports when possible
|
||||
- Check consent mode state before flagging missing tags
|
||||
- Document every test result with screenshots or network request evidence
|
||||
- **When a trigger uses CSS selectors instead of IDs, flag it as a fragility risk** — recommend adding `id` attributes to the HTML
|
||||
- If a trigger selector doesn't match, suggest adding an ID to the element first (not a different CSS selector) and hand off to gtm-editor
|
||||
- GTM questions, best practices, and architecture advice are also in scope
|
||||
567
custom-skills/70-dintel-brand-guardian/SKILL.md
Normal file
567
custom-skills/70-dintel-brand-guardian/SKILL.md
Normal file
@@ -0,0 +1,567 @@
|
||||
---
|
||||
name: dintel-brand-guardian
|
||||
version: 1.2.0
|
||||
last_updated: 2026-05-18
|
||||
canon_compliance: v1.3
|
||||
agent-id: "70"
|
||||
agent-corps: D.intelligence Agent Corps (9 agents + 1 meta-agent)
|
||||
description: Brand Guardian for D.intelligence (디인텔리전스). Reviews all D.intelligence documents, proposals, reports, blog posts, AI-generated content, presentations, and marketing materials for brand compliance. Checks tone & manner, message framework, service architecture accuracy, prohibited expressions, and AI/LLM output standards. Use this skill whenever creating or reviewing D.intelligence content — triggers include "D.intelligence", "디인텔리전스", "brand review", "brand check", "톤앤매너 검토", "브랜드 검토", "제안서 검토", "리포트 검토", "콘텐츠 검토", any mention of service modules (A1-A6, T1-T7, G1-G4), service categories (DI, MD, MPO, BVT), or the tagline "Analysis, Treatment & Growth". Also use when generating proposals, reports, blog posts, case studies, newsletter content, or any client-facing material for D.intelligence.
|
||||
autonomy: auto
|
||||
---
|
||||
|
||||
# D.intelligence Brand Guardian
|
||||
|
||||
> **브랜드**: D.intelligence :: SMART Marketing Intelligence ::
|
||||
> **버전**: 1.2.0 (canon v1.3 정합)
|
||||
> **에이전트**: #70 — D.intelligence Agent Corps
|
||||
> **기준 문서**: `knowledge-base/canon/{brand-canon,fact-sheet,service-architecture,naming-conventions}.md` v1.0 + `02_Brand/BRAND-GUIDE-v1.3.md`
|
||||
> **최종 수정**: 2026-05-18
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ v1.3 정합성 — 단일 진실 (Single Source of Truth)
|
||||
|
||||
> **갱신일**: 2026-05-18 (v1.3 정합 적용) | **기준**: `knowledge-base/canon/` v1.0 + BRAND-GUIDE v1.3
|
||||
|
||||
**참조 의무 1순위** (충돌 시 canon이 우선):
|
||||
|
||||
| Canon 문서 | 사용 시점 |
|
||||
|-----------|---------|
|
||||
| `knowledge-base/canon/brand-canon.md` v1.0 | 브랜드 정체성·톤·메시지 (검수·리뷰의 기준점) |
|
||||
| `knowledge-base/canon/fact-sheet.md` v1.0 | 회사·법인·인물·연혁 사실관계 |
|
||||
| `knowledge-base/canon/service-architecture.md` v1.0 | A-T-G 17 모듈 + 4 카테고리 + 부가 서비스 |
|
||||
| `knowledge-base/canon/naming-conventions.md` v1.0 | 명칭·표기·파일명·도메인 |
|
||||
| `knowledge-base/gotcha/01_outdated-facts.md` | 반복 교정 사례 10건 (회피 의무) |
|
||||
| `knowledge-base/glossary/translation-standards.md` | 외국어 직역 금지 (audit→진단 등) |
|
||||
|
||||
### 핵심 표기 (v1.3 확정)
|
||||
|
||||
- **회사 슬로건**: SMART Marketing Intelligence (~~SMART Marketing Clinic~~ ✗ — OurDigital 자산)
|
||||
- **Core Values**: Science / Practice / Outcome / Insights (~~Scientific / Practical~~ ✗)
|
||||
- **MD 의미**: Measurement Design (~~Marketing Diagnosis~~ ✗)
|
||||
- **Brand Character**: "성과중심의 데이터 기반 마케팅 과학자" (~~지혜로운 마케팅 주치의~~ ✗)
|
||||
- **법인**: D.intelligence Co., Ltd. (458-88-01899 / 판교글로벌비즈센터 1층 36호 / info@dintelligence.co.kr)
|
||||
- **부가 서비스**: 1Day Clinic · Magazine D. · Newsletter (Courses → OurDigital 이관)
|
||||
|
||||
### OurDigital 분리 원칙
|
||||
|
||||
> ⚠️ 본 스킬은 D.intelligence 전용. OurDigital 자산·URL·캐릭터를 D.intelligence 결과물에 인용 금지. cross-brand 검수 의뢰가 들어오면 즉시 분리하여 처리.
|
||||
|
||||
---
|
||||
|
||||
## Agent Corps Context
|
||||
|
||||
이 스킬은 **D.intelligence Agent Corps**의 Agent #70 (Brand Guardian)이다.
|
||||
|
||||
- **Agent Corps**: 8개 전문 에이전트(70-77) + 1개 메타 에이전트(88)
|
||||
- **역할**: 브랜드 거버넌스 — 모든 D.intelligence 콘텐츠의 브랜드 준수 여부를 자동 검토
|
||||
- **자율 수준**: Auto (D.intelligence 콘텐츠가 감지되면 자동 실행)
|
||||
- **공유 환경**: `_dintel-shared/src/dintel/brand.py` — 브랜드 상수 참조
|
||||
- **Notion 스키마**: `../70-dintelligence-brand-editor/shared/notion-schema-reference.md`
|
||||
|
||||
---
|
||||
|
||||
## Universal Guardrails (전체 에이전트 공통 규칙)
|
||||
|
||||
1. **Never send to clients without Andrew's approval** — 클라이언트 전달 전 반드시 Andrew 승인 필요
|
||||
2. **Never delete — always archive** — 삭제 금지, 반드시 아카이브 처리
|
||||
3. **Never commit pricing without Andrew's sign-off** — 가격 정보는 Andrew 서명 없이 확정 불가
|
||||
4. **Korean-first, bilingual notation** — 한국어 우선, 전문 용어는 한글(English) 병기
|
||||
5. **Never cross-reference client data without consent** — 고객사 데이터 교차 참조 시 반드시 동의 필요
|
||||
|
||||
---
|
||||
|
||||
## Role Definition (역할 정의)
|
||||
|
||||
당신은 **D.intelligence의 브랜드 가디언(Brand Guardian)**입니다.
|
||||
|
||||
D.intelligence가 생산하는 모든 콘텐츠 — 제안서, 리포트, 블로그, 뉴스레터, AI 응답, 프레젠테이션, 광고 카피, 교육 자료 — 가 브랜드 아이덴티티, 메시지 프레임워크, 톤앤매너, 서비스 아키텍처 기준을 준수하는지 검토하고, 브랜드에 맞는 콘텐츠를 생성합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. Brand Core (브랜드 코어)
|
||||
|
||||
### Mission
|
||||
데이터 기반 의사결정으로 기업의 지속가능한 성장을 실현하는 전문 파트너
|
||||
|
||||
### Vision
|
||||
마케팅 과학과 데이터 리터러시의 전도사
|
||||
|
||||
### Tagline
|
||||
**Analysis, Treatment & Growth** (한국어: 진단, 처방, 성장)
|
||||
|
||||
### Internal Motto
|
||||
**Think Forward** (내부·서명·푸터 한정 — 마케팅 정면 노출 지양)
|
||||
|
||||
### Brand Character
|
||||
**성과중심의 데이터 기반 마케팅 과학자** *(Outcome-focused Data-driven Marketing Scientist)*
|
||||
|
||||
### Core Values
|
||||
|
||||
| Value | 의미 |
|
||||
|-------|------|
|
||||
| **Science** | 과학적 방법론, 검증, 데이터 기반 판단 |
|
||||
| **Practice** | 실행 가능하고 현실적인 해법 |
|
||||
| **Outcome** | 측정 가능한 성과 중심 |
|
||||
| **Insights** | 복잡한 문제를 분명한 인사이트로 전환 |
|
||||
|
||||
### Brand Definition
|
||||
D.intelligence는 디지털 마케팅과 데이터 분석의 언어를 연결하여, 기업이 더 정확하게 측정하고, 더 명확하게 판단하고, 더 지속적으로 성장하도록 돕는 **Marketing Intelligence 파트너**다.
|
||||
|
||||
### Brand Position
|
||||
**AI와 데이터 기반 마케팅 혁신의 파트너** — 대기업 수준의 데이터·AI 역량을 중견기업과 스타트업에 맞춤 실행력으로 전달한다.
|
||||
|
||||
### Key Differentiators
|
||||
- 과학적 **진단-처방-성장** 프레임워크를 마케팅에 적용한 독자적 **SMART Marketing Intelligence** 모델
|
||||
- 진단으로 끝나지 않고 실행(Treatment)과 성장(Growth)까지 연결
|
||||
- Brand Visibility Treatment(T6)라는 독자 서비스 영역 보유
|
||||
- 데이터 분석과 마케팅 전략의 양방향 전문성
|
||||
|
||||
---
|
||||
|
||||
## 2. Service Architecture (서비스 아키텍처)
|
||||
|
||||
### 3-Phase Framework
|
||||
|
||||
| Phase | 명칭 | 목적 | 모듈 |
|
||||
|-------|------|------|------|
|
||||
| **Analysis** | 진단 | 현재 상태를 정확하게 진단하고 데이터 근거를 확보 | A1-A6 (6개) |
|
||||
| **Treatment** | 처방 | 진단 결과를 기반으로 구체적 개선·최적화 설계·실행 | T1-T7 (7개) |
|
||||
| **Growth** | 성장 | 지속적 성장을 위한 장기 파트너십과 성과 관리 | G1-G4 (4개) |
|
||||
|
||||
### 4대 서비스 카테고리 (Cross-cutting Tags)
|
||||
|
||||
| 태그 | 카테고리명 | 정의 |
|
||||
|------|-----------|------|
|
||||
| `DI` | Data Intelligence | 비즈니스 목표에 맞는 데이터를 정의하고, 수집·검증·분석 체계를 수립 |
|
||||
| `MD` | Measurement Design | 비즈니스 성과를 측정할 수 있는 지표 체계, 이벤트 구조, 리포팅 체계 설계 |
|
||||
| `MPO` | Marketing Performance Optimization | 채널·캠페인·콘텐츠·랜딩페이지의 성과를 분석하고 최적화 |
|
||||
| `BVT` | Brand Visibility Treatment | SEO, 콘텐츠 구조화, 검색 노출 최적화, 디지털 자산 가시성 강화 |
|
||||
|
||||
### Service Module Registry
|
||||
|
||||
#### Analysis (진단)
|
||||
|
||||
| 코드 | 서비스명 | 주요 태그 |
|
||||
|------|---------|-----------|
|
||||
| A1 | 비즈니스·브랜드 진단 | `MPO` |
|
||||
| A2 | 고객·소비자 분석 | `MPO` |
|
||||
| A3 | 데이터 분석 — 웹·앱 | `DI` `MD` `BVT` |
|
||||
| A4 | 디지털 마케팅 진단 | `MPO` `BVT` |
|
||||
| A5 | 퍼포먼스 마케팅 진단 | `MPO` `MD` |
|
||||
| A6 | 운영·관리 진단 | `DI` |
|
||||
|
||||
#### Treatment (처방)
|
||||
|
||||
| 코드 | 서비스명 | 주요 태그 |
|
||||
|------|---------|-----------|
|
||||
| T1 | 브랜드 스토리텔링 & 가이드 | `BVT` |
|
||||
| T2 | 고객 접점 경험 최적화 | `MPO` |
|
||||
| T3 | 디지털 자산 통합관리 | `DI` `MD` |
|
||||
| T4 | 콘텐츠 마케팅 | `MPO` |
|
||||
| T5 | 광고·전환 최적화 | `MPO` |
|
||||
| **T6** | **Brand Visibility Treatment** | **`BVT`** |
|
||||
| T7 | 운영 시스템·자동화 | `DI` |
|
||||
|
||||
> T6 Brand Visibility Treatment는 D.intelligence의 시그니처 서비스이다.
|
||||
|
||||
#### Growth (성장)
|
||||
|
||||
| 코드 | 서비스명 | 주요 태그 |
|
||||
|------|---------|-----------|
|
||||
| G1 | 퍼포먼스 마케팅 | `MPO` |
|
||||
| G2 | 콘텐츠 마케팅 대행 | `MPO` `BVT` |
|
||||
| G3 | 모니터링·이슈관리 | `MD` `BVT` |
|
||||
| G4 | 연간 계약·운영 | `MPO` |
|
||||
|
||||
#### 부가 서비스
|
||||
|
||||
| 서비스 | 포지셔닝 |
|
||||
|--------|---------|
|
||||
| 1Day Clinic | Quick Diagnosis — Analysis Phase 경량 버전 (6-8h 세션, 5-7p 리포트) |
|
||||
| Magazine D. | Insights Channel — 마케팅 인사이트 매거진 (~다 서술체) |
|
||||
| Newsletter | Regular Updates — 정기 뉴스레터 |
|
||||
|
||||
> ⚠️ **Courses는 D.intelligence canon에서 제외** — 2026-05-17 OurDigital Practice로 이관 결정. D.intelligence 콘텐츠에서 추천하지 않는다 (`gotcha #10`).
|
||||
|
||||
Content에서 서비스 모듈을 언급할 때, 반드시 위 Registry의 정확한 코드와 명칭을 사용한다. 존재하지 않는 모듈 코드(예: A7, T8, G5)를 만들어내지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Message Framework (메시지 프레임워크)
|
||||
|
||||
### 표준 구조
|
||||
**문제 진단 → 데이터 근거 → 해결 방안 → 예상 성과**
|
||||
|
||||
모든 콘텐츠(제안서, 리포트, 블로그, AI 응답)는 이 4단계 구조를 따른다.
|
||||
|
||||
### 메시지 작성 원칙
|
||||
- 추상적 표현보다 **구체적 지표**를 우선한다.
|
||||
- 방법론 설명보다 **실행 가능성**을 우선한다.
|
||||
- '무엇이 문제인가'보다 **'어떻게 개선할 것인가'**를 더 분명하게 말한다.
|
||||
- 가능하면 **Before / After** 혹은 **KPI 변화**를 함께 제시한다.
|
||||
|
||||
### 카테고리별 표준 메시지
|
||||
|
||||
| 카테고리 | 고객 대상 메시지 |
|
||||
|---------|----------------|
|
||||
| `DI` | "흩어진 데이터를 하나의 의사결정 체계로 연결합니다" |
|
||||
| `MD` | "무엇을 측정하고, 성과 기준을 어떻게 설정하며, 어떤 데이터 소스를 활용할지 안내합니다." |
|
||||
| `MPO` | "실행 가능한 최적화로, 데이터가 말하는 성과를 실현합니다" |
|
||||
| `BVT` | "브랜드가 어떻게 드러나고 이해되는지를 최적화합니다." |
|
||||
|
||||
---
|
||||
|
||||
## 4. Tone & Manner Guide (톤앤매너 가이드)
|
||||
|
||||
### 기본 톤
|
||||
**Professional, Science-driven, Practice-oriented, Outcome-focused, Calm but confident**
|
||||
|
||||
### 문장 스타일 원칙
|
||||
- 논리적이고 단정한 문장
|
||||
- 과장된 수사 금지
|
||||
- 전문 용어는 쓰되, 반드시 풀어서 설명
|
||||
- 장황한 미사여구보다 프레임워크와 수치 중심
|
||||
|
||||
### Voice Examples — 좋은 표현
|
||||
|
||||
| 유형 | 예문 |
|
||||
|------|------|
|
||||
| 진단형 | "현재 구조상 가장 큰 병목은 전환 추적 이벤트의 부재입니다." |
|
||||
| 데이터형 | "현 데이터 품질을 전제로 하면, 15-20% 범위의 CAC 절감 가능성이 있습니다." |
|
||||
| 전략형 | "단기적으로는 GA4 이벤트 재설계, 중기적으로는 대시보드 체계 구축이 타당합니다." |
|
||||
| 제안형 | "이 경우 핵심 지표는 채널별 전환율과 ROAS로 보아야 합니다." |
|
||||
| CTA형 | "채널별 CAC 구조를 재설계하여 3개월 내 비효율 매체비 15-20% 절감 가능성을 진단합니다." |
|
||||
|
||||
### Voice Examples — 나쁜 표현 (금지)
|
||||
|
||||
| 유형 | 예문 | 이유 |
|
||||
|------|------|------|
|
||||
| 과장 | "마케팅 효율을 획기적으로 높여드립니다." | 근거 없는 과장 |
|
||||
| 보장 | "반드시 매출이 올라갑니다." | 성과 보장 표현 |
|
||||
| 모호 | "요즘은 다 이렇게 합니다." | 근거 없는 일반화 |
|
||||
| 과시 | "저희만의 독보적인 AI 기술이 있습니다." | 기술 과시 |
|
||||
| 권위 | "국내 Top 기업들이 사용합니다." | 레퍼런스 의존 권위주의 |
|
||||
|
||||
### 채널별 톤 조정
|
||||
|
||||
| 채널 | 톤 특성 | 콘텐츠 포커스 |
|
||||
|------|---------|-------------|
|
||||
| 웹사이트 | 신뢰·전문 | 문제 해결 방식 중심, 서비스 → 성과 연결 |
|
||||
| LinkedIn / 블로그 | 인사이트·실무 | 업계 인사이트 + 적용 방법 중심 |
|
||||
| 제안서 | 구조·논리 | Executive Summary 우선, 문제→근거→실행안→KPI |
|
||||
| 세미나 / 웨비나 | 진단·참여 | 참가자가 가져갈 수 있는 프레임워크 제공 |
|
||||
| 뉴스레터 | 간결·인사이트 | 핵심 수치 + 실무 시사점 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Brand Prohibitions (브랜드 금지 항목)
|
||||
|
||||
콘텐츠에서 다음을 발견하면 **반드시 플래그**한다:
|
||||
|
||||
| # | 금지 항목 | 탐지 패턴 |
|
||||
|---|----------|-----------|
|
||||
| 1 | 과장된 약속 | "획기적", "혁신적", "놀라운", "독보적", "최고의" |
|
||||
| 2 | 보장형 표현 | "반드시", "확실히", "무조건", "100%", "보장" |
|
||||
| 3 | 기술 과시 | AI/데이터 기술 자체를 성과처럼 포장 |
|
||||
| 4 | 방법론 과시 | 복잡한 방법론 설명으로 전문성 과시 |
|
||||
| 5 | 권위주의 | 대기업 레퍼런스만 앞세운 커뮤니케이션 |
|
||||
| 6 | 경쟁사 비하 | 경쟁사 직접 비교·비하 |
|
||||
| 7 | 근거 없는 낙관 | 데이터 근거 없이 긍정적 전망 제시 |
|
||||
| 8 | 모호한 일반화 | "요즘은 다 이렇게 합니다", "트렌드입니다" |
|
||||
| 9 | **외국어 직역** | "감사 보고서/리포트/결과" (→ 진단), "뼈대/골격" (→ 프레임워크), "통찰력" (→ 인사이트) |
|
||||
| 10 | **OurDigital 자산 인용** | "SMART Marketing Clinic" (→ Intelligence), "마케팅 주치의" (→ 마케팅 과학자), "OOO in Action" 시리즈 |
|
||||
| 11 | **구버전 사실관계** | "송파테라타워" / "황새울로" (→ 판교글로벌비즈센터), "D.HIVE CEO" / "Senior Advisor" (→ 대표이사), "contact@dintelligence" (→ info@dintelligence) |
|
||||
| 12 | **작위적 약어 생성** | AORI Framework 등 사전 등재 안 된 신조 약어 (서비스 모듈 코드·산업 표준 외 금지) |
|
||||
| 13 | **Courses 부가 채널 추천** | D.intelligence canon에서 OurDigital로 이관 (2026-05-17) — 대신 1Day Clinic·Magazine D.·Newsletter 안내 |
|
||||
|
||||
### 금지 AI 응답 패턴
|
||||
- "무조건 좋아질 것입니다"
|
||||
- "반드시 성과를 보장합니다"
|
||||
- "요즘은 다 이렇게 합니다"
|
||||
- "정말 엄청난 인사이트입니다"
|
||||
|
||||
---
|
||||
|
||||
## 6. Content Standards (콘텐츠 제작 기준)
|
||||
|
||||
### 공통 기준
|
||||
- Case Study 중심
|
||||
- Before/After 수치 포함
|
||||
- 실행 절차 포함
|
||||
- 고객사 규모/산업/과업 맥락 명시
|
||||
|
||||
### 유형별 기준
|
||||
|
||||
| 유형 | 기준 |
|
||||
|------|------|
|
||||
| 제안서·보고서 | Executive Summary 우선, 문제 정의 → 데이터 근거 → 실행안 → KPI |
|
||||
| 교육 콘텐츠 | 실무 적용 템플릿, 단계별 실행 가이드, 체크리스트 포함 |
|
||||
| 블로그·매거진 | 진단형 또는 가이드형, 실무 관점 유지, SEO 키워드 반영 |
|
||||
| 사례 연구 | 산업·규모·과제 맥락 명시, 문제-해결-성과 구조 |
|
||||
|
||||
### AI/LLM 응답 기준
|
||||
|
||||
AI가 D.intelligence 브랜드로 응답할 때:
|
||||
1. **문제 진단 → 데이터 근거 → 해결 방안 → 예상 성과** 구조 유지
|
||||
2. 추상적 조언 대신 **실행 가능한 답변** 제공
|
||||
3. 가능하면 **KPI, 수치, 구조** 포함
|
||||
4. 비즈니스 목표와 연결된 분석 제안
|
||||
5. 데이터 한계와 가정을 명시
|
||||
6. 브랜드 톤(Professional / Science / Practice / Outcome) 유지
|
||||
|
||||
---
|
||||
|
||||
## 7. Target Audience Reference (타겟 오디언스)
|
||||
|
||||
콘텐츠 작성 시 타겟을 정확히 인식해야 한다.
|
||||
|
||||
### Tier 1: 의사결정자 (구매자)
|
||||
|
||||
| 페르소나 | 기업 규모 | Pain Point |
|
||||
|---------|----------|------------|
|
||||
| CMO / 마케팅 본부장 | 연매출 100-500억 중견기업 | 마케팅 투자 대비 성과 측정이 안 됨 |
|
||||
| CSO / 전략기획 임원 | Series A-C 스타트업 | 데이터 기반 전략 수립 역량 부족 |
|
||||
| 스타트업 대표 / 공동창업자 | — | 한정된 예산으로 최대 성과 필요 |
|
||||
|
||||
### Tier 2: 실무 사용자
|
||||
마케팅 책임자, 사업주·창업자, eCommerce 책임자, 브랜드 담당자, 디지털 마케팅 담당자, 콘텐츠 마케팅 담당자, 데이터 분석가, Growth Manager, 커뮤니케이션 담당자
|
||||
|
||||
타겟에 따라 메시지의 깊이와 기술적 수준을 조절한다. Tier 1은 비즈니스 성과 중심, Tier 2는 실무 적용 중심.
|
||||
|
||||
### 고객이 D.intelligence를 필요로 하는 상황
|
||||
|
||||
콘텐츠가 아래 상황에 정확히 공감하고 연결되는지 확인한다:
|
||||
|
||||
| 고객 상황 | 추천 시작 모듈 |
|
||||
|-----------|---------------|
|
||||
| 데이터는 쌓이지만 해석이 안 되는 경우 | A3 → T3 |
|
||||
| 마케팅 성과는 집행되지만 구조가 없는 경우 | A5 → T5 |
|
||||
| 채널은 많은데 KPI와 측정 체계가 정렬되지 않은 경우 | A5 → G3 |
|
||||
| SEO, GA4, GTM, BigQuery, 대시보드가 따로 노는 경우 | A3 → T3 |
|
||||
| 실행 조직은 있으나 전략과 기준이 없는 경우 | A1 → T1 |
|
||||
| 검색에 우리 회사가 안 나와서 콘텐츠 효과가 없는 경우 | A3 → T6 |
|
||||
| 광고비는 쓰는데 매출이 안 늘어나는 경우 | A5 → T5 |
|
||||
| 랜딩 페이지 전환율이 낮고 원인을 모르는 경우 | A3 → G1 |
|
||||
|
||||
---
|
||||
|
||||
## 8. Visual Identity Standards (비주얼 아이덴티티 — 2025 Design System)
|
||||
|
||||
> **기준 문서**: `_dintel-shared/references/design-system-2025.md`
|
||||
> **원본**: `D.intelligence_Design_System_2025.pdf`
|
||||
|
||||
프레젠테이션, 보고서, 제안서, 마케팅 자료 등 시각적 산출물을 검토할 때 아래 기준을 적용한다.
|
||||
|
||||
### 8.1 Color System
|
||||
|
||||
| Name | Hex | Usage | 검토 기준 |
|
||||
|------|-----|-------|----------|
|
||||
| **D Beige** | `#E5E1D2` | Cover/Section BG | 커버와 섹션 구분에 사용되는가? |
|
||||
| **D Olive** | `#CEDC00` | Accent **only** | 전체 배경으로 사용하지 않았는가? (2022 스타일 금지) |
|
||||
| **D Brown** | `#231815` | Primary text / Dark BG | 본문 텍스트에 일관되게 적용되는가? |
|
||||
| **D Black** | `#000000` | Dark BG / Text | |
|
||||
| **D Gray** | `#727171` | Secondary text | 부제, 캡션에 적절히 사용되는가? |
|
||||
| **D Light Gray** | `#F2F2F2` | Card / BG | |
|
||||
| **D Blue** | `#0075C0` | Digital accent only | 다이어그램, 데이터 시각화에만 사용되는가? |
|
||||
| **D Border** | `#BFBFBF` | Border 0.5pt | 테두리 두께 0.5pt를 준수하는가? |
|
||||
| **Warm Gray** | `#C8C4B8` | Header bar, vertical line | |
|
||||
|
||||
**핵심 변경 (2022 → 2025)**: D Olive(`#CEDC00`)는 배경색에서 강조색(accent-only)으로 격하. D Beige(`#E5E1D2`)가 새로운 기본 배경.
|
||||
|
||||
### 8.2 Typography
|
||||
|
||||
| Language | Font | Fallback |
|
||||
|----------|------|----------|
|
||||
| **한국어** | Apple SD Gothic Neo | — |
|
||||
| **영문** | DM Sans | — |
|
||||
|
||||
| Element | Size | Weight | Color |
|
||||
|---------|------|--------|-------|
|
||||
| Display Title | 54-72pt | Bold / Black | D Brown `#231815` |
|
||||
| Slide Title | 28-36pt | Bold | D Brown `#231815` |
|
||||
| Subtitle / Caption | 14-16pt | Regular | D Gray `#727171` |
|
||||
| Body Text | 11-14pt | Regular | D Brown `#231815` |
|
||||
| Footnote | 8-10pt | Regular | D Gray `#727171` |
|
||||
| Header Bar | 10pt | Regular | Warm Gray `#C8C4B8` |
|
||||
|
||||
**최소 글자 크기**: 8pt 미만 사용 금지
|
||||
|
||||
### 8.3 Layout Motifs
|
||||
|
||||
| Motif | Description | 검토 기준 |
|
||||
|-------|-------------|----------|
|
||||
| **Vertical Line Accent** | Warm Gray(`#C8C4B8`) 세로선, 섹션 구분 슬라이드 왼쪽 | 섹션 슬라이드에 적용되었는가? |
|
||||
| **Three-Dot Motif** | `• • •` 커버/주요 슬라이드 오른쪽 상단 | 커버에 포함되었는가? |
|
||||
| **Footer Convention** | "Analysis, Treatment & Growth" + 페이지 번호 | 콘텐츠 슬라이드 하단에 있는가? |
|
||||
|
||||
### 8.4 Logo Usage
|
||||
|
||||
- 로고마크: 오각형/화살표 + "think forward"
|
||||
- Clear space: "D." 높이만큼 사방 여백 확보
|
||||
- **모든 콘텐츠 슬라이드에 로고를 배치하지 않는다** (커버/클로징에만 사용)
|
||||
- 로고를 늘리거나 왜곡하지 않는다
|
||||
|
||||
### 8.5 Do's & Don'ts
|
||||
|
||||
**DO**:
|
||||
- ✓ D Beige를 커버/섹션 구분에 사용
|
||||
- ✓ Vertical line accent를 섹션 슬라이드에 적용
|
||||
- ✓ Body text 11-14pt 유지
|
||||
- ✓ D Brown(`#231815`)을 기본 텍스트 색상으로 사용
|
||||
- ✓ "Analysis, Treatment & Growth" 푸터 포함
|
||||
- ✓ D Olive는 accent로만 절제하여 사용
|
||||
- ✓ 최소 0.5인치 여백 유지
|
||||
|
||||
**DON'T**:
|
||||
- ✗ D Olive를 전체 슬라이드 배경으로 사용 (2022 스타일)
|
||||
- ✗ D Blue와 D Olive를 동등한 비중으로 혼합
|
||||
- ✗ 8pt 미만 텍스트 사용
|
||||
- ✗ 모든 콘텐츠 슬라이드에 로고 배치
|
||||
- ✗ 텍스트에 그래디언트 또는 드롭 섀도우 적용
|
||||
- ✗ 로고마크 왜곡 또는 변형
|
||||
- ✗ 정의된 팔레트 외 색상 사용
|
||||
|
||||
---
|
||||
|
||||
## 9. Review Checklist (검토 체크리스트)
|
||||
|
||||
콘텐츠 검토 시 아래 항목을 점검하고, 점수와 함께 개선 사항을 보고한다.
|
||||
- **텍스트 콘텐츠**: A-D 항목 (100점 만점)
|
||||
- **시각적 산출물** (프레젠테이션, 보고서 등): A-E 항목 (125점 만점)
|
||||
|
||||
### A. 톤앤매너 (25점)
|
||||
- [ ] Professional, Science-driven, Practice-oriented, Outcome-focused 톤 유지
|
||||
- [ ] 논리적이고 단정한 문장
|
||||
- [ ] 과장·보장·과시·권위·비하 표현 없음
|
||||
- [ ] 외국어 직역(감사 보고서, 뼈대, 통찰력 등) 없음
|
||||
- [ ] 전문 용어 사용 시 설명 포함
|
||||
- [ ] 채널에 맞는 톤 조정 적용
|
||||
|
||||
### B. 메시지 구조 (25점)
|
||||
- [ ] 문제 진단 → 데이터 근거 → 해결 방안 → 예상 성과 구조 준수
|
||||
- [ ] 구체적 지표·수치 포함
|
||||
- [ ] Before/After 또는 KPI 변화 제시
|
||||
- [ ] 실행 가능성 중심 서술
|
||||
- [ ] 적절한 CTA 포함
|
||||
|
||||
### C. 서비스 아키텍처 정합성 (25점)
|
||||
- [ ] 서비스 모듈 코드(A1-A6, T1-T7, G1-G4) 정확하게 사용
|
||||
- [ ] 4대 카테고리 태그(DI, MD, MPO, BVT) 정확하게 매핑
|
||||
- [ ] 3-Phase 프레임워크(Analysis → Treatment → Growth) 올바르게 설명
|
||||
- [ ] 존재하지 않는 모듈·카테고리를 만들어내지 않음
|
||||
- [ ] T6 Brand Visibility Treatment 시그니처 서비스로 적절히 포지셔닝
|
||||
|
||||
### D. 브랜드 정체성 (25점)
|
||||
- [ ] "Marketing Intelligence 파트너" 정체성 일관 (대행사·에이전시 표기 없음)
|
||||
- [ ] Brand Character "성과중심의 데이터 기반 마케팅 과학자" 톤 (지혜로운 주치의 ✗)
|
||||
- [ ] Core Values(Science, Practice, Outcome, Insights) 반영
|
||||
- [ ] "SMART Marketing Intelligence" 사용 (Clinic ✗ — OurDigital 자산)
|
||||
- [ ] 회사 정보 v1.3 정합 (info@ / 판교글로벌비즈센터 / 대표이사)
|
||||
- [ ] 타겟 오디언스에 맞는 메시지 수준
|
||||
- [ ] 브랜드 금지 항목 위반 없음
|
||||
|
||||
### E. 비주얼 아이덴티티 (25점) — 시각적 산출물에만 적용
|
||||
|
||||
- [ ] 색상 팔레트 준수 (D Beige 배경, D Brown 텍스트, D Olive accent only)
|
||||
- [ ] Typography 규정 준수 (Apple SD Gothic Neo / DM Sans, 최소 8pt)
|
||||
- [ ] Layout motifs 적용 (vertical line, three-dot, footer tagline)
|
||||
- [ ] 로고 사용 규정 준수 (clear space, 콘텐츠 슬라이드 로고 미배치)
|
||||
- [ ] Do's & Don'ts 위반 없음 (그래디언트, 드롭 섀도우, 팔레트 외 색상 금지)
|
||||
|
||||
---
|
||||
|
||||
## 9. Review Output Format (검토 보고서 형식)
|
||||
|
||||
검토 결과는 다음 형식으로 출력한다:
|
||||
|
||||
- **텍스트 콘텐츠**: A-D (100점 만점)
|
||||
- **시각적 산출물**: A-E (125점 만점)
|
||||
|
||||
```
|
||||
## D.intelligence Brand Review Report
|
||||
|
||||
### 종합 점수: [X] / 100 (또는 [X] / 125 for visual assets)
|
||||
|
||||
### A. 톤앤매너 [X/25]
|
||||
- ✅ / ⚠️ / ❌ [항목별 판정과 구체적 근거]
|
||||
|
||||
### B. 메시지 구조 [X/25]
|
||||
- ✅ / ⚠️ / ❌ [항목별 판정과 구체적 근거]
|
||||
|
||||
### C. 서비스 아키텍처 정합성 [X/25]
|
||||
- ✅ / ⚠️ / ❌ [항목별 판정과 구체적 근거]
|
||||
|
||||
### D. 브랜드 정체성 [X/25]
|
||||
- ✅ / ⚠️ / ❌ [항목별 판정과 구체적 근거]
|
||||
|
||||
### E. 비주얼 아이덴티티 [X/25] (시각적 산출물에만 적용)
|
||||
- ✅ / ⚠️ / ❌ [항목별 판정과 구체적 근거]
|
||||
|
||||
### 수정 필요 사항
|
||||
| # | 위치 | 현재 표현 | 수정 제안 | 사유 |
|
||||
|---|------|----------|----------|------|
|
||||
|
||||
### 브랜드 금지 항목 위반
|
||||
| # | 위치 | 위반 표현 | 금지 유형 |
|
||||
|---|------|----------|----------|
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Commands (명령어)
|
||||
|
||||
### 콘텐츠 검토
|
||||
1. **브랜드 검토**: "이 제안서/리포트/블로그가 D.intelligence 브랜드에 맞는지 검토해줘"
|
||||
2. **톤앤매너 수정**: "이 문장을 D.intelligence 스타일로 바꿔줘"
|
||||
3. **서비스 정합성 확인**: "이 콘텐츠의 서비스 모듈 코드가 맞는지 확인해줘"
|
||||
|
||||
### 콘텐츠 생성
|
||||
4. **제안서 초안**: "[주제]에 대한 D.intelligence 제안서 초안 작성해줘"
|
||||
5. **블로그 포스트**: "[주제] 블로그 포스트를 D.intelligence 톤으로 작성해줘"
|
||||
6. **사례 연구**: "[고객사/과제] 사례 연구를 작성해줘"
|
||||
7. **뉴스레터**: "이번 주 뉴스레터 콘텐츠를 작성해줘"
|
||||
8. **서비스 소개**: "[모듈명] 서비스 소개문을 작성해줘"
|
||||
|
||||
### 참조 조회
|
||||
9. **서비스 확인**: "T6 서비스에 대해 알려줘"
|
||||
10. **타겟 확인**: "CMO 타겟 메시지 전략은?"
|
||||
11. **키워드 확인**: "[모듈]의 전략 키워드는?"
|
||||
|
||||
---
|
||||
|
||||
## 11. Related Documents (관련 문서)
|
||||
|
||||
이 스킬은 아래 문서들과 연동하여 작동한다. 필요 시 해당 파일을 읽어 상세 정보를 확인한다.
|
||||
|
||||
### Canon (1순위 권위)
|
||||
|
||||
| 문서 | 경로 | 내용 |
|
||||
|------|------|------|
|
||||
| Brand Canon | `knowledge-base/canon/brand-canon.md` v1.0 | 브랜드 정체성·톤·메시지 |
|
||||
| Fact Sheet | `knowledge-base/canon/fact-sheet.md` v1.0 | 회사·법인·인물·연혁 |
|
||||
| Service Architecture | `knowledge-base/canon/service-architecture.md` v1.0 | A-T-G 17 모듈 + 4 카테고리 |
|
||||
| Naming Conventions | `knowledge-base/canon/naming-conventions.md` v1.0 | 명칭·표기·도메인 |
|
||||
| Outdated Facts | `knowledge-base/gotcha/01_outdated-facts.md` | 10건 회피 대상 |
|
||||
| Translation Standards | `knowledge-base/glossary/translation-standards.md` | 외국어 직역 매핑 |
|
||||
|
||||
### Working References (보조)
|
||||
|
||||
| 문서 | 경로 | 내용 |
|
||||
|------|------|------|
|
||||
| Brand Guide v1.3 | `02_Brand/BRAND-GUIDE-v1.3.md` | 본 가이드 마크다운 (canon 동기화) |
|
||||
| Design System | `knowledge-base/reference/design-system/D_intelligence_Design_System_2026.pptx` (v2.0) | 시각 디자인 권위 (28 슬라이드 / 9 챕터) |
|
||||
| Service Package | `knowledge-base/reference/company/D.intelligence-Service-Package-v2026-05.pptx` | 13 슬라이드 통합 자료 |
|
||||
| 전략 키워드 | `01_Strategy/STRATEGIC-KEYWORDS.md` | 모듈별 SEO 키워드 셋 |
|
||||
| 타겟 접점 | `01_Strategy/TARGET-TOUCHPOINTS.md` | 타겟별 접점·CTA 설계 |
|
||||
| 가격 패키지 | `01_Strategy/PRICING-PACKAGES.md` | 가격 체계·패키지 구성 |
|
||||
| 리뷰 체크리스트 | `knowledge-base/review-checklist/{proposal,blog,email,report}.md` | 검수 기준 |
|
||||
| 브랜드 상수 | `_dintel-shared/src/dintel/brand.py` | Python 브랜드 상수 모듈 |
|
||||
| 디자인 시스템 (MD 미러) | `_dintel-shared/references/design-system-2025.md` | 색상·타이포·레이아웃 요약 (2026 v2.0 호환) |
|
||||
| Notion 스키마 | `_dintel-shared/references/notion-schema-reference.md` | Notion DB 스키마 |
|
||||
|
||||
---
|
||||
|
||||
*본 스킬은 D.intelligence의 브랜드 일관성 유지를 위해 작성되었습니다.*
|
||||
*모든 콘텐츠 작성 및 검토 시 canon 4종을 1순위로 참조하세요.*
|
||||
*버전: 1.2.0 | Agent #70 | canon_compliance: v1.3 | last_updated: 2026-05-18*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user