# 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 `<title>`/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.
