# SERP Site Name & Schema Fix Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix Google's "Aimclear" site-name display for dealer subdomains by correcting apex domain canonical/entity signals, and clean up dealer page JSON-LD to remove invalid Product nodes and add real differentiation signals.

**Architecture:** Part A adds a new `app/api/landing/route.ts` Route Handler that reads and transforms `public/index.html` per-host; `proxy.ts` gets a 3-line rewrite for the four AMSOIL apex domains. Part C modifies `generateDealerSchema` in `DealerTemplate.tsx` to remove 8 invalid Product nodes and enrich the `AutoPartsStore` node.

**Tech Stack:** Next.js 15 App Router, TypeScript, Jest. Working branch: `claude-amsoil-index-troubleshooting`. PRs target `dev`.

---

## File Map

| File                                           | Action                          | What it does                                                                                                                |
| ---------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `app/api/landing/route.ts`                     | Create                          | Serves `public/index.html` with host-aware canonical/entity substitutions for AMSOIL apex domains                           |
| `app/api/landing/__tests__/route.test.ts`      | Create                          | Unit tests for the `transformForHost` pure function                                                                         |
| `proxy.ts`                                     | Modify (~line 892)              | Rewrite apex domain root requests to `/api/landing` before they fall through to `public/index.html`                         |
| `components/DealerTemplate.tsx`                | Modify (`generateDealerSchema`) | Remove 8 Product nodes + `makesOffer`; add `image`, `logo`, `hasMap`, `sameAs`, `priceRange`; fix double-slash from PR #899 |
| `components/__tests__/DealerTemplate.test.tsx` | Modify                          | Add/update tests for the schema changes                                                                                     |

---

## Task 1: Route Handler - `app/api/landing/route.ts`

**Files:**

- Create: `app/api/landing/route.ts`
- Create: `app/api/landing/__tests__/route.test.ts`

The handler reads `public/index.html` once at module load (cached in memory), then for each request replaces the hard-coded `amsoil.aimclear.com` references with the actual request host. The transformation logic is extracted as `transformForHost` so it can be unit-tested without filesystem or HTTP setup.

- [ ] **Step 1: Write the failing tests**

Create `app/api/landing/__tests__/route.test.ts`:

```ts
import { transformForHost } from '../route';

const SAMPLE_HTML = `
<link rel="canonical" href="https://amsoil.aimclear.com/" />
<meta property="og:url" content="https://amsoil.aimclear.com/" />
<meta name="twitter:url" content="https://amsoil.aimclear.com/" />
<meta property="og:image" content="https://amsoil.aimclear.com/media/amsoil-logo.png" />
"@id": "https://amsoil.aimclear.com/#website"
"url": "https://amsoil.aimclear.com/"
"name": "AIMCLEAR"
"url": "https://aimclear.com/"
"description": "Premier marketing and website development agency partnering with AMSOIL"
`.trim();

describe('transformForHost', () => {
  it('replaces all amsoil.aimclear.com URL occurrences with the given host', () => {
    const result = transformForHost(SAMPLE_HTML, 'myamsoil.com');
    expect(result).not.toContain('amsoil.aimclear.com');
    expect(result).toContain('https://myamsoil.com/');
  });

  it('replaces the AIMCLEAR organization name', () => {
    const result = transformForHost(SAMPLE_HTML, 'myamsoil.com');
    expect(result).not.toContain('"name": "AIMCLEAR"');
    expect(result).toContain('"name": "AMSOIL Dealer Landing Pages"');
  });

  it('replaces the aimclear.com organization URL', () => {
    const result = transformForHost(SAMPLE_HTML, 'myamsoil.com');
    expect(result).not.toContain('"url": "https://aimclear.com/"');
    expect(result).toContain('"url": "https://myamsoil.com/"');
  });

  it('replaces the AIMCLEAR agency description', () => {
    const result = transformForHost(SAMPLE_HTML, 'myamsoil.com');
    expect(result).not.toContain('Premier marketing and website development agency');
    expect(result).toContain('Platform for AMSOIL dealers');
  });

  it('works correctly for each of the four apex domains', () => {
    const apexDomains = ['myamsoil.com', 'shopamsoil.com', 'myamsoil.ca', 'shopamsoil.ca'];
    for (const host of apexDomains) {
      const result = transformForHost(SAMPLE_HTML, host);
      expect(result).not.toContain('amsoil.aimclear.com');
      expect(result).toContain(`https://${host}/`);
    }
  });

  it('does not mutate the input string', () => {
    const original = SAMPLE_HTML;
    transformForHost(SAMPLE_HTML, 'myamsoil.com');
    expect(SAMPLE_HTML).toBe(original);
  });
});
```

- [ ] **Step 2: Run tests to confirm they fail**

```bash
npm test -- --testPathPattern="app/api/landing" 2>&1 | tail -15
```

Expected: FAIL with "Cannot find module '../route'"

- [ ] **Step 3: Implement the route handler**

Create `app/api/landing/route.ts`:

```ts
import { NextRequest, NextResponse } from 'next/server';
import { readFileSync } from 'fs';
import { join } from 'path';

const APEX_DOMAINS = new Set(['myamsoil.com', 'shopamsoil.com', 'myamsoil.ca', 'shopamsoil.ca']);

// Read once at module load — only changes on deploy
const template = readFileSync(join(process.cwd(), 'public', 'index.html'), 'utf-8');

/**
 * Replace all hard-coded amsoil.aimclear.com references in the landing page
 * HTML with host-appropriate values so each apex domain self-identifies
 * correctly to Google's site-name and entity systems.
 *
 * Exported for unit testing.
 */
export function transformForHost(html: string, host: string): string {
  const origin = `https://${host}`;
  return html
    .replace(/https:\/\/amsoil\.aimclear\.com\//g, `${origin}/`)
    .replace(/"name": "AIMCLEAR"/g, '"name": "AMSOIL Dealer Landing Pages"')
    .replace(/"url": "https:\/\/aimclear\.com\/"/g, `"url": "${origin}/"`)
    .replace(
      /"description": "Premier marketing and website development agency partnering with AMSOIL"/g,
      '"description": "Platform for AMSOIL dealers to create professional landing pages"'
    );
}

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest): Promise<NextResponse> {
  const host = (request.headers.get('host') || '').split(':')[0].toLowerCase();

  if (!APEX_DOMAINS.has(host)) {
    return NextResponse.redirect(new URL('/', request.url), 302);
  }

  const html = transformForHost(template, host);
  return new NextResponse(html, {
    headers: { 'Content-Type': 'text/html; charset=utf-8' },
  });
}
```

- [ ] **Step 4: Run tests to confirm they pass**

```bash
npm test -- --testPathPattern="app/api/landing" 2>&1 | tail -15
```

Expected: PASS - all 6 tests green

- [ ] **Step 5: Commit**

```bash
git add app/api/landing/route.ts app/api/landing/__tests__/route.test.ts
git commit -m "feat(seo): add host-aware landing route for AMSOIL apex domains"
```

---

## Task 2: Proxy rewrite for apex domains

**Files:**

- Modify: `proxy.ts` (~line 892)

The `proxy` function already extracts `hostname` from the request host header at the top of the function. The root path block (`if (pathname === '/')`) runs the auth check, then falls through to `nextWithoutDealerHeader(request)` which serves `public/index.html` via the `next.config.mjs` rewrite. We intercept apex domains just before that fallthrough.

- [ ] **Step 1: Locate the insertion point**

Open `proxy.ts` and find the line that reads:

```ts
return nextWithoutDealerHeader(request); // Serves public/index.html
```

It is the last line inside the `if (pathname === '/') {` block, after the `if (token)` redirect. The surrounding context looks like:

```ts
    if (token) {
      return NextResponse.redirect(new URL('/dashboard', getSiteUrl().origin));
    }
    return nextWithoutDealerHeader(request); // Serves public/index.html
  }
```

- [ ] **Step 2: Insert the apex domain rewrite**

Replace just the final return in that block so it reads:

```ts
    if (token) {
      return NextResponse.redirect(new URL('/dashboard', getSiteUrl().origin));
    }

    // Apex AMSOIL domains get host-aware canonical/entity signals via /api/landing.
    // amsoil.aimclear.com falls through to public/index.html unchanged.
    const AMSOIL_APEX = new Set([
      'myamsoil.com',
      'shopamsoil.com',
      'myamsoil.ca',
      'shopamsoil.ca',
    ]);
    if (AMSOIL_APEX.has(hostname.split(':')[0].toLowerCase())) {
      return NextResponse.rewrite(new URL('/api/landing', request.url));
    }
    return nextWithoutDealerHeader(request); // Serves public/index.html
  }
```

- [ ] **Step 3: Run TypeScript check**

```bash
npx tsc --noEmit 2>&1 | grep "proxy.ts" | head -10
```

Expected: no errors on proxy.ts

- [ ] **Step 4: Smoke test via curl (requires the app running)**

If the dev server is running on port 3000:

```bash
curl -s -H "Host: myamsoil.com" http://localhost:3000/ | grep -m3 "canonical\|aimclear\|AIMCLEAR"
```

Expected output should show `myamsoil.com` in the canonical and no `aimclear.com` references.

- [ ] **Step 5: Commit**

```bash
git add proxy.ts
git commit -m "feat(seo): rewrite AMSOIL apex domain root to host-aware landing handler"
```

---

## Task 3: Remove invalid Product nodes from dealer schema

**Files:**

- Modify: `components/DealerTemplate.tsx` (function `generateDealerSchema`, lines ~30–255)
- Modify: `components/__tests__/DealerTemplate.test.tsx`

Two changes in one commit:

1. Strip the trailing slash from `pageUrl` to prevent `//` double-slash in `@id` values after PR #899 merges
2. Remove `makesOffer` from the `AutoPartsStore` node
3. Remove all 8 `Product` nodes
4. Remove the now-dead `resolveCategoryUrl` helper and `categoryUrls` object

**Export `generateDealerSchema` for testing.** Currently unexported; change `function generateDealerSchema` to `export function generateDealerSchema`.

- [ ] **Step 1: Write the failing tests**

In `components/__tests__/DealerTemplate.test.tsx`, add a `describe('generateDealerSchema')` block (or update existing one if present):

```ts
import { generateDealerSchema } from '../DealerTemplate';
import type { DealerInfo } from '@/types/dealer';

const baseDealerInfo: DealerInfo = {
  subdomain: 'testdealer',
  domain: 'com',
  domainPrefix: 'myamsoil',
  name: 'Test Dealer',
  phone: '+15551234567',
  email: 'test@example.com',
  address: {
    city: 'Minneapolis',
    state: 'MN',
    zip: '55401',
    street: '123 Main St',
    country: 'US',
  },
  description: '<p>Test description</p>',
  navigation: [],
  heroSlider: null,
  quickLinks: [],
  hideAddress: false,
  contactName: 'Test',
  showContactName: false,
  logoUrl: undefined,
  headScripts: undefined,
  bodyScripts: undefined,
  socialLinks: undefined,
  preferredLanguage: 'en',
  amsoilLinkDomain: 'com',
};

describe('generateDealerSchema', () => {
  it('returns valid JSON-LD with @context and @graph', () => {
    const schema = generateDealerSchema(baseDealerInfo) as Record<string, unknown>;
    expect(schema['@context']).toBe('https://schema.org');
    expect(Array.isArray(schema['@graph'])).toBe(true);
  });

  it('does not contain Product nodes', () => {
    const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
    const productNodes = schema['@graph'].filter((n) => n['@type'] === 'Product');
    expect(productNodes).toHaveLength(0);
  });

  it('does not include makesOffer on AutoPartsStore', () => {
    const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
    const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
      string,
      unknown
    >;
    expect(store).toBeDefined();
    expect(store['makesOffer']).toBeUndefined();
  });

  it('produces @id values without double slashes', () => {
    const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
    const json = JSON.stringify(schema);
    expect(json).not.toContain('///#');
    expect(json).not.toContain('myamsoil.com//#');
  });

  it('AutoPartsStore @id uses correct URL format', () => {
    const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
    const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
      string,
      unknown
    >;
    expect(store['@id']).toBe('https://testdealer.myamsoil.com/#localbusiness');
  });
});
```

- [ ] **Step 2: Run tests to confirm they fail**

```bash
npm test -- --testPathPattern="DealerTemplate" 2>&1 | tail -20
```

Expected: FAIL - "generateDealerSchema is not exported" and the Product/makesOffer tests will fail once the export is added.

- [ ] **Step 3: Export `generateDealerSchema` and remove dead code**

In `components/DealerTemplate.tsx`:

**a)** Change the function declaration on the line that reads `function generateDealerSchema(dealer: DealerInfo): object {` to:

```ts
export function generateDealerSchema(dealer: DealerInfo): object {
```

**b)** Change the `pageUrl` assignment (currently `const pageUrl = getCanonicalUrl({...})`) to strip the trailing slash:

```ts
const pageUrl = getCanonicalUrl({
  subdomain: dealer.subdomain,
  domain: dealer.domain,
  domainPrefix: dealer.domainPrefix,
}).replace(/\/$/, '');
```

**c)** Delete the `resolveCategoryUrl` helper and `categoryUrls` object entirely (lines ~47–61):

```ts
// DELETE these lines:
const resolveCategoryUrl = (usUrl: string): string => {
  return resolveAmsoilLink(usUrl, linkDomain);
};

const categoryUrls = {
  motorOil: resolveCategoryUrl('https://www.amsoil.com/c/motor-oil/2/'),
  transmissionFluid: resolveCategoryUrl('https://www.amsoil.com/c/transmission-fluid/19/'),
  filters: resolveCategoryUrl('https://www.amsoil.com/c/filters/29/'),
  fuelAdditives: resolveCategoryUrl('https://www.amsoil.com/c/fuel-additives/16/'),
  gearOil: resolveCategoryUrl('https://www.amsoil.com/c/gear-oil/25/'),
  grease: resolveCategoryUrl('https://www.amsoil.com/c/bearing-chassis-grease/28/'),
  hydraulicOil: resolveCategoryUrl('https://www.amsoil.com/c/hydraulic-oil/13/'),
  compressorOil: resolveCategoryUrl('https://www.amsoil.com/c/compressor-oil/15/'),
};
```

**d)** On the `AutoPartsStore` node, remove the entire `makesOffer` array property:

```ts
      // DELETE these lines from the AutoPartsStore node:
      makesOffer: [
        { '@id': `${pageUrl}/#offer-motor-oils` },
        { '@id': `${pageUrl}/#offer-transmission-fluids` },
        { '@id': `${pageUrl}/#offer-filtration-products` },
        { '@id': `${pageUrl}/#offer-fuel-additives` },
        { '@id': `${pageUrl}/#offer-gear-lubes` },
        { '@id': `${pageUrl}/#offer-greases` },
        { '@id': `${pageUrl}/#offer-hydraulic-oils` },
        { '@id': `${pageUrl}/#offer-compressor-oils` },
      ],
```

**e)** Delete all 8 `Product` node objects from the `@graph` array (lines ~123–255). Each looks like:

```ts
      // DELETE all 8 blocks of this pattern:
      {
        '@type': 'Product',
        '@id': `${categoryUrls.motorOil}#product`,
        name: 'AMSOIL Synthetic Motor Oils',
        ...
      },
```

After removing, the `@graph` array should contain exactly 4 nodes: `AutoPartsStore`, `Organization/Brand`, `WebSite`, `WebPage`.

- [ ] **Step 4: Check for unused imports**

After removing `categoryUrls` and `resolveCategoryUrl`, verify `resolveAmsoilLink` is still used elsewhere in the file (it is - it's used in the nav link rendering). If `isAmsoilUrl` or `appendZoParam` are now unused, remove those imports too. Run:

```bash
npx tsc --noEmit 2>&1 | grep "DealerTemplate" | head -10
```

Expected: no errors

- [ ] **Step 5: Run tests to confirm they pass**

```bash
npm test -- --testPathPattern="DealerTemplate" 2>&1 | tail -20
```

Expected: PASS for all generateDealerSchema tests

- [ ] **Step 6: Commit**

```bash
git add components/DealerTemplate.tsx components/__tests__/DealerTemplate.test.tsx
git commit -m "fix(schema): remove invalid AMSOIL product nodes from dealer page JSON-LD"
```

---

## Task 4: Enrich AutoPartsStore with differentiation signals

**Files:**

- Modify: `components/DealerTemplate.tsx` (function `generateDealerSchema`)
- Modify: `components/__tests__/DealerTemplate.test.tsx`

Add `image`, `logo`, `hasMap`, `sameAs`, and `priceRange` to the `AutoPartsStore` node. All are conditional on data availability and respect `dealer.hideAddress`.

- [ ] **Step 1: Write failing tests**

Add to the `describe('generateDealerSchema')` block in `components/__tests__/DealerTemplate.test.tsx`:

```ts
it('includes image pointing to the /og OG image URL', () => {
  const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(store['image']).toBe('https://testdealer.myamsoil.com/og');
});

it('includes priceRange', () => {
  const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(store['priceRange']).toBe('$-$$$');
});

it('includes hasMap when hideAddress is false and address is present', () => {
  const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(typeof store['hasMap']).toBe('string');
  expect(store['hasMap'] as string).toContain('google.com/maps');
  expect(store['hasMap'] as string).toContain(encodeURIComponent('Minneapolis'));
});

it('omits hasMap when hideAddress is true', () => {
  const schema = generateDealerSchema({ ...baseDealerInfo, hideAddress: true }) as {
    '@graph': Record<string, unknown>[];
  };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(store['hasMap']).toBeUndefined();
});

it('includes logo when logoUrl is set', () => {
  const schema = generateDealerSchema({
    ...baseDealerInfo,
    logoUrl: 'https://cdn.example.com/logo.png',
  }) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(store['logo']).toBe('https://cdn.example.com/logo.png');
});

it('omits logo when logoUrl is not set', () => {
  const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(store['logo']).toBeUndefined();
});

it('includes sameAs array when social links are present', () => {
  const dealer = {
    ...baseDealerInfo,
    socialLinks: {
      facebook: 'https://facebook.com/testdealer',
      instagram: 'https://instagram.com/testdealer',
    },
  };
  const schema = generateDealerSchema(dealer) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(Array.isArray(store['sameAs'])).toBe(true);
  expect(store['sameAs']).toContain('https://facebook.com/testdealer');
  expect(store['sameAs']).toContain('https://instagram.com/testdealer');
});

it('omits sameAs when no social links are set', () => {
  const schema = generateDealerSchema(baseDealerInfo) as { '@graph': Record<string, unknown>[] };
  const store = schema['@graph'].find((n) => n['@type'] === 'AutoPartsStore') as Record<
    string,
    unknown
  >;
  expect(store['sameAs']).toBeUndefined();
});
```

- [ ] **Step 2: Run tests to confirm they fail**

```bash
npm test -- --testPathPattern="DealerTemplate" 2>&1 | tail -20
```

Expected: FAIL - the new assertions for `image`, `priceRange`, `hasMap`, `logo`, `sameAs` will all fail

- [ ] **Step 3: Add enrichment fields to AutoPartsStore**

In `components/DealerTemplate.tsx`, find the `AutoPartsStore` node object inside `generateDealerSchema`. It currently ends with the `address` field (after removing `makesOffer` in Task 3). Update it to:

```ts
      {
        '@type': 'AutoPartsStore',
        '@id': `${pageUrl}/#localbusiness`,
        name: `${dealer.name}, Authorized AMSOIL Dealer`,
        description: `Authorized Independent AMSOIL Dealer specializing in premium AMSOIL synthetic lubricants and filters in ${dealer.address.city}, ${dealer.address.state}.`,
        url: `${pageUrl}/`,
        telephone: dealer.phone,
        image: `${pageUrl}/og`,
        priceRange: '$-$$$',
        ...(dealer.logoUrl ? { logo: dealer.logoUrl } : {}),
        ...(!dealer.hideAddress && dealer.address.city && dealer.address.state
          ? {
              hasMap: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
                [dealer.address.street, dealer.address.city, dealer.address.state, dealer.address.zip]
                  .filter(Boolean)
                  .join(', ')
              )}`,
            }
          : {}),
        ...((() => {
          const links = dealer.socialLinks;
          if (!links) return {};
          const urls = [
            links.facebook,
            links.instagram,
            links.youtube,
            links.pinterest,
            links.linkedin,
            links.x || links.twitter,
          ].filter((v): v is string => Boolean(v));
          return urls.length > 0 ? { sameAs: urls } : {};
        })()),
        brand: {
          '@type': 'Brand',
          '@id': `${amsoilBase}/#brand`,
        },
        address: {
          '@type': 'PostalAddress',
          streetAddress: dealer.address.street || '',
          addressLocality: dealer.address.city,
          addressRegion: dealer.address.state,
          postalCode: dealer.address.zip,
          addressCountry: dealer.address.country === 'Canada' ? 'CA' : 'US',
        },
      },
```

- [ ] **Step 4: Run tests to confirm they pass**

```bash
npm test -- --testPathPattern="DealerTemplate" 2>&1 | tail -20
```

Expected: PASS for all tests

- [ ] **Step 5: Run full test suite to check for regressions**

```bash
npm test 2>&1 | tail -10
```

Expected: same pass/fail counts as baseline (3670 pass, 35 pre-existing DB failures)

- [ ] **Step 6: Commit**

```bash
git add components/DealerTemplate.tsx components/__tests__/DealerTemplate.test.tsx
git commit -m "feat(schema): enrich AutoPartsStore with image, logo, hasMap, sameAs, priceRange"
```

---

## Task 5: Open PR

- [ ] **Step 1: Push branch and open PR targeting `dev`**

```bash
git push origin claude-amsoil-index-troubleshooting
gh pr create \
  --repo aimclear/amsoil-dlp \
  --base dev \
  --head claude-amsoil-index-troubleshooting \
  --title "fix(seo): apex canonical identity + dealer schema cleanup and enrichment" \
  --body "$(cat <<'EOF'
## Summary

Fixes the root cause of dealer subdomain pages showing "Aimclear" as the Google SERP site name, cleans up invalid dealer page structured data, and adds differentiation signals.

### Part A — Apex domain canonical fix
- New `app/api/landing/route.ts` serves `public/index.html` with host-specific replacements for the four AMSOIL apex domains (`myamsoil.com`, `shopamsoil.com`, `myamsoil.ca`, `shopamsoil.ca`)
- `proxy.ts` rewrites apex root requests to `/api/landing`; `amsoil.aimclear.com` continues serving the static file unchanged
- Result: `myamsoil.com/` emits `canonical = https://myamsoil.com/`, `WebSite.@id = https://myamsoil.com/#website`, `Organization.name = "AMSOIL Dealer Landing Pages"` instead of all pointing to `amsoil.aimclear.com`

### Part B — Trailing slash (PR #899, merged separately)

### Part C — Dealer page JSON-LD cleanup + enrichment
- Removes 8 invalid `Product` nodes (AMSOIL category links with no prices — MAP policy prevents dealers from listing prices; these were generating 8,000+ invalid rich result errors across the dealer network)
- Removes `makesOffer` from `AutoPartsStore` (was only valid in context of the now-removed Product nodes)
- Adds `image`, `priceRange`, conditional `logo`, `hasMap` (respects `hideAddress`), `sameAs` (social links) to `AutoPartsStore`
- Fixes a double-slash `@id` bug introduced by PR #899's trailing slash change

## Test plan
- [ ] `npm test -- --testPathPattern="api/landing"` — new Route Handler tests pass
- [ ] `npm test -- --testPathPattern="DealerTemplate"` — schema tests pass
- [ ] Full suite: same baseline pass count
- [ ] After deploy to dev: `curl -Ls https://myamsoil.com/ | grep canonical` → shows `myamsoil.com`, not `amsoil.aimclear.com`
- [ ] Rich Results Test on a dealer page → 0 Product/Merchant invalid items (was 8)
- [ ] Schema Markup Validator on a dealer page → `AutoPartsStore` shows `image`, `priceRange`, `sameAs` for dealers with social links
EOF
)"
```

---

## Self-Review

**Spec coverage check:**

- ✅ Part A - `app/api/landing/route.ts` + `proxy.ts` (Tasks 1 + 2)
- ✅ Part B - marked done in plan header (PR #899)
- ✅ Part C remove - Product nodes + makesOffer (Task 3)
- ✅ Part C add - image, logo, hasMap, sameAs, priceRange (Task 4)
- ✅ `hideAddress` guard on `hasMap` (Task 4 Step 3)
- ✅ Double-slash PR #899 fix (Task 3 Step 3b)

**Placeholder scan:** No TBDs. All code steps contain actual code. All test steps contain actual test code with concrete assertions.

**Type consistency:**

- `DealerInfo` used consistently across Tasks 3 and 4
- `generateDealerSchema` exported in Task 3, imported in test file in Task 3 (same function name throughout)
- `pageUrl` defined in Task 3 with `.replace(/\/$/, '')`, used unchanged in Task 4

**One gap found and addressed:** The `sameAs` construction uses an IIFE to collect social URLs - this keeps the object spread clean but is worth noting. An alternative is extracting a `getSameAs(socialLinks)` helper before the object literal. Either works; the IIFE is consistent with keeping all schema logic inside `generateDealerSchema`.
