# Apex Stub Pages + Dealer Directory - Design Spec

**Date:** 2026-06-09
**Branch:** `amsoil-seo-work` (coordinate merge with `amsoil-robots-txt`)
**Status:** Approved design, pending implementation plan

## 1. Problem

Google treats the platform's dealer pages as thin affiliation / doorway-adjacent content:

- 1,099 active dealers; only ~678 subdomain dealers indexed (~70%). Custom-domain dealers index at ~95%.
- Two stuck GSC states: **"Discovered – currently not indexed"** (orphaned - dealer subdomains have zero crawlable inbound links; sitemap entries pass no equity) and **"Crawled – currently not indexed"** (template-duplication verdict - out of scope here, separate content track).
- All four apex domains serve the same AIMCLEAR-branded `public/index.html`, which (a) poisons site-name derivation for every dealer subdomain ("Aimclear" in SERPs) and (b) reads as "multiple domain names that funnel users" - Google's literal doorway-page definition.

**This build fixes the orphaning and apex-identity problems.** Per-dealer content differentiation (the thin-affiliation verdict) is a separate, larger track.

## 2. Goals & KPIs

1. Every active dealer gets ≥1 crawlable, followed, descriptive inbound link from their apex domain.
2. Each apex carries a distinct, consumer-first identity (correct site-name derivation, no AIMCLEAR branding).
3. KPI: joe@'s SerpApi indexation monitor (ok / WRONG-NAME / NOT-INDEXED per sitemap host) + GSC indexation coverage. Expect site-name changes to lag recrawl by days–weeks.

## 3. Decisions log (do not re-litigate)

| Decision                                          | Resolution                                                                                                                                   |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Noindex root domains                              | **Rejected** (boss) - stubs and directory must be indexable                                                                                  |
| Redirect apexes to amsoil.aimclear.com            | **Rejected** - wrong brand identity                                                                                                          |
| Per-dealer profile pages on apex (Zillow pattern) | **Rejected** - beyond scope; each dealer's own site _is_ the product                                                                         |
| Noindex thin active dealers                       | **Rejected** - gate applies only to non-active statuses                                                                                      |
| New dealer profile fields                         | **Rejected** - use existing `description`, `logoUrl`, NAP                                                                                    |
| Rollout env flag (`STUB_ENABLED_DOMAINS`)         | **Rejected** - validate on dev + staging, then cut all 4 domains at once                                                                     |
| Stub audience                                     | Consumer-first ("find a dealer"); dealer signup secondary                                                                                    |
| Public directory path                             | `/dealers` - internal dealer-site route renames to `/sites`                                                                                  |
| Hub structure                                     | Locator + spotlight (mockup option C)                                                                                                        |
| Stub layout                                       | Funnel + trust content (mockup option B)                                                                                                     |
| State page layout                                 | City-grouped list + sticky city jump nav (mockup option C)                                                                                   |
| Spotlight selection                               | Quality gate (logo + description + customized page) → tier-weighted, date-seeded rotation within pool. No need-weighting; no pure tier perk. |
| Geo personalization ("dealers near you")          | Roadmap fast-follow - client-side post-hydration enhancement only; static render stays canonical                                             |

## 4. Architecture

### 4.1 Route rename: `/dealers/*` → `/sites/*` (internal)

- `app/dealers/[subdomain]/[domainSlug]/` (incl. `[...slug]`, `og/`) moves to `app/sites/[subdomain]/[domainSlug]/`.
- `proxy.ts` rewrite targets update (subdomain routing ~L699–854, custom-domain routing ~L677, slug handling ~L235–277).
- The protective block at `proxy.ts:513–562` (method guard, bot-probe blocking, 301-to-canonical-subdomain, ambiguous-subdomain 404) **moves to the `/sites/` prefix**. The `/dealers/` interception is **removed** - otherwise `/dealers/texas` would be interpreted as a dealer subdomain lookup. Regression tests required for both halves.
- Apex robots.txt platform disallow swaps `/dealers/` → `/sites/`. **Build on the `amsoil-robots-txt` branch's `app/robots.ts`** (platform disallow: `/api/, /dashboard/, /auth/, /admin/, /dev/, /dev-tools/, /sites/`). Never re-add `/_next/` or any `/og` block (the `/og` routes use `X-Robots-Tag: noindex`, which requires crawlability).
- ISR cache keys change with the path; first deploy re-renders all dealer pages (DeployHQ full re-deploy resets ISR anyway).

### 4.2 Apex stub pages

- New App Router route renders the stub when `parseHostContext()` returns null on a production apex; `proxy.ts` routes apex `/` to it instead of `public/index.html` for the 4 production domains.
- `public/index.html` remains the page for `amsoil.aimclear.com` only (dealer-acquisition audience). The unwired `app/api/landing/route.ts` `transformForHost()` experiment is **retired/deleted**.
- Authenticated users: existing proxy redirect `/` → `/dashboard` is untouched. Old `/landing` → `/` redirect untouched.
- Content driven by `lib/apex-config.ts`: one typed entry per domain - positioning copy, hero, trust-band themes, region band, meta/OG, JSON-LD identity, hreflang pair. A missing field for any domain fails CI (this enforces "four distinct copy sets").

**Layout (option B, all domains share skeleton):** header (logo, Find a Dealer, For Dealers) → hero (headline + Find-a-dealer CTA) → 3-card trust band → popular states/provinces band → dealer-signup band (links `/register`) → footer (terms, etc.). Tailwind + Inter, mobile-first per `docs/STYLING_GUIDE.md`; landing-page aesthetic, not CMS design tokens.

**Per-domain positioning:**

| Domain         | Positioning                       | Trust-band themes                                                | Region band |
| -------------- | --------------------------------- | ---------------------------------------------------------------- | ----------- |
| myamsoil.com   | Relationship: "your local dealer" | personal service · local expertise · dealer relationships        | US states   |
| shopamsoil.com | Purchase intent: "where to buy"   | product lines · how dealer purchasing works · specialty matching | US states   |
| myamsoil.ca    | Relationship, Canada              | cold-climate performance · provinces · personal service          | Provinces   |
| shopamsoil.ca  | Purchase intent, Canada           | product lines · Canadian availability · specialty matching       | Provinces   |

Copy is written individually per domain - no `{country}`-slot templating. Acceptance criterion: four genuinely distinct copy sets.

### 4.3 Directory: hub + state pages

**Hub `/dealers` (per apex):**

1. Hero: headline + search input (client-side filter over an embedded lightweight JSON index - name/city/region/url for every active dealer on the apex, ~60KB at 600 dealers; server HTML remains the canonical crawlable content).
2. Spotlight strip "Featured dealers": pool = active dealers with logo + description + customized `pageContent` (customized = differs from the default template seed; exact predicate decided in the implementation plan); selection tier-weighted, date-seeded deterministic rotation (stable within an ISR cycle).
3. State/province grid with live counts → links to state pages.
4. "States with fewer than 5 dealers": dealers listed inline (every dealer crawlable from the apex regardless of density).

**State pages `/dealers/[region]` (e.g. `/dealers/texas`):**

- Generated only for regions with ≥5 active dealers (config constant). Below-threshold regions live on the hub; grid links to `/dealers#vermont` anchors.
- Breadcrumb → `h1` "AMSOIL Dealers in {Region}" → count/intro line → dealer rows **grouped by city** (`h2` city headings) with sticky city jump-nav sidebar (collapses to chip row on mobile).
- Dealer row: logo, **business name + city/state as the followed anchor** (plain `<a>`, NO nofollow/sponsored/ugc - these links are the point), description excerpt.
- Region slugs from a **fixed state/province table** (US states + CA provinces). Unknown/typo'd `state` values group under "Other locations" on the hub and never mint a page. Invalid region slugs 404 (contrast: amsoil.com serves Michigan cities at `/us/minnesota/` with a 200).
- A region dropping below threshold 404s and falls out of sitemap/grid.

**Cross-border dealers:** domestic regions group first; e.g. a US-address dealer on a `.ca` domain appears in a "US-based dealers" group at the bottom (and vice versa).

### 4.4 Data layer

- `getDealersForApex(prefix, domain, region?)`: `status IN ('active','cancelled_pending') AND subdomain IS NOT NULL`, scoped by `domainPrefix`/`domain`, minus a **single shared exclusion list** of test/demo/reserved subdomains (`test123`, `demo-*`, `joewarnertest`, `dealer`, `http`, `search`, `go`, …) consolidated and reused by sitemap + directory + spotlight.
- Selects: `subdomain, businessName, city, state, country, description, logoUrl, customDomain, customDomainStatus, subscriptionTier, pageContent (customization check only)`.
- Dealer link URLs via the existing canonical-URL helper (custom domain when active, else subdomain; no trailing slash per PR #903).

### 4.5 Caching

- Stubs: ISR, revalidate daily (content is config-driven; changes ship with deploys).
- Hub + state pages: ISR revalidate 1h **plus** on-demand `revalidateTag('directory-{prefix}-{domain}')` on dealer activate/deactivate/subdomain/custom-domain/profile changes.
- Spotlight: seeded by current date → daily rotation, deterministic per render (no `Math.random()` in RSC).

## 5. SEO mechanics

- **Stubs:** per-domain title/description; no-slash canonical; `og:site_name` = domain's own name; per-domain OG image; `WebSite` + `Organization` JSON-LD with the domain's identity (kills "Aimclear" site-name derivation); homepage-only hreflang pairs `en-US`/`en-CA` (`myamsoil.com↔myamsoil.ca`, `shopamsoil.com↔shopamsoil.ca`); GA4 `G-NR1G252PS2` with production-domain guard (ported from index.html).
- **Directory:** `CollectionPage` + `ItemList` + `BreadcrumbList` JSON-LD on hub and state pages; no-slash canonicals; titles "AMSOIL Dealers in {Region} | {DomainName}".
- **Sitemap:** hub + state pages added to each apex sitemap (alongside existing dealer URLs).
- **Robots:** see §4.1; rebase on `amsoil-robots-txt`.
- **Anchor text:** business name + city/state (descriptive, doubles as local-relevance signal).

### Competitive bar (verified 2026-06-09)

amsoil.com `/all-locations/` city pages contain **no dealer names or links in crawlable HTML** - only a count plus identical boilerplate, BreadcrumbList-only schema; `/locator/map.aspx` is JS-only. Our directory is therefore likely the only crawlable surface on the web naming most of these dealers and linking their sites. We beat it with: named dealers + followed links + per-page unique listings; correct region data with hard 404s; `ItemList` schema; no ZIP/JS gate. We deliberately stop at state granularity - corporate's city-level pages are the thin-page trap we're avoiding.

## 6. Error handling

- DB failure during revalidation → serve stale (stale-while-revalidate), error propagation per the #830 pattern.
- Empty/below-threshold regions → 404 (and removed from sitemap/grid on next revalidation).
- Bot-probe + method guards preserved on `/sites/`; directory routes get standard handling.
- Missing dealer assets degrade gracefully: no logo → initial-letter avatar; no description → name/city-only row (still linked).
- Host spoofing / non-production hosts: existing `parseHostContext` dev behavior unchanged.

## 7. Testing (TDD - failing tests first)

1. **Proxy:** apex `/` → stub per domain; subdomain → `/sites/...` rewrite; custom-domain rewrite; old `/dealers/` interception removed (`/dealers/texas` reaches the directory, never the subdomain lookup); probe/method guards on `/sites/`; authed `/`→`/dashboard`; `/landing`→`/`.
2. **Apex config:** all 4 domains present with all required fields; copy uniqueness assertion (no two domains share hero/trust copy).
3. **Query:** status filter, subdomain NOT NULL, exclusion list, prefix/domain scoping, region filter, cross-border grouping.
4. **Directory rendering:** city grouping, jump nav anchors, inline sub-threshold states, dealer row anchor text + plain rel, region-slug 404s.
5. **Spotlight:** quality gate, tier weighting, date-seed determinism, pool exhaustion fallback.
6. **SEO:** JSON-LD validates per page type; canonical no-slash guards (extend PR #903 test); hreflang pairs; sitemap membership; robots rules (on top of `amsoil-robots-txt`).
7. **Revalidation:** tag invalidation on dealer lifecycle events.
8. **Parity checklist (automatable subset):** favicons, GA4 wiring + prod guard, `/register` reachable from stub, terms/footer links, OG/Twitter tags present and host-correct.

## 8. Release gate: parity checklist (manual + automated, per domain)

GA4 events firing · favicons · `/register` signup flow · terms/footer links · OG/Twitter cards validate · JSON-LD validates (Rich Results test) · auth redirect `/`→`/dashboard` · `/landing` redirect · sitemap.xml + robots.txt correct per host · canonical/hreflang correct · Lighthouse ≥ parity with old page · dealer subdomains unaffected (spot-check ISR pages render at `/sites/` rewrite) · GSC URL inspection on hub + 2 state pages per domain.

**Rollout:** dev → staging soak against checklist → single prod cutover (DeployHQ full re-deploy, build as `amsoildlp`) → request indexing (hub + state pages, 4 properties) → monitor SerpApi dashboard + GSC weekly.

## 9. Coordination & sequencing

- `amsoil-robots-txt` branch (robots rewrite, rel tiers, `/og` noindex) merges first or this branch rebases on it; whoever merges second rebases. Our robots edits are written against its version of `app/robots.ts`.
- Directory→dealer links: plain followed `<a>` (`noopener noreferrer` only with `target="_blank"`). The rel-tier enforcement for ZO/CMS links lives in the other branch.

## 10. Out of scope / roadmap

- Per-dealer content differentiation (thin-affiliation fix) - separate track.
- Client-side geo "dealers near you" re-ranking (spotlight component keeps a post-hydration enhancement seam).
- Dealer-facing "SEO readiness" dashboard nudge (logo/description/customization → spotlight eligibility is the soft incentive for now).
- Reindex-queue prioritization by activation date; GBP backlink resource for dealers.
- City-level pages where density justifies (future, per-region opt-in).
- Subdirectory consolidation of dealer sites (structural root cause; large migration; roadmap only).

## 11. Risks

| Risk                                                                   | Mitigation                                                                                                                   |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| index.html replacement regression (analytics, signup funnel, SEO tags) | §8 parity checklist as hard release gate; amsoil.aimclear.com keeps index.html                                               |
| `/sites` rename breaks dealer rendering                                | TDD proxy suite + staging soak; ISR fully rebuilds on deploy anyway                                                          |
| Four stubs still read as doorway set                                   | Distinct positioning/copy per domain enforced by config-completeness + uniqueness tests                                      |
| Directory pages themselves judged thin                                 | Hub = functional tool (search/spotlight/counts); state pages = differentiated rows with descriptions; no sub-threshold pages |
| Spotlight perceived as favoritism                                      | Objective quality gate + rotation; documented criteria                                                                       |
| Merge conflict with robots branch                                      | Sequencing agreement in §9                                                                                                   |
