# Design — `ourdigital-presales-seo` skill - **Status**: Approved design (2026-05-27). Ready for implementation planning. - **Author**: OurDigital (andrew.yim@ourdigital.org) - **Origin**: Standardizes the Sono Hotels & Resorts pre-sales diagnostic (`~/Workspaces/shr-workspace/audits/2026-05-27-presales/`) into a reusable skill, adding estimate generation and a sales slide deck. ## 1. Purpose & scope A single OurDigital Claude Code skill that runs a **pre-sales SEO + entity diagnostic** for any prospect domain and produces, as **confirmed step-by-step stages**: 1. Technical / on-page scan 2. Knowledge Graph / entity analysis (Korean market, Naver-aware) 3. Consolidated opportunity brief 4. **Rate-card-based cost estimate (견적)** 5. **Editable PPTX sales-briefing deck** + short client PDF **Public-data only** by default (no client GSC/GA4/GTM access assumed) — suited to prospecting where access isn't yet granted. ### Non-goals (YAGNI) - Not a full post-contract audit (that stays with `seo-comprehensive-audit`). - No fixed 3-tier package output (user chose findings→rate-card line items only). - No automated sending/emailing of deliverables. - No fully-autonomous mode — execution is step-by-step gated. ## 2. Invocation & inputs (Stage 0) Invoked as `/ourdigital-presales-seo` (optionally with a domain arg). Stage 0 gathers: | Input | Req | Default | |---|:---:|---| | `domain` | ✅ | — | | `brand_name` + `aliases[]` (for KG; e.g. 앤/& /EN variants) | ✅ | derived from site ``/og | | `sub_brands[]`, `properties[]` (entity targets) | — | auto-extracted from crawl URL patterns | | `competitors[]` | — | from `references/competitor_sets.md` by vertical | | `market` / `language` | — | `South Korea` / `ko` (Naver-aware) | | `output_dir` | — | account workspace if exists, else `seo-workspace/prospecting/<date>-<prospect>/` (see §7) | | `vertical` | — | inferred (hotel/resort default rubric) | Stage 0 also runs a **preflight tool check** (Firecrawl, DataForSEO, `GOOGLE_KG_API_KEY`, headless Chrome, `python-pptx`) and reports any missing capability with its fallback. ## 3. Pipeline (step-by-step; each stage presents results and WAITS for user confirmation) | Stage | Actions | Primary tools | Output | |---|---|---|---| | 0 Scope | inputs, folders, preflight | — | `scope.md`, folders | | 1 Discovery | robots.txt, sitemap status, `firecrawl_map` inventory, scale estimate, URL hygiene (`/test`, params, dup `/sb`↔`/brand_loc`) | Firecrawl, WebFetch | `data/urls.json`, scan §1 | | 2 Technical/on-page | JSON-LD extraction, meta/title/H1 duplication, hreflang completeness, CWV (Lighthouse) | Firecrawl scrape, DataForSEO Lighthouse | `01_technical-onpage-scan.md`, `data/cwv_lighthouse.json` | | 3 KG/entity | KG API (ko) over master/parent/legacy/membership/sub-brand/property/competitor sets; live SERP panel verification | `kg_query.py` (Google KG API), DataForSEO SERP | `02_knowledge-graph-entity.md`, `data/kg_*.json`, `data/serp_panels_findings.md` | | 4 Brief | synthesize, severity ranking, competitive gap table | — | `03_presales-opportunity-brief.md` | | 5 Estimate | findings→rate card mapping → ranged 견적 | `estimate.py` + `rate_card.yaml` | `05_estimate_ko.md`, `05_estimate.xlsx` — **review gate** | | 6 Deliverables | short client PDF + branded PPTX deck | `render_pdf.sh` (Chrome), `build_deck.py` (python-pptx) | `client-brief.pdf`, `sales-deck.pptx` — **review before send** | | 7 Archive | push consolidated report (03 brief + estimate summary) to the OurDigital SEO Audit DB — **standard final stage** | `notion_writer.py` | Notion row in SEO Audit Log | Each stage appends to the shared **`findings.json`** data contract (§5), the integration seam between analysis and the estimate/deck generators. **Archive target (standard):** every run archives to the OurDigital SEO Audit database `2c8581e5-8a1e-8035-880b-e38cefc2f3ef` (https://www.notion.so/dintelligence/2c8581e58a1e8035880be38cefc2f3ef). Row title `<프로스펙트> SEO 사전진단 (Pre-sales) — <YYYY-MM-DD>`; set `Target URL`, `Audit Date`, `Account Code`. This is the system of record for prospect + client audits alike. ## 4. Component breakdown (units & interfaces) - **`SKILL.md`** — orchestration: stage definitions, per-stage gating, Korean-first output rules, tool fallbacks, sandbox-disable notes for KG/Chrome/Notion network calls. - **`scripts/kg_query.py`** — IN: entity list (group,label,query) + lang + key (env). OUT: `kg_raw.json`, `kg_flat.json`, console summary (score/type/lodging-flag/own-entity). Generalized from the SHR script (entities parameterized, not hardcoded). - **`scripts/estimate.py`** — IN: `findings.json` + `rate_card.yaml` + rules. OUT: `05_estimate_ko.md` + `05_estimate.xlsx` (항목·상세·수량·단가 range·금액; one-time + monthly subtotals; `OD-YYYY-NNN`; disclaimer) **and `data/estimate.json`** (selected line items + totals, consumed by `build_deck.py`). - **`scripts/build_deck.py`** — IN: `findings.json` + `estimate.json` + `deck_theme`. OUT: `sales-deck.pptx` (9 slides, §6). python-pptx. - **`scripts/render_pdf.sh`** — IN: client-brief HTML. OUT: PDF via headless Chrome (Korean system fonts). - **`references/rate_card.yaml`** — single source of OurDigital service rates (see §5.1). - **`references/findings_to_service.md`** — finding-class → severity → service-line rubric (§5.2). - **`references/competitor_sets.md`** — default KR competitor benchmarks by vertical (hotel/resort seeded: 롯데/신라/조선/한화/켄싱턴). - **`templates/`** — `01/02/03` md, `client_brief.html`, `estimate_OD.md`, `deck_theme.py`. ## 5. Estimate logic > **Superseded (2026-05-28):** the flat per-service ranges below were replaced by an > **effort-based** engine (role-hours × billing_rate 0.70) loaded from OurDigital's real > quotation templates. See `references/rate_card.yaml`, `references/sow_templates.yaml`, > and `references/findings_to_service.md`. Validated to reproduce the real Basic (₩10.5M) > and Treatment (₩25.0M) quotes exactly. The notes below are kept as historical context. ### 5.1 `rate_card.yaml` (from `ourdigital-backoffice`) ```yaml quote_prefix: OD # OD-YYYY-NNN currency: KRW services: technical_audit: {label: "Technical Audit / 기술 SEO 진단", unit: one_time, min: 3000000, max: 5000000} technical_remediation: {label: "기술 개선 실행", unit: project, min: 3000000, max: 8000000} onpage_entity: {label: "On-Page / Entity Optimization", unit: monthly, min: 1500000, max: 3000000} schema_build: {label: "구조화 데이터 구축(1회)", unit: one_time, min: 2000000, max: 4000000} local_seo: {label: "Local SEO", unit: monthly, min: 1000000, max: 2000000} gtm_setup: {label: "GTM Setup", unit: project, min: 2000000, max: 4000000} ga4_impl: {label: "GA4 Implementation", unit: project, min: 1500000, max: 3000000} dashboard: {label: "Dashboard Development", unit: project, min: 3000000, max: 6000000} ``` *(Values mirror the backoffice rate card; treated as estimate ranges. Skill reads this file — no hardcoded prices.)* ### 5.2 `findings_to_service.md` rubric (finding class → service line) | Finding class (from `findings.json`) | Service line(s) | Scope driver | |---|---|---| | broken sitemap / low crawl-coverage / SPA rendering / CWV poor | `technical_audit` + `technical_remediation` | site size, # templates | | missing/weak schema, entity gaps, sub-brand/property entities, Wikipedia/sameAs | `schema_build` (one-time) + `onpage_entity` (retainer) | # sub-brands + # properties | | property local packs / GBP-URL mismatch | `local_seo` | # properties | | no GSC/GA4, measurement gaps | `ga4_impl` and/or `dashboard`, `gtm_setup` | — | | duplicate meta / title i18n / hreflang / content confusion | `onpage_entity` | # templates | `estimate.py` selects line items per detected findings, scales `qty` by drivers (e.g., property count → local-SEO months/scope), sums one-time vs monthly, and renders the 견적. Always includes the disclaimer: *ranges; finalized after a precise diagnostic with GSC/GA4 access.* ## 6. PPTX deck spec (`build_deck.py`, 9 slides) 1. Title — prospect + "검색 가시성 사전 진단" + date + OurDigital 2. 한눈에 보기 — asset strength vs search-visibility gap 3. Finding 1 — 크롤/색인 (sitemap, discoverable-URL count) 4. Finding 2 — Core Web Vitals 5. Finding 3 — 엔티티/브랜드 인식 (entity type, name split, legacy contamination) 6. Finding 4 — 서브브랜드/프로퍼티 엔티티 + 경쟁 벤치마크 table 7. 개선 로드맵 — Phase 0 (긴급 기술) / 1 (엔티티) / 2 (콘텐츠·로컬) 8. 예상 견적 — rate-card line items + ranges + disclaimer 9. 다음 단계 / CTA — 30분 미팅 · 정밀 진단 · 파일럿 Branding from `ourdigital-brand-guide` (colors/fonts/logo); fallback theme = navy `#11243d` / accent `#1b6fb3` (the SHR brief styling). Slides are content-populated from `findings.json` + `estimate.json`, leaving text editable. ## 7. Output routing Default: if `~/Workspaces/<slug>-workspace/` exists → `…/audits/<YYYY-MM-DD>-presales/`; else `~/Workspaces/seo-workspace/prospecting/<YYYY-MM-DD>-<prospect>/` (per global routing rule). Overridable at Stage 0. `data/` holds raw artifacts; audit md + deck/PDF at top level. ## 8. Dependencies & documented fallbacks | Capability | Tool | Fallback / gotcha | |---|---|---| | URL inventory | Firecrawl `map` | OurSEO `crawl_website` **caps ~60 pages** regardless of `max_pages`; broken sitemap limits discovery — report the discoverable count as a finding | | Page signals | Firecrawl `scrape` (json) | re-scrape if cache returns empty | | SERP panels / CWV | DataForSEO `serp_organic_live_advanced`, `on_page_lighthouse` | — | | Entity DB | Google KG Search API | needs `GOOGLE_KG_API_KEY` / `GOOGLE_API_KEY`; sandbox-disable for network | | Client PDF | headless Chrome `--print-to-pdf` | needs Korean system font (AppleSDGothicNeo present on macOS); sandbox-disable | | Deck | `python-pptx` | install if missing | | Notion archive | `notion_writer.py` → DB `2c8581e5-8a1e-8035-880b-e38cefc2f3ef` | use `--properties` with `Target URL`/`Audit Date`/`Account Code` only; `Audit ID` is a read-only **formula** (do not set); `Site`/`Found Date` from old docs are wrong property names | ## 9. Data contract — `findings.json` (analysis ↔ generators seam) ```jsonc { "prospect": {"name": "", "domain": "", "aliases": [], "vertical": ""}, "discovery": {"sitemap_status": 500, "robots_sitemap_declared": false, "discoverable_urls": 96, "estimated_pages": "thousands", "url_hygiene": ["test_page_exposed", "dup_path_scheme", "param_urls"]}, "technical": {"cwv": {"lcp_ms":0,"cls":0,"ttfb_ms":0,"perf":0}, "schema": {"org": "bare|complete|none", "hotel_on_property": true}, "meta_dupe": true, "title_i18n_mismatch": true, "hreflang": "incomplete"}, "entity": {"panel": "company|hotel|none", "name_split": true, "legacy_contamination": true, "subbrands_with_entity": 0, "properties_with_entity": 0, "competitor_benchmark": [{"name":"","score":0,"type":"","wikipedia":false}]}, "findings": [{"id":"", "class":"", "severity":"critical|high|medium", "evidence":"", "recommended_services":[]}] } ``` Stages 1–4 populate it; Stages 5–6 consume it. This is the key isolation boundary: generators never re-crawl. ## 10. Validation - Dry-run on the SHR data (already collected) → estimate + deck must reproduce sensible output. - `kg_query.py` unit check: known entity (롯데호텔) returns LodgingBusiness + high score. - Deck opens in PowerPoint/Keynote; Korean renders; placeholders editable. - Estimate totals = sum of selected line items; disclaimer present. ## 11. Future (out of scope now) 3-tier package view; auto Naver SERP module; multi-language decks; CRM hand-off.