Compare commits

...

55 Commits

Author SHA1 Message Date
f27fb7a2c4 Add jamie-copy-trimmer (48) and dintel-campaign-designer (78) skills
Some checks failed
Verify Skills / verify-skills (push) Has been cancelled
Import two skills authored in Claude Cowork: jamie-copy-trimmer as a
root-only Jamie skill, and campaign-gate-process as dintel-campaign-designer
with full D.intelligence Agent Corps packaging (code/desktop/shared,
agent-id 78, Draft & Wait autonomy) since it now sits inside the 70-79
block. Bumps the corps roster count to 9 agents + 1 meta-agent across all
sibling skills and registers #78 in the shared USER-GUIDE/README. Also adds
.claude/commands wrappers and ~/.claude/skills symlinks so both are usable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 12:59:19 +09:00
Andrew Yim
a08dc316be Merge pull request #14 from ourdigital/fix/notion-writer-skill-paths
fix(notion-writer): correct stale 02→32 skill paths
2026-06-27 11:42:19 +09:00
dc18594d76 fix(notion-writer): correct stale 02→32 skill paths after dir renumber
The 02-notion-writer dir was renumbered to 32-notion-writer, but the
skill's SKILL.md (both code + desktop variants) still pointed venv/cd
paths at the nonexistent 02 dir, and the slash-command file had a
~/Projects (should be ~/Project) typo. Fixes invocation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:41:39 +09:00
Andrew Yim
0b3a0ab129 Merge pull request #13 from ourdigital/notion-writer-cli-enhancements
feat(notion-writer): file uploads + native markdown engine (v1.3.0)
2026-06-27 11:27:11 +09:00
5c904756ab fix(ntn-files): propagate NOTION_API_KEY into ntn subprocesses
Force ntn to authenticate as the same integration that writes the page by
injecting NOTION_API_TOKEN=$NOTION_API_KEY into every ntn subprocess env.
Notion scopes file uploads to the creating integration, so a mismatch caused
"Could not find file_upload with ID …" when attaching uploaded images.

Added test_upload_passes_token_env (9 total in test_ntn_files.py) and updated
code/CLAUDE.md to reflect that a separate ntn login is no longer required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
07fca7fb81 fix(notion-writer): fence-aware image/translate, NtnUploadError boundary, dead-code removal
FIX 1: _content_has_local_images and extract_local_images now skip lines inside
  ``` fences so a local-image ref inside a code block is not treated as real.
FIX 2: md_translate.translate() tracks fence state; callout/columns/mention
  transforms are suppressed for lines inside ``` ... ``` blocks (pass verbatim).
FIX 3: main() catches NtnUploadError and prints a clean error + e.stderr before
  sys.exit(1); body moved to _main_body(parser, args).
FIX 4: ntn_files.upload() raises NtnUploadError on empty stdout instead of
  IndexError, giving a clean message combined with FIX 3.
FIX 5: Removed unused write_to_page() (both page paths are inlined).
FIX 6: create_page_markdown() omits the 'markdown' key when value is falsy,
  avoiding empty-markdown sends on DB rows created without --file/--stdin.
FIX 7: CLAUDE.md documents --allow-deleting-content scope (markdown engine only).
TDD: Added test_fence_passthrough_no_transform (md_translate, suite now 8) and
  test_local_image_inside_fence_ignored (engine_routing, suite now 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
00edd2e5dc docs(notion-writer): document engines + file uploads (v1.3.0)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
478ac61975 fix(notion-writer): fail loudly on markdown image append + upsert prop update
Mirror the blocks path's error handling in the markdown engine:
- page + DB markdown two-phase image append now check append_to_page's
  return and exit(1) on failure instead of printing success silently
- markdown DB upsert guards update_page_properties return
- restore blocks-engine page progress print + replace-failure print
- move unused parent computation into the create branch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
5ff9b3d9f5 feat(notion-writer): --engine routing + two-phase image append
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
cfbca6cc15 feat(notion-writer): markdown endpoint helpers + version-aware client
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
dfbc52e531 feat(notion-writer): dialect->Notion enhanced markdown translator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
337f2ad6e1 feat(notion-writer): materialize local images via upload walk
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
9fbd719048 feat(notion-writer): parse standalone images to image blocks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
5385f3bddd feat(notion-writer): ntn_files upload + preflight
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
75cfcb9ad3 docs(notion-writer): fix Task 2 upload tests to mock builtins.open
upload() opens the file before subprocess.run; the two upload tests must
mock open() so the nonexistent /tmp paths don't raise FileNotFoundError.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:31 +09:00
bfade2b722 docs(notion-writer): fix plan — circular import, columns loop, error hint
Pre-flight review fixes: lazy import in md_translate to break the
notion_writer<->md_translate cycle; clean columns state machine; markdown
deletion hint in explain_api_error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:12:48 +09:00
2b36feb81b docs(notion-writer): implementation plan for CLI enhancements
9 TDD tasks: venv baseline, ntn_files (upload+preflight), image parsing,
materialize walk, md_translate dialect translator, compat markdown
helpers + version-aware client, CLI engine routing + two-phase image
append, docs (v1.3.0), live smoke test. 55 unit tests across 4 suites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:12:48 +09:00
aa003e28cc docs(notion-writer): design spec for CLI file uploads + markdown engine
Tier 1: local ![](path) images uploaded via `ntn files create` and
embedded as file_upload image blocks (parser stays pure; upload happens
in an isolated post-parse walk).

Tier 2: opt-in `--engine markdown` writes through Notion's native
enhanced-markdown endpoints (POST /pages, PATCH .../markdown) with a
dialect translator (callouts/columns/toggles/mentions) so one source
doc works in both engines. Default engine stays `blocks` (no regression).

Adds ntn_files.py + md_translate.py, version-aware client (2026-03-11
for markdown writes only), new test suites, venv setup, docs → v1.3.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:12:48 +09:00
87635e4208 fix(skill): absent claim term -> INCONCLUSIVE hint; note surge-tuning (final-review minors #3,#4)
- gsc_signal_delta.py: extract `found` local var; add first branch in verdict_hint
  chain so a term absent from both GSC windows yields INCONCLUSIVE (not ARTIFACT).
  Existing ARTIFACT / CONFIRMED-PARTIAL / PARTIAL branches unchanged (elif chain).
- test_gsc_signal_delta.py: add test_absent_claim_term_inconclusive asserting
  found=False and "INCONCLUSIVE" in verdict_hint for a term in neither fixture.
- code/CLAUDE.md: one-line surge-tuning note — verdict_hint/in_top_movers are
  calibrated for upward claims; for drops, inspect top_decliners directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuT3W81t88QQFaxY2ruWv2
2026-06-26 10:38:14 +09:00
4f78534e59 test(skill): genesis 호텔 smoke fixtures + end-to-end ARTIFACT check 2026-06-26 10:29:35 +09:00
d20675d02c feat(skill): register seo-signal-validation in marketplace; reconcile spec layout 2026-06-26 10:25:46 +09:00
2cadc30825 feat(skill): gsc_signal_delta helper + tests + code notes 2026-06-26 10:21:36 +09:00
c35250f06a feat(skill): seo-signal-validation SKILL.md decision half (L3-L4, verdict, output) 2026-06-26 10:16:11 +09:00
3383878e12 feat(skill): seo-signal-validation SKILL.md measurement half (L1-L2) 2026-06-26 10:10:47 +09:00
f953887b97 docs(skill): add 35-seo-signal-validation implementation plan
5 bite-sized tasks: SKILL.md measurement half (L1-L2) + decision half
(L3-L4/verdict), gsc_signal_delta.py helper with TDD tests, marketplace
registration + spec-layout reconcile, and a genesis-case smoke test that
asserts the JHR "호텔" claim resolves to ARTIFACT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuT3W81t88QQFaxY2ruWv2
2026-06-26 10:05:57 +09:00
18b39ef6ea docs(skill): add 35-seo-signal-validation design spec
New conductor skill that adjudicates whether a claimed SERP/Knowledge-Graph
movement for a (term, entity) pair is real, misattributed, an artifact, or
unprovable. Stateless, on-demand; delegates measurement to seo-serp-analysis,
seo-position-tracking, and seo-knowledge-graph. Genesis: JHR "호텔 16→3"
SEMrush claim refuted via GSC/GA4/live-SERP cross-check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuT3W81t88QQFaxY2ruWv2
2026-06-26 09:58:50 +09:00
e1f4d75dc2 graphify: index full doc layer via Gemini 3.1 Pro
Indexed 869 skill docs + 6 images (excl. 73 redundant .claude/commands mirrors)
using Gemini 3.1 Pro (~1.3M in / 301k out tokens, ~$6.23 - no Claude tokens):
+1552 nodes, +1308 edges, 61 doc<->code cross-links, 500 new doc communities
labeled by skill. Every skill's docs/specs are now queryable; prior graph was
code-only. Extraction offloaded to Gemini via graphify's OpenAI-compatible backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 02:20:45 +09:00
6c97dfc913 graphify: index ourdigital-okf skill into knowledge graph
OKF-scoped --update of custom-skills/97-ourdigital-okf (7 code + 19 docs):
+131 nodes (62 AST + 75 semantic), +207 edges, 8 OKF communities, 6 hyperedges
capturing the produce/validate/visualize design and zero-dep utility trio.
Prior graph was code-only; the OKF skill is now queryable. ~172k extraction tokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:33:44 +09:00
c068372abf Add graphify knowledge graph outputs and pending changes
Includes graphify-out/ (interactive graph.html, GRAPH_REPORT.md, graph.json
from Gemini-backed extraction) plus .graphifyignore/.gitignore scope config,
alongside other pending working-tree changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:41:30 +09:00
587a32d239 feat(okf): add install.sh; changelog; e2e produce->validate->visualize verified
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:29 +09:00
626abd4173 feat(okf): add SKILL.md variants (top/code/desktop), CLAUDE.md, README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:29 +09:00
c6585c817f docs(okf): add distilled spec, frontmatter reference, and templates
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:29 +09:00
75acd3aa3e feat(okf): add minimal okf_viz Cytoscape generator with tests
Full suite green (20 tests). Smoke-tested on Google crypto_bitcoin bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:29 +09:00
7b239dda8f feat(okf): add okf_validate; harden YAML parser for block lists + folded scalars
Verified conformant against Google reference bundles (crypto_bitcoin, ga4, stackoverflow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:29 +09:00
9762ee97ab feat(okf): add okf_common frontmatter/link parser with tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:29 +09:00
4416833cb3 feat(okf): scaffold ourdigital-okf skill skeleton + mini fixture bundle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:35:28 +09:00
b574c97fcc chore(marketplace): add ourdigital-skills local marketplace + fix README install docs (#1) 2026-06-16 14:08:27 +09:00
7daa4cda68 SEO schema validator skill update 2026-06-08 13:21:09 +09:00
Andrew Yim
8ffb6bec6b docs(ourdigital): align journal + research skill style guides to v3.3/v2.1 (#12)
Some checks failed
Verify Skills / verify-skills (push) Has been cancelled
Merge-align the remaining OurDigital skill references to the authoritative
guides, preserving each skill's structure:

- 03-journal: brand identity, three-channel context, journal role/length
  (English, 1,000-2,000 words), authority order, OurDigital-vs-Clinic boundary,
  shared writing principles adapted for English. English voice preserved — the
  Korean-blog-only rules (평서체, 전문용어 병기) were deliberately NOT imposed.
- 04-research: per-channel voice/tone aligned to Writing Style Guide v2.1;
  fixed an incorrect 경어체 (~입니다) rule for the Korean blog → 평서체; SEO
  numbers corrected (title ~40자/≤60, meta ≤155, English slug); added brand
  identity + authority order + Clinic boundary.

05-document brand_config.json (corporate doc colors) and the claude-ai-export
snapshot were intentionally left out of scope.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:27:54 +09:00
Andrew Yim
9b914b9dd4 fix(estimate-engine): quote SKILL.md description to fix verify-skills YAML error (#11)
The unquoted description contained "Costing methods: effort …" — the mid-string
": " made YAML treat it as a nested mapping ("mapping values are not allowed
here"), failing verify-skills since 2026-05-27. Single-quote the value (it has
inner double quotes but no apostrophes), preserving the exact text.

verify_skills.py: 76/76 skills valid.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:21:26 +09:00
Andrew Yim
aab98f405d docs(ourdigital): align brand/blog/designer skill references to v3.3/v2.1 (#10)
Merge-align the core trio's reference material to the authoritative OurDigital
guides (Blog Project Instruction v3.3, Writing Style Guide v2.1, Visual Style
Guide v2.1), preserving each skill's structure:

- 01-brand-guide: brand-foundation + writing-style — identity statement, mission,
  three channels, OurDigital-vs-Clinic boundary, four writing principles,
  평서체/전문용어 병기 rules, authority order.
- 02-blog: blog-style-guide — channel identity, writing characteristics,
  post structure (char counts), SEO (title/meta/English slug), Ghost metadata,
  self-edit checklist.
- 06-designer: visual_metaphors (both copies kept identical) — bright editorial
  minimalism, Preferred/Avoid metaphor dictionary, 5 palettes, signature motifs,
  Korean-via-restraint, human-trace rule.

Source of truth: knowledge-base/ourdigital in our-ai-editor (PR #12).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:16:07 +09:00
6f69a6a484 Route estimate outputs to central archive (ourdigital-space/estimates)
All estimates now archive to ~/Workspaces/ourdigital-space/estimates/
<YYYY-MM-DD>_<prospect|account>_<service>/ (single 견적 archive). Engine
SKILL.md documents the output-home convention (engine stays --out-dir-agnostic).
presales-seo Stage 5 writes the estimate to $EST (archive); Stage 6 deck reads
from $EST but stays in the engagement bundle.

(Workspace side, not in this repo: SHR estimate moved to the archive, pointer
left in the engagement, ourdigital-space/CLAUDE.md documents estimates/.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:22:33 +09:00
9f7f9e7221 education: clarify ga4_gtm_intermediate (=기본구성/숨고), add marketing-analytics course
The 숨고 GA4/GTM 중급 기본견적 is identical to the existing ga4_gtm_intermediate
course (17 lessons, ₩1,570,000) — labeled it as the 기본구성 standard + aliases
rather than duplicating. Added the genuinely distinct GA4/GTM 마케팅 애널리틱스
course (22 lessons incl. 메타/구글 솔루션 + 워크숍 + 트리트먼트) → list ₩4,070,000.

Validated: intermediate ₩1,570,000, marketing_analytics ₩4,070,000.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:15:56 +09:00
6ac547e78f refactor(skills): clean skill names (strip NN- prefix from name:) — convention change
Some checks failed
Verify Skills / verify-skills (push) Has been cancelled
Adopt: directory keeps its NN- ordering prefix; skill `name:` is the clean form
without it (dir 16-seo-schema-validator → name: seo-schema-validator). Nicer to
invoke, matches the original desktop/SKILL.md names, still globally unique.

- 71 root SKILL.md: name: NN-foo → name: foo (flat skills + reference-curator suite).
  Plugins (mac-optimizer/multi-agent-guide/dintel-bootstrap) already clean; 95 already clean.
- scripts/migrate_skill_root.py: derive name = dirname minus NN- prefix (skill_name()).
- CLAUDE.md + SKILL-MIGRATION-GUIDE.md: document the dir-prefix / clean-name convention.

verify_skills.py: 0 name collisions across all renamed skills. (The ~/.claude/skills
symlinks were re-pointed to the clean names separately — filesystem only.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:11:01 +09:00
08cb20fc67 Add real digital_ads + digital_branding catalogs
Built from real D.intelligence docs (not the 견적 자료 folder, which has only
GA4/GTM + education — confirmed):
- digital_branding: TNS 유학원 디지털 브랜딩 진단 컨설팅 → ₩9,000,000 (5 fixed stages)
- digital_ads: 디하이브 디지털 광고·퍼포먼스 마케팅 대행 계약 → ₩6,000,000/월 retainer
  (media-spend commission % is per-deal, kept as a parameter — not invented)

effort method now supports fixed-amount tasks (Unit Cost) and monthly retainers
(unit: monthly); render shows 고정/—//월 and retainer/commission notes.
Validated: branding ₩9.0M, ads ₩6.0M/월; no regression (SEO 25.0M, coaching 1.57M).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:06:59 +09:00
c9bdbb57f7 Extract ourdigital-estimate-engine; presales-seo now calls it
New skill 96-ourdigital-estimate-engine: method-aware quoting engine
(effort / coaching / procurement) with universal rate_card + per-service
catalog. Real catalogs: seo (effort), education (coaching); stubs:
digital_ads, digital_branding. Validated to reproduce real quotes —
SEO basic ₩10.5M / treatment ₩25.0M, SHR chain ₩29.5M, L'Escape basic
₩10.5M, GA4/GTM coaching ₩1,570,000, procurement +15%.

Refactor 95-ourdigital-presales-seo: remove rate_card.yaml, sow_templates.yaml,
estimate.py (migrated to engine); add findings_to_scope.py; Stage 5 now maps
findings→scope.json and calls the engine CLI. build_deck/kg_query unchanged;
end-to-end validated on SHR (29.5M) + deck renders engine estimate.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:54:11 +09:00
34c3a1df4f ci: run verify_skills.py on every push and PR
Some checks failed
Verify Skills / verify-skills (push) Has been cancelled
Add .github/workflows/verify-skills.yml — installs pyyaml and runs
scripts/verify_skills.py on push and pull_request, failing the build if any skill
would not load. GitHub Actions runs it; Gitea Actions also reads .github/workflows/
when Actions is enabled, so it covers both remotes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:30:18 +09:00
137b927477 fix(skills): make all skills load-valid + add scripts/verify_skills.py
Add a comprehensive load verifier and fix the two issues it found:

- scripts/verify_skills.py: validates every loadable unit (flat root, suite sub-skill,
  plugin skill) with real YAML parsing, name regex + global uniqueness, frontmatter
  <=1024, description sanity, plugin.json JSON validity, and orphan detection. Read-only.
- 92-tui-design-template (root + code/SKILL.md): fix invalid YAML `triggers:` block
  (`- "a", "b"` multi-scalar list items) -> one phrase per list item.
- 17-seo-schema-generator: remove ">" from description ("generate -> validate" ->
  "generate then validate"); angle brackets are disallowed in descriptions.

Result: 75/75 loadable skills valid — 0 failures, 0 name collisions, 0 orphans,
0 plugin-manifest errors (65 flat + 3 plugins + 7 suite sub-skills).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:22:33 +09:00
95d6fdf499 Add luxury/vertical signal to auto-tiering
pick_baseline now applies a premium floor: if prospect.vertical matches
rate_card.tiering.premium_verticals (luxury/premium/deluxe/5성/특1급…), the
auto-selected tier is floored to premium_min_tier (default basic) so a premium
single property won't drop to the smb entry tier.

Validated: L'Escape (hotel_luxury, 1 property) smb -> basic (₩10.5M);
non-luxury single -> smb (₩3.0M); SHR chain -> treatment (₩29.5M) unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:15:55 +09:00
0496262cd5 feat(skills): author root SKILL.md for reference-curator suite + 7 sub-skills
Finish the migration for the dirs the bulk pass couldn't auto-handle:

- 90-reference-curator/SKILL.md: hand-authored suite orchestrator (pipeline overview,
  7-stage table, /reference-curator run modes, install) — the single loadable entry.
- 90-reference-curator/0{1..7}-*/SKILL.md: generated from each sub-skill's desktop/SKILL.md.
- scripts/migrate_skill_root.py: generalized discovery to find nested suite sub-skills
  (rglob desktop/code SKILL.md), so the migrator now handles suites too.

81-mac-optimizer, 91-multi-agent-guide, 94-dintel-bootstrap need NO root SKILL.md: they
are Claude Code plugins whose skill correctly lives at skills/<name>/SKILL.md (validated).
Adding a root SKILL.md there would violate plugin structure.

All SKILL.md repo-wide validate: flat-root=65, suite-sub=7, plugin-skills=3, 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:14:46 +09:00
60734dbde7 Recalibrate estimate for SMB acceptability
Real-world feedback: list-rate calc overshot SMB-acceptable levels.
- scaling: driver properties_total -> subbrands_total (chains share templates),
  cap x6.5 -> x2.0, applied to On-page only (Technical now fixed site-wide)
- add 'smb' entry tier (lean hours @ 0.55 billing); 3-tier auto-select
  (smb/basic/treatment) by portfolio size; per-tier billing; --baseline smb enabled
- docs (findings_to_service.md, SKILL.md) synced to 3-tier model

Effect: SHR 25-property chain 71.5M -> 29.5M; SMB single hotel ~3.0M;
basic/treatment still reproduce real 10.5M/25.0M at 1 property.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:13:38 +09:00
e519a49cc4 feat(skills): bulk-add root SKILL.md to 61 skills (native Agent Skills loadability)
Run the additive migration pass from SKILL-MIGRATION-GUIDE: generate a root SKILL.md
for every skill that lacked one, copied from its desktop/SKILL.md (or code/SKILL.md),
with name set to the directory name and description + body preserved verbatim.

- scripts/migrate_skill_root.py: the reusable, non-destructive migrator (dry-run default).
- 61 new root SKILL.md (desktop source for most; code/SKILL.md for 61/62/92).
- Untouched: 16/17/95 (already had root); desktop/ and code/ packaging left intact.
- All 64 root SKILL.md validate: frontmatter <=1024, kebab name, description present.

Still MANUAL (no SKILL.md source — commands/README only), need hand-authored root SKILL.md:
81-mac-optimizer, 90-reference-curator, 91-multi-agent-guide, 94-dintel-bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:05:20 +09:00
5f66f57a8e Rebuild estimate engine to effort-based SOW model
Replace flat per-service ranges with OurDigital's real quoting model
(role_rate × billing_rate 0.70 × standard hours), sourced from the
06_Working Template quotes:
- rate_card.yaml: role rate card, billing/basis/terms, tools, scaling bands
- sow_templates.yaml: basic + treatment task-hour templates
- estimate.py: assemble SOW from findings, scale Technical/On-page hours by
  properties_total, 제안가 = 합계 floored to 500k
- build_deck.py: estimate slide shows module 소계 + 제안가 (point)
- findings_to_service.md / SKILL.md / DESIGN.md: synced to new model

Validated: reproduces real Basic ₩10.5M and Treatment ₩25.0M exactly;
SHR (25 properties) scales to ₩71.5M, L'Escape (1) = ₩25.0M.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:51:52 +09:00
01201ee4a2 docs(skills): add SKILL-MIGRATION-GUIDE + adopt root-SKILL.md hybrid as standard
- reference/SKILL-MIGRATION-GUIDE.md: one-page recipe to add a root SKILL.md to an
  existing skill (additive, no rewrite) — target structure, frontmatter checklist,
  a verified validation snippet, incremental-adoption guidance. References 16/17.
- CLAUDE.md: replace "Dual-Platform Skill Structure" with the hybrid (root SKILL.md +
  code/ + desktop/) as the going-forward standard; add "Root SKILL.md first" design
  principle; require a root SKILL.md for new skills; link the guide in Key Reference Files.
- SKILL-FORMAT-REQUIREMENTS.md: note the root-SKILL.md standard + cross-link the guide.

Validation snippet self-tested against 16/17 (frontmatter ≤1024, kebab name, all
referenced relative paths resolve).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:50:50 +09:00
1e3b2a4fa0 chore(seo-schema-generator): restore dual-platform code/ + desktop/ structure
The full-merge commit removed 17's code/ and desktop/ folders along with the old
template-fill generator, leaving 17 structurally inconsistent with 16 (and the repo's
documented dual-platform convention). Restore them for the MERGED skill:

- code/CLAUDE.md — Claude Code directive pointing to the root two-mode pipeline
  (no script duplication; root scripts/ stays the single source of truth).
- desktop/SKILL.md + skill.yaml — Claude Desktop directive for the two-mode skill.
- desktop/tools/{firecrawl,perplexity}.md — restored verbatim from git (still used by
  Mode 1 crawl / Mode 2 research).

The old generator's logic stays retired; only the dual-platform folder structure and
relevant tool docs return. 17 now matches 16's layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:45:10 +09:00
206 changed files with 36843 additions and 900 deletions

View 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
}
]
}

View 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

View 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

View File

@@ -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
View 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
View File

@@ -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
View 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-*/

View File

@@ -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)
@@ -160,19 +162,28 @@ This is a Claude Skills collection repository containing:
| 81 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" |
| 82 | our-gdrive-organizer | Organize a Google Drive folder under OurDigital conventions: refresh README index, refresh per-subfolder READMEs, propose renames + moves | "/organize", "organize the Drive folder", "refresh the index", "rescan the folder" |
## Dual-Platform Skill Structure
## Skill Structure (root `SKILL.md` + dual-platform packaging)
Each skill has two independent versions:
**Going-forward standard.** Each skill has a root `SKILL.md` (the official Agent Skills
format — natively loadable by Claude Code / Desktop / API) plus the OurDigital `code/` +
`desktop/` packaging. Reference implementations: `16-seo-schema-validator`,
`17-seo-schema-generator`. To migrate an older skill, follow
`reference/SKILL-MIGRATION-GUIDE.md` (additive — no rewrite).
```
XX-skill-name/
├── code/ # Claude Code version
│ ├── CLAUDE.md # Action-oriented directive
│ ├── scripts/ # Executable Python/Bash
│ └── docs/ # Documentation
├── SKILL.md # Root directive — official Agent Skills format (LOADABLE)
├── scripts/ # Runnable scripts (single source of truth)
├── references/ # Heavy docs, loaded on demand
├── templates/ # Optional
├── fixtures/ # Optional sample/test inputs
├── desktop/ # Claude Desktop version
│ ├── SKILL.md # Skill directive with YAML frontmatter
├── code/ # Claude Code packaging
│ ├── CLAUDE.md # Redirects to ../SKILL.md + ../scripts (no duplication)
│ └── scripts/ # Legacy/aux tools only
├── desktop/ # Claude Desktop packaging
│ ├── SKILL.md # Desktop directive with YAML frontmatter
│ ├── skill.yaml # Extended metadata (optional)
│ └── tools/ # MCP tool documentation
@@ -180,24 +191,24 @@ XX-skill-name/
└── README.md # Overview
```
### Platform Differences
Older skills may still be `code/` + `desktop/` only (no root `SKILL.md`) — valid, but not
directly loadable. Migrate incrementally, not in bulk.
| Aspect | `code/` | `desktop/` |
|--------|---------|------------|
| Directive | CLAUDE.md | SKILL.md (with YAML frontmatter) |
| Metadata | In CLAUDE.md | skill.yaml (optional) |
| Tool docs | N/A | tools/ directory |
| Execution | Direct Bash/Python | MCP tools only |
| Scripts | Required | Reference only |
### Layer roles
### SKILL.md Format (Desktop)
| Layer | Role |
|-------|------|
| root `SKILL.md` | Canonical, loadable directive + bundled resources |
| `code/` | Claude Code packaging; `CLAUDE.md` redirects to the root |
| `desktop/` | Claude Desktop view; `SKILL.md` + `skill.yaml` + `tools/` |
### SKILL.md frontmatter (root + desktop)
```markdown
---
name: skill-name-kebab-case
name: skill-name-kebab-case # clean name: dir minus the NN- prefix
description: |
Brief description of what the skill does.
Triggers: keyword1, keyword2, 한국어 트리거.
What it does + when to use. Triggers: keyword1, keyword2, 한국어 트리거.
---
# Skill Title
@@ -205,6 +216,11 @@ 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
```
@@ -239,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/
@@ -257,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/
@@ -287,10 +305,11 @@ our-claude-skills/
## Skill Design Principles
1. **One thing done well** - Each skill focuses on a single capability
2. **Directives under 1,500 words** - Concise, actionable
3. **Self-contained** - Each platform version is fully independent
4. **Code-first development** - Build Claude Code version first
5. **Progressive numbering** - Logical grouping by domain
2. **Root `SKILL.md` first** - Ship a loadable root `SKILL.md`; keep `code/`+`desktop/` as packaging
3. **Directives under 1,500 words** - Concise, actionable; push detail into `references/`
4. **Self-contained** - Bundle scripts/refs as relative paths; one source of truth (no script dupes)
5. **Code-first development** - Build and self-test the runnable version first
6. **Progressive numbering** - Logical grouping by domain
## Creating New Skills
@@ -299,8 +318,12 @@ our-claude-skills/
python example-skills/skills-main/skill-creator/scripts/init_skill.py <skill-name> --path custom-skills/
```
New skills must include a root `SKILL.md` (the hybrid pattern above); model them on
`16-seo-schema-validator` / `17-seo-schema-generator`.
## Key Reference Files
- `reference/SKILL-FORMAT-REQUIREMENTS.md` - Format specification
- `reference/SKILL-MIGRATION-GUIDE.md` - Add a root `SKILL.md` to an existing skill (hybrid pattern, going-forward standard)
- `reference/SKILL-FORMAT-REQUIREMENTS.md` - Dual-platform format specification
- `example-skills/skills-main/skill-creator/SKILL.md` - Skill creation guide
- `AGENTS.md` - Agent routing guide for Task tool

View File

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

View 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

View File

@@ -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 | 무료 상담 신청하기 |

View File

@@ -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가 글을 지배해서는 안 된다 |
---

View 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

View File

@@ -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문장]
```

View 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

View File

@@ -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,5003,000자 |
| **`journal.ourdigital.org`** | **English** | **Industry trends, techhuman intersection, reflective essay** | **1,0002,000 words** |
| `ourstory.day` | Korean | 개인 에세이, 삶의 성찰, 일상의 관찰 | 8001,500자 |
| `Medium` | English | Technology, marketing, AI for broad audiences | 8001,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,0002,000 words |
| **Content focus** | Industry trends, techhuman 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,0002,000 words?
- [ ] **Self-edit**: identify the single weakest element ("all pass" is not allowed).

View 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

View File

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

View 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

View 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

View File

@@ -1,101 +1,104 @@
# Visual Metaphor Dictionary
<!-- 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 23 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

View File

@@ -1,101 +1,104 @@
# Visual Metaphor Dictionary
<!-- 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 23 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

View 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

View 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` - 발표 스타일

View 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` - 문서 스타일

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

View 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

View 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

View 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

View 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

View 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

View File

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

View File

@@ -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]
}
}

View File

@@ -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
]
}
}

View File

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

View File

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

View File

@@ -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, 스키마 생성, 스키마 저작,
구조화 데이터 생성, 미발행 사이트 스키마, 기존 사이트 스키마 추출.

View File

@@ -0,0 +1,52 @@
# CLAUDE.md — seo-schema-generator (Claude Code)
## Canonical entry point
This skill was upgraded to a **two-mode source-to-schema pipeline** (it absorbed and
retired the old template-fill generator). The authoritative directive and run
instructions live in the skill root:
- **`../SKILL.md`** — the two modes, the claims-register pivot, stage gates, how to run.
- **`../scripts/extract_site_claims.py`** — Mode 1: existing site → claims register.
- **`../scripts/build_schema_drafts.py`** — claims register → JSON-LD drafts + dataset CSV.
- **`../scripts/type_templates.json`** — draft templates (edit JSON to add a type).
- **`../references/`** — `site-extraction-methodology.md` (Mode 1), `source-to-schema-methodology.md` (Mode 2), `source-authority-hierarchy.md`, `entity-and-type-map.md`.
- **`../templates/`** — `claims-register.csv`, `source-register.csv`, `review-guide.md`.
```bash
# Try the bundled sample first (Mode 2)
python ../scripts/make_sample.py
python ../scripts/build_schema_drafts.py ../fixtures/sample_claims.csv --out drafts_out
# Mode 1 — existing site → claims register (URLs, or local .html / a directory offline)
python ../scripts/extract_site_claims.py https://example.com/ --out site_claims
python ../scripts/build_schema_drafts.py site_claims/claims_register.csv --out drafts_out
# Hand off to the QA gate (must reach zero P0)
python ../../16-seo-schema-validator/scripts/validate_schema.py \
drafts_out/schema_drafts_dataset.csv --out qa_out
```
Core rule: **only CONFIRMED, non-conflicting claims become schema.** Unfilled template
slots are pruned — never emitted as `{{…}}` or `TODO`. **Generate (17) → Validate (16).**
## Retired
The previous template-fill tool (`schema_generator.py` + per-type JSON templates) is
superseded by the claims-register engine above and has been removed. Use
`build_schema_drafts.py` for all generation.
## Notion output (OurDigital SEO Audit Log)
When generation is part of an OurDigital/D.intelligence engagement, log a summary to the
SEO Audit Log database. Per the user-level Notion rule, push **page content** with the
`notion-writer` skill; use Notion MCP only for **properties**.
| Field | Value |
|-------|-------|
| Database ID | `2c8581e5-8a1e-8035-880b-e38cefc2f3ef` |
| Category | `Schema/Structured Data` |
| Audit ID | `SCHEMA-YYYYMMDD-NNN` |
Report content in Korean; keep technical terms (Schema, JSON-LD, claims register) and
URLs/code unchanged.

View File

@@ -0,0 +1,55 @@
---
name: seo-schema-generator
description: |
Generates validation-ready JSON-LD for a site via a claims register — Mode 1 from
an existing website, Mode 2 from collected sources for a not-yet-published site.
Triggers: generate schema, create JSON-LD, source-to-schema, pre-launch schema,
schema from site, claims register, 스키마 생성, 스키마 저작.
---
# SEO Schema Generator
> Desktop reference. The full, runnable specification (scripts, references, templates,
> fixtures, stage gates) is the skill-root `SKILL.md` and its bundled directories — this
> file is the Claude Desktop entry point.
## Purpose
Author JSON-LD for a site whether or not its pages exist yet. Both scenarios route
through one pivot — a **claims register** (provenance-tracked, conflict-resolved facts) —
then generate pruned drafts that hand off to **seo-schema-validator** (generate → validate).
## Two modes, one pipeline
| | Mode 1 — existing site | Mode 2 — collected sources |
|---|---|---|
| Source of truth | the live pages | scattered sources (DART, Wikidata, brochures) |
| Seed the register with | extract from pages | manual research |
| Hard part | extraction & mapping | authority hierarchy + entity reconciliation |
Everything after the claims register (build drafts → prune unfilled slots → validate)
is identical. **Only CONFIRMED, non-conflicting claims become schema;** unfilled template
slots are deleted, never shipped as placeholders.
## MCP Tool Usage
```
mcp__firecrawl__scrape / crawl : Mode 1 — pull existing pages to extract facts
mcp__perplexity__search : Mode 2 — discover & cross-check authoritative sources
```
## Workflow
1. Lock the entity→type map (scope first).
2. Seed the claims register (Mode 1: extract from pages · Mode 2: research → register).
3. Reconcile to CONFIRMED; clear conflicts.
4. Build drafts from type templates (placeholders pruned).
5. Validate with seo-schema-validator — gate = zero P0.
6. Fix P0, re-validate, then client review against the report (not raw JSON).
## Notes
- Mode 1 inference (title/OpenGraph) is seeded PENDING and never auto-ships; existing
JSON-LD is seeded CONFIRMED. If a site already has good JSON-LD, audit it with
seo-schema-validator (Mode B) instead of regenerating.
- Authoritative rich-result eligibility still needs Google's online test on a sample.

View File

@@ -0,0 +1,14 @@
# Skill metadata (extracted from SKILL.md frontmatter)
name: seo-schema-generator
description: |
JSON-LD generator with two modes — from an existing website, or from collected
sources for a not-yet-published site — both via a claims register. Triggers:
generate schema, create JSON-LD, source-to-schema, schema from site, claims register.
# Optional fields
allowed-tools:
- mcp__firecrawl__*
- mcp__perplexity__*
# triggers: [] # TODO: Extract from description

View File

@@ -0,0 +1,15 @@
# Firecrawl
> TODO: Document tool usage for this skill
## Available Commands
- [ ] List commands
## Configuration
- [ ] Add configuration details
## Examples
- [ ] Add usage examples

View File

@@ -0,0 +1,15 @@
# Perplexity
> TODO: Document tool usage for this skill
## Available Commands
- [ ] List commands
## Configuration
- [ ] Add configuration details
## Examples
- [ ] Add usage examples

View 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

View 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 23 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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"

View 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

View 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

View 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

View 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

View 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

View 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 (`![alt](./file.png)`) 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
```

View File

@@ -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 (`![alt](./image.png)`) are auto-uploaded to Notion and embedded as `file_upload` image blocks. Remote images (`![](https://...)`) 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 ![chart](./chart.png)
# 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 (`![](local)` → 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.

View File

@@ -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}},
)

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

View File

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

View 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

View File

@@ -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![remote](https://ex.com/a.png)\n\n"
"![local](./b.png)\n\nbody")
without, imgs = notion_writer.extract_local_images(content)
assert imgs == [("local", "./b.png")]
assert "![local](./b.png)" not in without
assert "![remote](https://ex.com/a.png)" in without # remote stays
def test_content_has_local_images():
assert notion_writer._content_has_local_images("![x](./y.png)") is True
assert notion_writer._content_has_local_images("![x](https://e/y.png)") 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![x](./y.png)\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 "![x](./y.png)" 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()

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

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

View File

@@ -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("![a chart](https://ex.com/c.png)")
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("![local](./pics/x.png)")
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 ![x](y.png) 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__}")

View File

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

View 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

View 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

View 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

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

View 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, ~180220 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 L1L2)
**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 13.
- 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 12).
- 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, L1L4, short-circuit) → Tasks 1 (L1L2) + 2 (L3L4); 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.**

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

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

View File

@@ -0,0 +1,4 @@
query clicks impressions position
호텔 9 371 18.1
grand josun busan 49 1500 3.4
조선호텔 150 5000 1.3
1 query clicks impressions position
2 호텔 9 371 18.1
3 grand josun busan 49 1500 3.4
4 조선호텔 150 5000 1.3

View File

@@ -0,0 +1,4 @@
query clicks impressions position
호텔 5 572 11.6
grand josun busan 250 4000 1.2
조선호텔 300 6000 1.1
1 query clicks impressions position
2 호텔 5 572 11.6
3 grand josun busan 250 4000 1.2
4 조선호텔 300 6000 1.1

View File

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

View File

@@ -0,0 +1 @@
# gsc_signal_delta.py uses the Python 3 standard library only — no deps.

View File

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

View 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*

View 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*

View 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` | 중복 확인, 기존 답변 관리 |

View 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*

View 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만 지원
- **한국어 특화**: 제이미성형외과 콘텐츠 전용 오타 사전

View 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*

View 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.**

View 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.**

View 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 **23 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`.

View File

@@ -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.*

View 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.*

View File

@@ -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.*

View File

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

View File

@@ -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 (15 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
```

View File

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

View File

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

View 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

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

View 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` |

Some files were not shown because too many files have changed in this diff Show More