v1.0 · March 2026
INTERNAL OPERATIONS DOCUMENT

SEO Operations Playbook

The complete system for how we research, build, and deliver local SEO strategies for Dotcom Design clients.

Version: 1.0 Updated: March 2026 Prepared for: Head of Operations Status: Active
How to use this playbook: Use the left sidebar to navigate between sections. The QA Checklist is interactive. All code blocks have a Copy button. This playbook is the single source of truth for all SEO strategy work.

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.

Source of Truth: This table is the only authorized source for SEO plan pricing. Never use prices from memory, estimates, or any other source. The price field in every app.js file must match this table exactly.
LevelPriceCombinationsNotes
SEO Booster$399/mo10Entry-level. Typically 1-2 keywords x 5-10 markets.
A$600/mo20Most common starting plan for multi-service clients.
B$900/mo30First upgrade. Adds keywords or markets.
C$1,200/mo40
D$1,600/mo50
E$2,000/mo60
F$3,000/mo90
G$4,000/mo120
H$5,000/mo150

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.

EXAMPLE: Plan A (20 combinations)
Go Deep (4 x 5)

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.

Go Wide (2 x 10)

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.

INPUTClient website URL, plan level, any exclusion notes from the user
OUTPUTConfirmed business name, HQ city, full service list, service area, existing city pages
TIME5-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.

Business Model Alignment: The business model identified in this step is the single most important input to keyword selection. A showroom client and a contractor client may offer similar products but require completely different keywords. "Cabinet makers" is a contractor keyword. "Kitchen cabinet showroom" is a retail keyword. Misidentifying the business model produces an entirely wrong keyword set.

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

InputSourceNotes
Client website URLUserRequired to start
Plan LevelUserDetermines total combinations
Service radius or regionUser (if not on site)Ask only if not found on website
Exclusions or notesUser (optional)Any keywords or markets to avoid
INPUTClient service list from Step 1
OUTPUTFull keyword list organized into tight, same-core-term buckets
TOOLscripts/semrush_keyword_research.py (SEMrush API key: 1f834cf506d399c7e0bbb87923381608)
The Cardinal Rule of Bucketing: A keyword is only a variant within a bucket if it shares the exact same core term. Different core terms, no matter how related the products may be, always form separate buckets. This rule has no exceptions.

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

ScenarioCorrectWrongWhy
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: Every family has a base keyword
// 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 TypeExamplesReason
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 servicesFlooring keywords for a cabinet-only clientCreates irrelevant pages that dilute domain authority.
Business model mismatches"cabinet makers" for a showroom clientImplies manufacturing/contracting, not retail. Wrong buyer intent.
INPUTClient HQ city, service region, total plan combinations, number of selected keywords
OUTPUTTiered market list with exactly N markets selected in strict population order
TOOLscripts/market_analysis.py
Market selection must be deterministic. There is no room for judgment or preference in this step. The formula produces a number, and the population-ranked list produces the markets. If a market is selected that is not next in the ranked list, the selection is wrong.

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.

MARKET SELECTION FORMULA
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

TierPopulation ThresholdNotes
Tier 1Population > 40,000 OR the client's HQ cityHQ 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 2Population 10,000 to 40,000Secondary markets. Selected after all Tier 1 markets are exhausted.
Tier 3Population < 10,000Rarely 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.

INPUTBucketed keyword list from Step 2, plan combination count
OUTPUTFinal 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.

PriorityRuleRationale
1Business 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.
2Maximize 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.
3Highest 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.
4Near-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.

StrategyWhen to UseExample
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.
INPUTSelected keywords, selected markets, all keyword research data
OUTPUTLive strategy website at seo-strategy.dotcomdesign.com/{clientdomain}
TECH STACKStatic HTML/CSS/JS. No React, no Vite, no CMS. Do not use webdev_init_project.
Read the brand skill and all reference files BEFORE writing a single line of code. The most common failure mode is building a custom design instead of the Dotcom Design template. The reference files at 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.

DIRECTORY STRUCTURE
/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 MarketsMatrix OrientationReason
5 or fewerKeywords as rows, cities as columnsFits cleanly in the standard table width.
6 or moreCities as rows, keywords as columnsCity 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

  1. Read dotcom-design-brand skill and all reference files in seo-strategist skill.
  2. Create the client directory: mkdir -p /home/ubuntu/seo-strategy-repo/{clientdomain}/
  3. Copy style.css and assets/ from an existing client directory.
  4. Write app.js using app_template.js as the base. Populate the full STRATEGY object.
  5. Write charts.js using charts_template.js as the base. Set selectedCount to the number of selected markets.
  6. Write index.html using index_template.html as the base. Update the <base href> tag and all client-specific prose.
  7. Preview locally with python3 -m http.server 8090 and verify all sections render correctly.
  8. Scroll through every section: Overview, Markets, Keywords, Matrix, Not Used, Opportunities.
REPOhttps://github.com/dotcomdesigniowa/seo-strategy-sites
LIVE URLhttps://seo-strategy.dotcomdesign.com/{clientdomain}
DEPLOY METHODGitHub push triggers Vercel auto-deploy. Live within 1-2 minutes.

Deployment Steps

  1. Pull the latest changes: cd /home/ubuntu/seo-strategy-repo && git pull origin main
  2. Confirm all client files are in place at seo-strategy-repo/{clientdomain}/
  3. Add the new client to the clients array in the root index.html.
  4. Add the client to the table in README.md.
  5. Stage, commit, and push:
DEPLOYMENT COMMANDS
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.

Writing Rules (Non-Negotiable)

No em dashes or en dashes anywhere in any user-visible text. This applies to all prose, descriptions, card labels, tier labels, hero text, and any string that renders on screen. Use a period, comma, colon, or parentheses instead.
WrongCorrect
"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 MARKETSTIER 1: PRIMARY MARKETS
Plan Level C — 40 Keyword-City CombinationsPlan Level C: 40 Keyword-City Combinations

app.js Data Field Rules

FieldRequired TypeWrongCorrect
priceNumeric (no formatting)"$900/mo"900
new_marketBoolean"true"true
variant_type (base)Every family must have oneFamily with only "near_me" entriesFamily 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.

CSS SUBGRID PATTERN
/* 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.

OPP-CARD REQUIRED CHILD ORDER
<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>

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.

ERROR Wrong business model keyword selected ("cabinet makers" for a showroom client)

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.

ERROR Countertop keywords nested inside laundry room cabinets bucket

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.

ERROR Wrong plan prices in the opportunities section

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.

ERROR Arbitrary market selection (skipping Renton to select Issaquah)

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.

ERROR Lower-volume keyword selected over a higher-volume keyword from a new service line

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.

ERROR Matrix orientation wrong with 10 markets (cities as columns)

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.

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.

PRE-BUILD
KEYWORD RESEARCH
MARKET SELECTION
MATRIX
OPPORTUNITIES CARDS
CSS & FORMATTING
DEPLOYMENT
0 / 30 complete