SEO Operations Playbook
The complete system for how Dotcom Design researches, builds, and delivers local SEO strategies for clients. Follow every step in order. Do not skip steps.
Every step must be completed in order before moving to the next. Each step has an Entry Condition (what must be ready before you start), a Completion Gate (what you must produce before you leave), and a Next Step directive. Do not advance until the Completion Gate is fully satisfied.
How to Use This Playbook
When a new SEO strategy is assigned, start at Step 1 and work through each step sequentially. The sidebar shows all 9 steps. The step number indicators in the sidebar show your position. At the bottom of every step page, a "Next Step" button takes you directly to the next step.
The Entry Condition block at the top of each step tells you exactly what must be true before you begin that step. If the entry condition is not met, stop and complete the prior step first.
The Completion Gate block at the bottom of each step (above the Next Step button) is a checklist of deliverables. Every item must be checked before you click Next Step.
Workflow at a Glance
| Step | Name | Input | Output |
|---|---|---|---|
| 1 | Client Data Extraction | Client website URL | Verified service list, HQ city, service area, existing pages |
| 2 | Keyword Research | Service list from Step 1 | Raw keyword list with search volumes for all service lines |
| 3 | Keyword Bucketing | Raw keyword list from Step 2 | Organized keyword families with base terms and variants |
| 4 | Market Analysis | HQ city and service area from Step 1 | Tiered market list sorted by population |
| 5 | Keyword Selection | Buckets from Step 3, markets from Step 4, plan level | Final selected keywords and markets with combination count confirmed |
| 6 | Matrix Generation | Selected keywords and markets from Step 5 | Complete keyword-city matrix with correct combination count |
| 7 | Site Assembly | All data from Steps 1-6 | Complete strategy website (index.html, app.js, charts.js, style.css) |
| 8 | Visual QA | Locally rendered site from Step 7 | Verified site with no layout, data, or formatting errors |
| 9 | Deployment | Verified site from Step 8 | Live site at seo-strategy.dotcomdesign.com/{clientdomain} |
Plan Levels & Pricing
These are the only valid plan levels. Every strategy must map to exactly one of these. Prices are non-negotiable and must appear verbatim on every strategy site.
The prices below are the source of truth. Do not use any other source. Do not estimate. If a plan level is not listed here, it does not exist.
| Plan Level | Monthly Price | Combinations | Notes |
|---|---|---|---|
| SEO Booster | $399/mo | 10 | Entry level. Often 1 keyword x 10 markets or 2 x 5. |
| Level A | $600/mo | 20 | Most common starting plan. |
| Level B | $900/mo | 30 | |
| Level C | $1,200/mo | 40 | |
| Level D | $1,600/mo | 50 | |
| Level E | $2,000/mo | 60 | |
| Level F | $3,000/mo | 90 | |
| Level G | $4,000/mo | 120 | |
| Level H | $5,000/mo | 150 |
Combination Math
The number of combinations is always: Keywords Selected x Markets Selected = Total Combinations. This must equal the plan level exactly. There is no rounding, no approximation.
Examples:
- Level A (20 combos): 4 keywords x 5 markets = 20. Or 2 keywords x 10 markets = 20.
- Level B (30 combos): 5 keywords x 6 markets = 30. Or 3 keywords x 10 markets = 30.
- SEO Booster (10 combos): 1 keyword x 10 markets = 10. Or 2 keywords x 5 markets = 10.
Client Data Extraction
Extract everything you need to know about the client before touching a single keyword. This step is the foundation. If it is wrong, every subsequent step is wrong.
You have the following from the user before starting this step:
- Client website URL
- Plan level (e.g., Level A, SEO Booster)
- Any explicit exclusions (cities to avoid, keywords to avoid)
If any of these are missing, ask the user before proceeding.
What to Extract
Browse the client website thoroughly. Do not skim. Visit the homepage, every service page, the about page, and the contact page. Extract the following:
| Data Point | Where to Find It | Why It Matters |
|---|---|---|
| Business name | Homepage, logo, title tag | Used in strategy site hero and prose |
| HQ city and state | Contact page, footer, address | Always the first selected market |
| Full service list | Services nav, service pages, homepage sections | Defines which keyword buckets are valid |
| Business model type | About page, homepage language | Retail showroom vs. contractor vs. manufacturer. Critical for keyword alignment. |
| Service area | Service area page, footer, about page | Defines the geographic scope for market analysis |
| Existing city pages | Sitemap (/sitemap.xml, /sitemap_index.xml, /page-sitemap.xml) | Avoid duplicating combinations the client already has pages for |
Do not infer services. Do not add services based on industry knowledge. If "cabinet makers" is not on the website, it is not a service. If "outdoor kitchen" is not on the website, it is not a service. The website is the only source of truth for services.
Business Model Classification
Before leaving this step, classify the client's business model. This classification directly controls which keywords are valid in Step 3.
| Model Type | Description | Valid Keywords | Invalid Keywords |
|---|---|---|---|
| Retail Showroom | Sells products from a physical location. Customers come to them. | "near me", "showroom", "store", "shop" | "makers", "contractor", "remodeling", "installation" |
| Contractor / Service | Comes to the customer's home or job site. | "near me", "contractor", "company", "service", "installation" | "showroom", "store", "shop" |
| Manufacturer | Makes products, sells wholesale or direct. | "custom", "made to order", "wholesale" | "near me", "showroom", "contractor" |
Before advancing to Step 2, confirm you have all of the following:
- Business name confirmed
- HQ city and state confirmed
- Full service list documented (only services explicitly on the website)
- Business model classified (Retail Showroom, Contractor, or Manufacturer)
- Service area documented
- Existing city pages noted (or confirmed none exist)
Keyword Research
Generate a raw list of all candidate keywords across every service line. Do not filter or select yet. That happens in Step 3 and Step 5.
Step 1 is complete. You have:
- A verified service list (only services on the client website)
- The business model classification
Using the SEMrush API
Run scripts/semrush_keyword_research.py using each service line as a seed keyword. The SEMrush API key is stored in the SEO Strategist skill.
If the SEMrush API balance is zero, use known industry search volumes from prior research. Document which keywords were sourced from SEMrush vs. estimated, so the data can be verified later.
Keyword Generation Rules
For each service line, generate keyword variants using these modifiers:
| Modifier Type | Examples | Notes |
|---|---|---|
| Local intent | near me, in [region] | Highest commercial intent. Always include. |
| Business type | store, showroom, company, companies, contractor | Must match business model. A showroom gets "store/showroom." A contractor gets "company/contractor." |
| Qualifier | custom, local, best, affordable | Include if they have meaningful search volume. |
| Action | buy, install, design | Include only if relevant to the business model. |
1. City or state names embedded in keywords (e.g., "kitchen cabinets Dallas") — cities are applied at the matrix stage, not the keyword stage. 2. DIY or how-to terms (e.g., "how to install kitchen cabinets"). 3. Job-seeking terms (e.g., "cabinet maker jobs"). 4. Terms outside the client's confirmed service list.
Volume Thresholds
Include all keywords with national monthly search volume above 100. Do not discard low-volume keywords at this stage — they may be valuable for niche service lines. The selection decision happens in Step 5, not here.
Before advancing to Step 3, confirm:
- At least 5 keyword candidates per service line
- Every keyword has a documented monthly search volume
- No city names embedded in any keyword
- No DIY, informational, or job-seeking terms included
- No terms outside the client's confirmed service list
Keyword Bucketing
Organize the raw keyword list into tight, logical families. This structure directly controls how keywords are displayed on the strategy site and how variants are grouped.
Step 2 is complete. You have a raw keyword list with search volumes for all service lines.
The Core Rule: Same Core Term Only
A keyword is only a variant within a bucket if it shares the exact same core term. This is not a guideline. It is the rule. If two keywords have different core terms, they are different buckets, regardless of how similar they seem.
WRONG: "custom kitchen cabinets" as a variant of "cabinet makers." These are different core terms ("kitchen cabinets" vs. "cabinet makers"). They are separate buckets.
WRONG: "bathroom vanity" as a variant of "bathroom cabinets." Different products, different core terms. Separate buckets.
WRONG: "kitchen cabinet showroom" as a variant of "kitchen cabinet store." "Showroom" and "store" are different core terms. Separate buckets.
RIGHT: "kitchen cabinets near me" and "custom kitchen cabinets" are both variants of the "kitchen cabinets" bucket. Same core term, different modifiers.
How to Build a Bucket
Every bucket must have exactly this structure:
- Base keyword (variant_type: "base") — the core term with no modifiers. Example: "kitchen cabinets". This is the anchor row. Every bucket must have one, even if the base term alone has low search volume.
- Near me variant (variant_type: "near_me") — the core term plus "near me". Example: "kitchen cabinets near me". Usually the highest-volume keyword in the bucket.
- Additional variants — other modifiers applied to the same core term. Example: "custom kitchen cabinets", "kitchen cabinets store".
The strategy site renderer groups variants under the base keyword row. If the base keyword is missing from the data, the renderer loses its anchor and incorrectly nests the variants under the previous bucket. This has caused repeated visual bugs. Every bucket must have a base keyword entry in the data, even if that keyword is not selected for the matrix.
Bucket Examples for Common Industries
| Industry | Correct Separate Buckets | Common Mistake |
|---|---|---|
| Kitchen & Bath Showroom | kitchen cabinets / bathroom cabinets / bathroom vanity / countertops / granite countertops / quartz countertops | Grouping bathroom vanity under bathroom cabinets |
| Tree Service | tree service / tree removal / tree trimming / stump removal / stump grinding | Grouping stump grinding under stump removal |
| Roofing | roofing contractor / roof repair / roof replacement / roof installation | Grouping roof repair under roofing contractor |
Before advancing to Step 4, confirm:
- Every keyword from Step 2 is assigned to exactly one bucket
- Every bucket has a base keyword entry
- No bucket contains keywords with different core terms
- Each bucket represents a distinct product or service (not a modifier variation)
- Buckets align with the client's business model classification from Step 1
Market Analysis
Build the full list of candidate markets within the client's service area. Sort and tier them by population. This list is the input to the market selection formula in Step 5.
Step 1 is complete. You have:
- HQ city and state
- Service area description (e.g., "Serving the Greater Phoenix Area" or "Serving within 35 miles of Bellevue, WA")
- Any explicitly excluded cities from the user
Building the Market List
Run scripts/market_analysis.py for the client's county or region. For multi-county service areas, run per county and combine. Filter to cities within approximately 35-40 miles of HQ.
Always include county seat cities even if their population falls below the tier thresholds. County seats carry disproportionate commercial search demand.
Tiering Rules
| Tier | Population Rule | Notes |
|---|---|---|
| Tier 1 | HQ city (always) + any city with population above 40,000 | Primary targets. Maximum search volume. |
| Tier 2 | Population 10,000 to 40,000 | Secondary targets. Good volume, less competition. |
| Tier 3 | Population below 10,000 | Expansion targets. Low volume but easy wins. |
Exclusion Rules
Remove any city that the user has explicitly excluded. Document the exclusion and the reason in the strategy notes. Do not add excluded cities to the Not Used section of the strategy site — exclusions are operational notes, not keyword/market decisions.
Before advancing to Step 5, confirm:
- All cities within the service area are listed
- Every city has a verified population figure from a credible source (Census, state estimates)
- Cities are sorted by Tier, then by population descending within each tier
- HQ city is at the top of Tier 1
- Excluded cities are removed and documented
Keyword Selection
Select the final keywords and markets for the strategy. This step produces the exact combination count required by the plan level. Every decision here must be defensible.
Steps 1 through 4 are complete. You have:
- Organized keyword buckets with search volumes (from Step 3)
- Tiered market list sorted by population (from Step 4)
- The plan level and its required combination count (from user intake)
The Selection Formula
The formula is: Keywords x Markets = Plan Combinations. You must decide how many keywords and how many markets to use. The two strategies are:
| Strategy | When to Use | Example (Level A, 20 combos) |
|---|---|---|
| Go Wide | 1-2 keywords dominate search volume. The business has a narrow, focused service offering. | 1 keyword x 20 markets, or 2 keywords x 10 markets |
| Go Deep | Multiple distinct service lines each have meaningful search volume. The business has a broad offering. | 4 keywords x 5 markets, or 5 keywords x 4 markets |
Keyword Selection Rules
- One keyword per service line maximum. Select the highest-volume base keyword from each service line bucket. Do not select two keywords from the same bucket.
- Maximize service line coverage. A lower-volume keyword for a distinct service line is more valuable than a higher-volume variant of an already-covered line.
- Business model alignment is mandatory. The selected keywords must match the client's business model. A retail showroom never gets "cabinet makers" or "remodeling contractor." A contractor never gets "showroom."
- The near me variant is almost always the right choice. "Kitchen cabinets near me" (74,000/mo) beats "kitchen cabinets" (201,000/mo) for local SEO because it has stronger local commercial intent.
Market Selection Rules
- Calculate the number of markets first: Plan Combinations / Number of Selected Keywords = Number of Markets.
- Select top-down by population, strictly. Take the top N markets from the sorted Tier 1 list, then Tier 2 if needed. No skipping. No cherry-picking.
- HQ city is always market #1. No exceptions.
- Honor all user exclusions. If the user said "not Seattle," Seattle is not in the list regardless of its population rank.
In past strategies, markets were selected inconsistently between versions of the same strategy. This is not acceptable. The formula produces one correct answer. Renton (108,429) always beats Issaquah (40,051) when selecting by population. There is no judgment call here.
Not Used Keywords
Every keyword that was researched but not selected must be placed in the Not Used section of the strategy site with a clear reason. Valid reasons are:
- Incorrect business model — the keyword implies a business model the client does not have
- Reserved for plan expansion — valid keyword but the plan level does not have room for it
- Low search volume — below the threshold to justify a combination slot
- Outside service area — the keyword implies a geography the client does not serve
Before advancing to Step 6, confirm:
- Keywords selected x Markets selected = Plan combination count exactly
- No two selected keywords are from the same bucket
- All selected keywords match the client's business model
- Markets are selected in strict top-down population order
- HQ city is market #1
- All user exclusions are honored
- Every non-selected keyword has a documented reason for not being selected
Matrix Generation
Build the keyword-city combination matrix. This is the core deliverable of the strategy. Every cell represents one page that will be built for the client.
Step 5 is complete. You have:
- Final list of selected keywords
- Final list of selected markets
- Confirmed combination count matching the plan level
Matrix Orientation Rule
The matrix has two valid orientations. Choose based on the number of markets:
| Markets Count | Orientation | Reason |
|---|---|---|
| 5 or fewer | Keywords as rows, cities as columns | Fits cleanly in the standard table width |
| 6 or more | Cities as rows, keywords as columns | City names are too long to use as column headers without overlapping |
The grand total row at the bottom of the matrix uses a special CSS class (.grand-total-row) that overrides the default hover highlight. If this row changes color on hover, the CSS rule is missing. Check the style.css reference file.
Enforcement Rules
- Total combinations must equal the plan level exactly. Count every cell.
- No keyword may appear in both the matrix AND the Not Used section.
- No Not Used keyword may appear in the Additional Opportunities section.
- Row and column totals must be mathematically accurate.
Before advancing to Step 7, confirm:
- Matrix orientation matches the market count rule
- Total cell count equals the plan combination count exactly
- Row totals and column totals are accurate
- No keyword appears in both the matrix and Not Used
- Grand total row CSS is applied correctly
Site Assembly
Build the branded strategy website using the canonical reference templates. Do not build from scratch. Do not use a React/Vite scaffold. Use the templates.
Steps 1 through 6 are complete. You have all strategy data finalized:
- Client data (name, HQ, services, business model)
- Keyword buckets with all variants and search volumes
- Tiered market list (all markets, not just selected ones)
- Selected keywords and markets
- Combination matrix with correct counts
- Not Used keywords with reasons
- Additional Opportunities (upgrade paths) with correct plan pricing
File Structure
Every client strategy site lives at: /home/ubuntu/seo-strategy-repo/{clientdomain}/
The four required files are:
| File | Source | What to Customize |
|---|---|---|
index.html | Copy from references/index_template.html | Update the <base href> tag to the client domain path |
app.js | Copy from references/app_template.js | Replace the STRATEGY constant with the client's data |
charts.js | Copy from references/charts_template.js | Update market data array and selectedCount |
style.css | Copy from references/style_reference.css | No changes. Copy as-is. |
Strategy sites are pure HTML/CSS/JS. They are not React apps. Do not use the Manus web scaffold. Write the four files directly into the client folder. Using the scaffold creates a React project that cannot be deployed to the seo-strategy-sites GitHub repo.
Writing Rules (Non-Negotiable)
- No em dashes or en dashes anywhere. Not in prose, not in labels, not in card text. Use a period, comma, colon, or parentheses instead.
- Plan hero text uses a colon: "Plan Level A: 20 Keyword-City Combinations" — not a dash.
- Tier labels use a colon: "TIER 1: PRIMARY MARKETS" — not a dash or em dash.
- Price fields are numeric: Store
900not"$900/mo". The renderer adds the formatting. - new_market is boolean: Store
trueorfalse, not a string.
opp-card Structure (Critical)
Every Additional Opportunities card must contain exactly 8 child divs in this order:
- opp-plan-label
- opp-price
- opp-combos-large
- opp-combos
- opp-headline
- opp-desc
- opp-new-market (use
style="visibility:hidden"if no new markets are added) - opp-kw-list
If any card has fewer than 8 children, the CSS subgrid alignment breaks for the entire row.
Before advancing to Step 8, confirm:
- All four files exist in the client folder
- style.css is an unmodified copy of the reference file
- The STRATEGY constant in app.js contains all client data
- No em dashes or en dashes in any user-visible text
- All price fields are numeric values
- All opp-cards have exactly 8 child divs
- The base href in index.html matches the client domain path
Visual QA
Render the site locally and verify every section before deploying. Do not deploy a site that has not been visually reviewed. Fixing errors after deployment is slower and riskier.
Step 7 is complete. All four site files exist in the client folder. The site can be rendered locally via a Python HTTP server.
How to Run the Local Preview
cd /home/ubuntu/seo-strategy-repo
python3 -m http.server 8090
# Then navigate to http://localhost:8090/{clientdomain}/
What to Check
| Section | What to Verify |
|---|---|
| Hero card | Client name, plan level text (colon format), combination count correct |
| Stat cards | Numbers match: combinations, keywords, markets |
| Market table | Correct cities, correct populations, correct tier assignments, HQ city first |
| Bar chart | All selected markets highlighted, correct selectedCount |
| Keyword table | Each bucket is a separate group, base keyword is the first row, variants are indented, no cross-bucket nesting |
| Matrix | Correct orientation (cities as rows if 6+ markets), correct combination count, grand total row does not highlight on hover |
| Not Used cards | No keyword appears in both the matrix and Not Used, every card has a reason |
| Opportunities cards | Correct prices (from Plan Levels page), correct combination counts, each card has 8 child divs, card heights align horizontally |
| Mobile layout | Resize browser to 375px width. All card grids should stack vertically. |
Before advancing to Step 9, confirm every item in the table above has been visually verified. Do not rely on the data being correct. Look at the rendered output.
- Hero section renders correctly with no formatting errors
- All stat card numbers are accurate
- Keyword table shows correct bucket groupings with no cross-bucket nesting
- Matrix orientation is correct for the market count
- Opportunities cards show correct prices from the Plan Levels table
- Card grids align horizontally (subgrid working)
- Mobile layout stacks cleanly
Deployment
Push the verified site to GitHub. Vercel auto-deploys within 1-2 minutes. Verify the live URL before delivering to the user.
Step 8 is complete. The site has been visually reviewed locally and all QA items are confirmed.
Deployment Steps
- Pull latest from GitHub to avoid conflicts:
cd /home/ubuntu/seo-strategy-repo && git pull origin main - Add the client entry to the
clientsarray in the rootindex.html - Update the README.md client table
- Commit and push:
git add .
git commit -m "feat: Add [Client Name] SEO strategy site"
git push origin main - Wait 60-90 seconds for Vercel to deploy
- Navigate to
https://seo-strategy.dotcomdesign.com/{clientdomain}/and verify the live site renders correctly
GitHub repo: https://github.com/dotcomdesigniowa/seo-strategy-sites. Vercel auto-deploys from the main branch. Do not manually trigger Vercel deployments. Push to GitHub and wait.
The strategy is complete when all of the following are true:
- Live site renders correctly at seo-strategy.dotcomdesign.com/{clientdomain}/
- Client entry added to root index.html
- README.md updated
- All changes committed and pushed to GitHub
Data & CSS Rules
Non-negotiable rules for data formatting and CSS layout. These rules exist because violations have caused real bugs in deployed strategy sites.
Writing Rules
| Rule | Wrong | Right |
|---|---|---|
| No em dashes or en dashes | "Level B adds two keywords — near me and showroom — across 5 markets." | "Level B adds two keywords (near me and showroom) across 5 markets." |
| Plan hero uses colon | "Plan Level A - 20 Keyword-City Combinations" | "Plan Level A: 20 Keyword-City Combinations" |
| Tier labels use colon | "TIER 1 — PRIMARY MARKETS" | "TIER 1: PRIMARY MARKETS" |
Data Type Rules
| Field | Wrong Type | Correct Type | Example |
|---|---|---|---|
| price | String | Number | 900 not "$900/mo" |
| new_market | String | Boolean | true not "true" |
| variant_type | Missing | String | "base", "near_me", or "variant" |
CSS Subgrid Rules
All card grids use CSS Subgrid for horizontal alignment. The pattern is:
- Parent container defines grid-template-rows with one track per card element
- Each card uses grid-row: span N and grid-template-rows: subgrid
- At max-width: 900px, all cards fall back to display: flex; flex-direction: column; grid-row: span 1
The opp-card specifically uses 8 tracks and grid-row: span 8. If a card has fewer than 8 children, the subgrid breaks for the entire row.
Common Errors
Every error listed here has occurred in a real strategy. Read this before starting any new strategy.
Including services not on the client website
What happened: "Cabinet makers" was included as a keyword for a retail showroom client. The term does not appear anywhere on the client's website and implies a manufacturing business model, not a retail showroom.
The rule: Only include services that are explicitly stated on the client's website. The website is the only source of truth.
Grouping keywords with different core terms into the same bucket
What happened: "Bathroom vanity near me" was placed as a variant under the "bathroom cabinets" bucket. These are different products with different core terms and must be separate buckets.
The rule: A keyword is only a variant if it shares the exact same core term. Different words = different buckets.
Missing base keyword causes incorrect rendering
What happened: Countertop keywords were placed in a bucket with no base keyword entry. The renderer lost its grouping anchor and nested all countertop keywords under the previous bucket (laundry room cabinets).
The rule: Every bucket must have a variant_type: "base" entry, even if the base term alone has low search volume and will not be selected.
Arbitrary market selection
What happened: Issaquah (population 40,051) was selected while Renton (population 108,429) was skipped. There was no documented reason for this decision.
The rule: Markets are selected in strict top-down population order. The formula produces one correct answer. There is no judgment call.
Selecting a lower-volume keyword when a higher-volume service line is available
What happened: "Kitchen cabinet showroom" (1,900/mo) was selected over "bathroom cabinets near me" (12,100/mo). The lower-volume keyword was for a service line already covered. The higher-volume keyword opened a new service line.
The rule: A lower-volume keyword for a distinct service line beats a higher-volume variant of an already-covered line.
Invented plan prices
What happened: Plan prices in the Additional Opportunities section were invented from memory instead of read from the Plan Levels table. The prices were wrong.
The rule: Always read the Plan Levels table in this Playbook before writing any price into the strategy data. Never estimate or recall prices from memory.
Wrong matrix orientation with 6+ markets
What happened: A strategy with 10 markets used keywords as rows and cities as columns. City names in column headers overlapped and became unreadable.
The rule: When markets is 6 or more, always use cities as rows and keywords as columns.
QA Checklist
Complete this checklist before delivering any strategy. Every item must be checked. This checklist is the final gate before the strategy is considered done.
Check each item as you verify it. Progress is shown at the top. The checklist resets when you navigate away.