SEO Operations Playbook
The complete system for how we research, build, and deliver local SEO strategies for Dotcom Design clients.
The Philosophy
The Dotcom Design SEO Strategy system exists to produce consistent, data-driven, and defensible local SEO strategies for our clients. It replaces a multi-hour manual process prone to subjective decisions with a systematic workflow that ensures every strategy is built on a foundation of clear rules and verifiable data.
The system's primary output is a client-facing, branded website that presents the final keyword-city matrix and demonstrates the rigorous research that produced it. This is not just a deliverable; it is a sales tool that shows the client exactly what they are paying for and why.
Why Rules Matter
Every error that has occurred in past strategies came from one of three root causes: logic gaps (no rule existed to prevent a bad decision), data structure bugs (a rendering assumption was not documented), or pricing from memory (numbers were invented instead of referenced). This Playbook exists to eliminate all three. Every decision point has a documented answer. Adherence is not optional.
The Output
Every completed strategy produces two deliverables: a live, branded strategy website at seo-strategy.dotcomdesign.com/{clientdomain} and a keyword-city matrix that is pasted into the Build Prompt for the website build. The strategy website is shared directly with the client and serves as the documented rationale for every page that will be built.
Plan Levels & Pricing
price field in every app.js file must match this table exactly.
| Level | Price | Combinations | Notes |
|---|---|---|---|
| SEO Booster | $399/mo | 10 | Entry-level. Typically 1-2 keywords x 5-10 markets. |
| A | $600/mo | 20 | Most common starting plan for multi-service clients. |
| B | $900/mo | 30 | First upgrade. Adds keywords or markets. |
| C | $1,200/mo | 40 | |
| D | $1,600/mo | 50 | |
| E | $2,000/mo | 60 | |
| F | $3,000/mo | 90 | |
| G | $4,000/mo | 120 | |
| H | $5,000/mo | 150 |
The Combination Formula
Every plan's combination count is the product of the number of selected keywords and the number of selected markets. The formula is always: Keywords x Markets = Total Combinations. The plan level determines the total, and the keyword/market split is a strategic decision. For example, Plan A (20 combinations) could be 4 keywords x 5 markets, or 2 keywords x 10 markets, depending on the client's service breadth and geographic footprint.
4 keywords, 5 markets. Use when the client has multiple distinct service lines each with meaningful search volume. Covers more services but in a tighter geographic area.
2 keywords, 10 markets. Use when 1-2 keywords dominate search volume and the client serves a large region. Maximizes geographic reach for the highest-intent terms.
Step 1: Client Data Extraction
| INPUT | Client website URL, plan level, any exclusion notes from the user |
| OUTPUT | Confirmed business name, HQ city, full service list, service area, existing city pages |
| TIME | 5-10 minutes |
What to Extract
Browse the client's website thoroughly. The goal is to build a complete picture of the business before any keyword research begins. Extract the following: the exact business name as it appears on the site, the industry and business model (showroom, contractor, service provider, etc.), the HQ city and state, a complete list of every service or product category offered, and the defined service area or radius.
Sitemap Check
After reviewing the main site, check the sitemap for existing city or service-area pages. Try these URLs in order: /sitemap.xml, /sitemap_index.xml, and /page-sitemap.xml. Record any existing keyword-city combinations. When building the matrix, these combinations should be noted as existing assets in the market table's rationale column.
Required Inputs Table
| Input | Source | Notes |
|---|---|---|
| Client website URL | User | Required to start |
| Plan Level | User | Determines total combinations |
| Service radius or region | User (if not on site) | Ask only if not found on website |
| Exclusions or notes | User (optional) | Any keywords or markets to avoid |
Step 2: Keyword Research & Bucketing
| INPUT | Client service list from Step 1 |
| OUTPUT | Full keyword list organized into tight, same-core-term buckets |
| TOOL | scripts/semrush_keyword_research.py (SEMrush API key: 1f834cf506d399c7e0bbb87923381608) |
The Bucketing Process
For each client service line, identify the primary search term. That term becomes the bucket's name and its base keyword. Every other keyword in that bucket must share that exact core term. Modifiers (near me, custom, store, showroom, companies) are added to the core term to create variants. A modifier alone does not create a new bucket; a different core term does.
Correct vs. Incorrect Examples
| Scenario | Correct | Wrong | Why |
|---|---|---|---|
| Cabinet client with showroom and store keywords | Two separate buckets: kitchen cabinet showroom and kitchen cabinet store |
Treating "store" as a variant of "showroom" | "showroom" and "store" are different core terms. Different user intent, different SERP results. |
| Bathroom product client | Two separate buckets: bathroom vanity and bathroom cabinets |
Treating "bathroom cabinets near me" as a variant of "bathroom vanity" | "vanity" and "cabinets" are different products. A user searching for one is not searching for the other. |
| Cabinet client with custom variant | custom kitchen cabinets as a variant of the kitchen cabinets bucket |
Treating "custom kitchen cabinets" as a variant of "cabinet makers" | "custom kitchen cabinets" shares the core term "kitchen cabinets." "Cabinet makers" is a completely different bucket (and a contractor term, not a showroom term). |
| Countertop client | Three separate buckets: countertops, granite countertops, quartz countertops |
Treating "granite countertops near me" as a variant of "countertops near me" | "granite countertops" and "quartz countertops" are distinct material-specific searches. They have different SERPs and different buyer intent. |
The Base Keyword Requirement
Every bucket in the keyword_table data array must have exactly one entry with variant_type: "base". This entry is the rendering anchor for the entire family. If a family only has near-me or modifier variants and no base keyword is explicitly added to the data, the rendering function loses its grouping anchor and will incorrectly nest those rows under the previous family in the table. This has caused repeated visual bugs and must be enforced without exception.
// CORRECT
{ family: "countertops", keyword: "countertops", variant_type: "base", ... },
{ family: "countertops", keyword: "countertops near me", variant_type: "near_me", ... },
{ family: "granite countertops", keyword: "granite countertops", variant_type: "base", ... },
{ family: "granite countertops", keyword: "granite countertops near me", variant_type: "near_me", ... },
// WRONG - missing base keyword for granite countertops family
{ family: "countertops", keyword: "countertops", variant_type: "base", ... },
{ family: "countertops", keyword: "countertops near me", variant_type: "near_me", ... },
{ family: "granite countertops", keyword: "granite countertops near me", variant_type: "near_me", ... },
// ^ This will render "granite countertops near me" nested under the countertops family
Keyword Exclusion Rules
The following keyword types must be excluded from all research output regardless of search volume.
| Exclusion Type | Examples | Reason |
|---|---|---|
| City or state names embedded in keyword | "kitchen cabinets Seattle," "roof repair Texas" | The city is added at the matrix stage, not the keyword stage. |
| Overly broad single terms | "demolition," "cabinets," "roofing" | No commercial intent signal. Impossible to rank competitively. |
| DIY or informational terms | "how to install kitchen cabinets," "cabinet dimensions" | Research intent, not buying intent. |
| Job-seeking terms | "cabinet maker jobs," "roofing contractor hiring" | Wrong audience entirely. |
| Terms outside client's services | Flooring keywords for a cabinet-only client | Creates irrelevant pages that dilute domain authority. |
| Business model mismatches | "cabinet makers" for a showroom client | Implies manufacturing/contracting, not retail. Wrong buyer intent. |
Step 3: Market Analysis & Selection
| INPUT | Client HQ city, service region, total plan combinations, number of selected keywords |
| OUTPUT | Tiered market list with exactly N markets selected in strict population order |
| TOOL | scripts/market_analysis.py |
The Market Selection Formula
The number of markets to select is always calculated as: Total Plan Combinations divided by Number of Selected Keywords. This is a hard formula. Once the number is calculated, select the top N markets from the master list in strict descending order by population, always starting with the client's HQ city.
Number of Markets = Total Plan Combinations / Number of Selected Keywords
Examples:
Plan A (20 combos) with 4 keywords = 5 markets
Plan A (20 combos) with 2 keywords = 10 markets
Plan B (30 combos) with 5 keywords = 6 markets
Plan C (40 combos) with 4 keywords = 10 markets
SEO Booster (10 combos) with 1 keyword = 10 markets
Market Tiering Rules
| Tier | Population Threshold | Notes |
|---|---|---|
| Tier 1 | Population > 40,000 OR the client's HQ city | HQ city is always Tier 1 regardless of population. County seat cities are also elevated to Tier 1 even if below 40,000 due to disproportionate commercial search demand. |
| Tier 2 | Population 10,000 to 40,000 | Secondary markets. Selected after all Tier 1 markets are exhausted. |
| Tier 3 | Population < 10,000 | Rarely selected. Only relevant for very small service regions or high-combination plans. |
Multi-County Regions
For clients serving a multi-county region (e.g., "Western Pennsylvania" or "the Greater Phoenix Area"), run the market analysis script per county and combine the results. Filter to cities within approximately 35 to 40 miles of the client's HQ. Always include county seat cities in the list even if their population falls below the Tier 1 threshold.
Handling Excluded Markets
When a market is explicitly excluded by the client (e.g., "not Seattle"), remove it from the master list entirely before applying the selection formula. Do not count it as one of the selected markets. Document the exclusion in the market table's rationale column and in the Tier 3 card.
Step 4: Keyword Selection
| INPUT | Bucketed keyword list from Step 2, plan combination count |
| OUTPUT | Final list of N selected keywords, one per distinct service line |
Selection Priority Rules
Keyword selection follows a strict priority order. These rules must be applied in sequence.
| Priority | Rule | Rationale |
|---|---|---|
| 1 | Business model alignment first. Only select keywords that match the client's business model. A showroom client never gets contractor keywords. A contractor never gets retail showroom keywords. | A misaligned keyword attracts the wrong buyer. The page will rank but convert at zero. |
| 2 | Maximize service line coverage. Select one keyword per distinct service line before selecting a second keyword from any line already covered. | Covering more service lines creates more revenue opportunities. A second keyword for an already-covered line is a lower-value addition than a first keyword for a new line. |
| 3 | Highest volume per service line. Within each service line, select the keyword with the highest national monthly search volume. | Higher volume means more potential traffic. All else being equal, the highest-volume keyword in a bucket is the best representative of that bucket's intent. |
| 4 | Near-me variants preferred over base terms. When both a base term and a near-me variant exist in a bucket, prefer the near-me variant as the selected keyword for local SEO. | "Kitchen cabinets near me" has explicit local buyer intent. "Kitchen cabinets" has mixed intent including national retailers and DIY content. |
Go Wide vs. Go Deep
Once keywords are selected, the combination formula determines how many markets to target. The strategic decision is whether to go wide (fewer keywords, more markets) or go deep (more keywords, fewer markets). Use the following guidance to make this decision.
| Strategy | When to Use | Example |
|---|---|---|
| Go Wide (1-2 keywords, many markets) | The client's business is defined by one or two dominant service lines. The service region is large. The top 1-2 keywords have significantly higher volume than all others. | Vanities Etc: 1 keyword (bathroom vanity near me, 33,100/mo) x 10 markets = 10 combinations. |
| Go Deep (3-5 keywords, fewer markets) | The client offers multiple distinct service lines each with meaningful search volume. The service region is concentrated around a metro area. | Kitchen Cabinets Etc: 4 keywords x 5 markets = 20 combinations. |
Step 5: Matrix Generation & Site Assembly
| INPUT | Selected keywords, selected markets, all keyword research data |
| OUTPUT | Live strategy website at seo-strategy.dotcomdesign.com/{clientdomain} |
| TECH STACK | Static HTML/CSS/JS. No React, no Vite, no CMS. Do not use webdev_init_project. |
skills/seo-strategist/references/ are the canonical templates. Use them.
File Structure
Every client strategy site lives in its own directory named after the client's domain. The directory is created inside the seo-strategy-repo repository.
/home/ubuntu/seo-strategy-repo/
{clientdomain}/
index.html (from references/index_template.html)
app.js (from references/app_template.js)
charts.js (from references/charts_template.js)
style.css (from references/style_reference.css)
assets/
logo_primary.png
logo_reversed.png
Matrix Orientation Rule
The orientation of the keyword-city matrix is determined by the number of selected markets. This rule exists to prevent city column headers from overlapping when there are many markets. It must be applied automatically based on the market count.
| Number of Markets | Matrix 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 as column headers overlap and become unreadable at 6+ markets. Flipping to cities-as-rows solves this permanently. |
The STRATEGY Data Object
The entire strategy is driven by a single JavaScript constant called STRATEGY in app.js. Every section of the website reads from this object. The structure must be followed exactly. Key fields and their requirements are documented in the Data and CSS Rules section.
Assembly Checklist
- Read
dotcom-design-brandskill and all reference files inseo-strategistskill. - Create the client directory:
mkdir -p /home/ubuntu/seo-strategy-repo/{clientdomain}/ - Copy
style.cssandassets/from an existing client directory. - Write
app.jsusingapp_template.jsas the base. Populate the fullSTRATEGYobject. - Write
charts.jsusingcharts_template.jsas the base. SetselectedCountto the number of selected markets. - Write
index.htmlusingindex_template.htmlas the base. Update the<base href>tag and all client-specific prose. - Preview locally with
python3 -m http.server 8090and verify all sections render correctly. - Scroll through every section: Overview, Markets, Keywords, Matrix, Not Used, Opportunities.
Step 6: Deployment
| REPO | https://github.com/dotcomdesigniowa/seo-strategy-sites |
| LIVE URL | https://seo-strategy.dotcomdesign.com/{clientdomain} |
| DEPLOY METHOD | GitHub push triggers Vercel auto-deploy. Live within 1-2 minutes. |
Deployment Steps
- Pull the latest changes:
cd /home/ubuntu/seo-strategy-repo && git pull origin main - Confirm all client files are in place at
seo-strategy-repo/{clientdomain}/ - Add the new client to the
clientsarray in the rootindex.html. - Add the client to the table in
README.md. - Stage, commit, and push:
cd /home/ubuntu/seo-strategy-repo
git add {clientdomain}/
git add index.html README.md
git commit -m "feat: Add [Client Name] SEO strategy site"
git push origin main
After pushing, wait approximately 40-60 seconds for Vercel to complete the deployment, then navigate to the live URL to verify. Check the hero, markets table, keyword table, matrix, not-used section, and opportunities cards.
Data & CSS Rules
Writing Rules (Non-Negotiable)
| Wrong | Correct |
|---|---|
"Level B adds two keywords — Insurance Agency Near Me and Renters Insurance — across all 5 markets." | "Level B adds two keywords (Insurance Agency Near Me and Renters Insurance) across all 5 markets." |
TIER 1 — PRIMARY MARKETS | TIER 1: PRIMARY MARKETS |
Plan Level C — 40 Keyword-City Combinations | Plan Level C: 40 Keyword-City Combinations |
app.js Data Field Rules
| Field | Required Type | Wrong | Correct |
|---|---|---|---|
price | Numeric (no formatting) | "$900/mo" | 900 |
new_market | Boolean | "true" | true |
variant_type (base) | Every family must have one | Family with only "near_me" entries | Family with one "base" entry plus variants |
CSS Subgrid Rules
All card grids in the strategy site use CSS Subgrid for horizontal alignment. This is not optional. The pattern requires two components: the parent grid defines the row tracks, and each card uses grid-template-rows: subgrid to inherit those tracks.
/* Parent grid defines tracks */
.opportunities-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-template-rows: auto auto auto auto auto 1fr minmax(24px, auto) auto; /* 8 tracks */
}
/* Card inherits tracks via subgrid */
.opp-card {
display: grid;
grid-row: span 8;
grid-template-rows: subgrid;
}
/* Mobile fallback - REQUIRED */
@media (max-width: 900px) {
.opp-card {
display: flex;
flex-direction: column;
grid-row: span 1;
}
}
The opp-card 8-Child Rule
Every .opp-card must contain exactly 8 child divs in this exact order. If a plan level does not add new markets, the .opp-new-market div must still be rendered with style="visibility:hidden" as a placeholder to maintain subgrid alignment.
<div class="opp-card">
<div class="opp-plan-label">...</div> <!-- 1 -->
<div class="opp-price">...</div> <!-- 2 -->
<div class="opp-combos-large">...</div> <!-- 3 -->
<div class="opp-combos">...</div> <!-- 4 -->
<div class="opp-headline">...</div> <!-- 5 -->
<div class="opp-desc">...</div> <!-- 6 -->
<div class="opp-new-market">...</div> <!-- 7: use visibility:hidden if no new market -->
<div class="opp-kw-list">...</div> <!-- 8 -->
</div>
Common Errors & How to Prevent Them
Every error documented here has occurred in a real strategy build. Each entry describes what went wrong, why it happened, and the specific rule that prevents it from happening again.
What happened: A keyword was selected that implied the client was a manufacturer or craftsman, when the client was a retail showroom.
Root cause: No rule existed requiring keyword alignment with the client's business model as identified in Step 1.
Prevention rule: Every selected keyword must be validated against the business model identified in Step 1. If a keyword implies a business model the client does not have, it goes in the Not Used section under "Incorrect Business Model," not in the matrix.
What happened: Four countertop keywords appeared visually nested under the laundry room cabinets family in the keyword table.
Root cause: The countertop families had no variant_type: "base" entry. The rendering function lost its grouping anchor and attached those rows to the previous family.
Prevention rule: Every keyword family in the keyword_table data array must have exactly one entry with variant_type: "base". This is required even if the base keyword itself is not selected for the matrix.
What happened: The opportunities cards showed incorrect prices (e.g., $800/mo instead of $900/mo for Plan B).
Root cause: Prices were written from memory instead of being read from the canonical pricing table.
Prevention rule: Before writing any opportunities data, read the Plan Levels and Pricing section of this Playbook. The price field must be a numeric value matching the table exactly. Never write a price from memory.
What happened: A smaller market (Issaquah, pop. 40,051) was selected while a significantly larger market (Renton, pop. 108,429) was skipped.
Root cause: Market selection was based on subjective judgment rather than the deterministic population-ordered formula.
Prevention rule: Markets are selected in strict top-down order by population. No market may be skipped unless it is explicitly excluded by the client. If a market is not next in the ranked list, it is not selected.
What happened: "Kitchen cabinet showroom" (1,900/mo) was selected over "bathroom cabinets near me" (12,100/mo), missing an entirely new service line.
Root cause: Keyword selection prioritized a variant within an already-covered service line over a new service line.
Prevention rule: Always maximize service line coverage before selecting a second keyword from any already-covered line. A lower-volume keyword for a new service line is more valuable than a higher-volume variant of an already-covered line.
What happened: A matrix with 10 markets used keywords-as-rows and cities-as-columns, causing city names to overlap and become unreadable.
Root cause: The orientation rule was not applied.
Prevention rule: When the number of selected markets is 6 or more, the matrix must use cities-as-rows and keywords-as-columns. This is enforced automatically by the buildMatrix() function in the template, but must be verified during the local preview step.
QA Checklist Live
Complete every item on this checklist before delivering a strategy. A strategy is not complete until every box is checked. Click each item to mark it complete.