|
| 1 | +# Rivian Tire Guide — 2.0.0 Roadmap |
| 2 | + |
| 3 | +> Status: proposal / planning. Current shipping version: **1.51.0**. |
| 4 | +> This document captures the findings of a full-codebase review (frontend/UX, |
| 5 | +> backend/data/AI, reviews/admin/SEO/analytics) and lays out what earns a |
| 6 | +> major-version release, with a detailed implementation plan for the chosen |
| 7 | +> first pillar (**Individual Tire Pages + SEO**). |
| 8 | +
|
| 9 | +--- |
| 10 | + |
| 11 | +## Positioning: what 2.0 means here |
| 12 | + |
| 13 | +Today the plugin is a solid, well-architected **tire catalog you browse** — clean |
| 14 | +REST API, real-world Roamer efficiency data, moderated reviews, genuine |
| 15 | +click/search analytics, prepared statements throughout. A `2.0` (semver major) |
| 16 | +should be a landmark, not polish. The through-line: |
| 17 | + |
| 18 | +> **From "tire catalog you browse" → "tire advisor that guides you," with every |
| 19 | +> tire individually discoverable on the open web.** |
| 20 | +
|
| 21 | +### Headline finding — the advertised AI feature does not exist |
| 22 | + |
| 23 | +The workspace docs describe "AI-powered recommendations (Claude API)," but the |
| 24 | +review found **no Claude/Anthropic integration anywhere in the code**. The only |
| 25 | +trace is an analytics label ("Top AI Queries") and a `search_type` column in |
| 26 | +`wp_rtg_search_events` that is never populated with `'ai'`. A major version is |
| 27 | +the right moment to either **build the AI advisor for real** or **remove the |
| 28 | +claim**. Building it is the stronger move (see Pillar 1). |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## The four pillars |
| 33 | + |
| 34 | +| # | Pillar | Why it's a 2.0 feature | |
| 35 | +|---|--------|------------------------| |
| 36 | +| 1 | **AI Tire Advisor** | Delivers the advertised-but-missing feature. Conversational "best winter tires for my R1T under $300" → structured picks + reasoning. Rate-limit buckets, `search_type='ai'`, and transient caching are already scaffolded. | |
| 37 | +| 2 | **Individual Tire Pages + SEO** ⭐ *(first focus)* | Biggest organic-growth lever. Every tire is currently trapped inside one shortcode with no indexable URL. Dedicated `Product` pages + schema + sitemap unlock search traffic. | |
| 38 | +| 3 | **Reviews → community & trust** | Turns one-off reviews into a retention engine: verified-owner badges, photos, helpful votes, multi-axis ratings, AI-assisted moderation. | |
| 39 | +| 4 | **Accessibility + UX baseline** | The quality bar of a 2.0, and the highest-count gap area (13 a11y issues). Focus traps, focus rings, ARIA live regions, reduced-motion, live search. | |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +### Pillar 1 — AI Tire Advisor *(highest leverage; deferred to a later phase)* |
| 44 | + |
| 45 | +Build a real Claude-powered recommender that turns natural-language questions |
| 46 | +into structured, explained tire picks over the live catalog. |
| 47 | + |
| 48 | +- New endpoint, e.g. `POST /wp-json/rtg/v2/recommend` → `{ picks: [{tire_id, reasoning, confidence}], alternatives: [...] }`. |
| 49 | +- Claude structured output (tool_use / JSON schema) grounded in the catalog + user prefs (vehicle, budget, conditions). |
| 50 | +- Hybrid fallback to the existing filter engine for simple queries or on API failure. |
| 51 | +- Reuse the existing per-fingerprint rate limiter (`class-rtg-ajax.php`) and transient caching keyed by normalized query + vehicle. |
| 52 | +- Emit `search_type='ai'` events so the existing analytics "AI Queries" surfaces become real. |
| 53 | +- API key via `wp-config.php` constant; track token cost; use the latest Claude model. |
| 54 | + |
| 55 | +**Enables:** AI-assisted review moderation (Pillar 3), smarter Roamer match |
| 56 | +suggestions. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +### Pillar 2 — Individual Tire Pages + SEO ⭐ *(detailed plan below)* |
| 61 | + |
| 62 | +Give every tire a crawlable, server-rendered URL (`/tires/{slug}/`) with a |
| 63 | +dedicated `Product` schema, canonical, per-tire OG/meta, breadcrumbs, and a |
| 64 | +sitemap — replacing today's JS-only `?tire=` deep links. |
| 65 | + |
| 66 | +See **"Detailed plan: Pillar 2"** below. |
| 67 | + |
| 68 | +--- |
| 69 | + |
| 70 | +### Pillar 3 — Reviews → community & trust |
| 71 | + |
| 72 | +- **Verified-owner** badge (WP user meta / attestation) — credibility today is zero; anyone can review any tire. |
| 73 | +- **Review photos** (moderated upload) and **"Was this helpful?"** votes (adds a moderation + sort signal). |
| 74 | +- **Multi-axis ratings**: wet grip, snow grip, noise, comfort, wear — the axes shoppers actually research, vs a single star. |
| 75 | +- **Frontend review sorting/filtering** (Most Helpful / Recent / Highest, filter by vehicle) — the reviews drawer is an unsorted list today. |
| 76 | +- **AI-assisted moderation**: toxicity/spam/duplicate scoring on the pending queue (pairs with Pillar 1). |
| 77 | +- Light community surface: top reviewers, review counts, public review history. |
| 78 | + |
| 79 | +Anchor files: `includes/class-rtg-reviews.php`, `includes/class-rtg-database.php` |
| 80 | +(ratings CRUD ~L1070–1159, moderation ~L1687–1703), `frontend/js/modules/ratings.js`, |
| 81 | +`admin/views/reviews-list.php`. |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +### Pillar 4 — Accessibility + UX baseline |
| 86 | + |
| 87 | +**Accessibility (WCAG):** |
| 88 | +- Modal focus traps + Escape + focus-return on close (review, compare, tooltip, image modals). |
| 89 | +- Visible `:focus-visible` rings on all interactive elements (some buttons `outline:none`). |
| 90 | +- `aria-live` on the tire count / result count, `aria-busy` during async fetch. |
| 91 | +- `prefers-reduced-motion` must disable **all** animation (tooltip/drawer currently animate regardless). |
| 92 | +- `aria-describedby` wiring for guest-review privacy note; verify star-rating SR semantics and hover contrast. |
| 93 | + |
| 94 | +**UX polish:** |
| 95 | +- Live search autocomplete/preview (search is button/Enter-only today). |
| 96 | +- Cascade feedback when vehicle selection narrows the size dropdown (silent today). |
| 97 | +- Adaptive price-slider bounds (hardcoded `$600` max regardless of data). |
| 98 | +- Mobile filter-drawer keyboard management + persistent state. |
| 99 | +- Distinguish "no tires exist" vs "filters too restrictive" in the empty state. |
| 100 | + |
| 101 | +Anchor files: `frontend/js/rivian-tires.js`, `frontend/js/modules/{filters,ratings,search,tooltips,server}.js`, |
| 102 | +`frontend/css/rivian-tires.css`. |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Supporting work (fold in; don't headline) |
| 107 | + |
| 108 | +- **Engagement:** saved & shareable comparisons (compare state isn't shareable), saved searches, price-drop / new-tire email alerts (`class-rtg-mailer.php` already exists). |
| 109 | +- **Admin productivity:** bulk **edit** (only bulk delete exists), CSV import dry-run/preview, insight cards ("N tires missing images/links"). |
| 110 | +- **Architecture (enables the pillars):** |
| 111 | + - Split the 2,251-line `RTG_Database` god class into query/service classes. |
| 112 | + - Add `do_action('rtg_tire_*')` / `rtg_review_*` extensibility hooks. |
| 113 | + - Introduce an `RTG_Cache` wrapper (optional Redis/object-cache support). |
| 114 | + - **Retire the fragile 28-element array row format** (`row[24]` magic indices scattered across JS) in favor of keyed objects — the single riskiest foundation for new features. |
| 115 | + |
| 116 | +## Explicitly deferred past 2.0 |
| 117 | + |
| 118 | +GraphQL layer, A/B-testing framework, REST auth/webhooks, session replay, |
| 119 | +retailer price-scraping (ToS/vendor risk). Real, but none load-bearing for the |
| 120 | +2.0 narrative — they'd dilute it. |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## Detailed plan: Pillar 2 — Individual Tire Pages + SEO |
| 125 | + |
| 126 | +### Problem |
| 127 | + |
| 128 | +- A tire is only reachable via the `[rivian_tire_guide]` shortcode page; the |
| 129 | + "detail view" is a client-side `?tire=<id>` deep link. |
| 130 | +- `RTG_Meta` emits per-tire OG/Twitter tags for `?tire=`, but the canonical URL |
| 131 | + is the query-arg URL — not a clean, indexable path. |
| 132 | +- `RTG_Schema` emits one big `ItemList` of every tire on the catalog page. |
| 133 | + Google strongly prefers a dedicated **`Product`** page per item for product |
| 134 | + rich results. |
| 135 | +- Net effect: **zero individually rankable tire URLs.** For a tire guide, that |
| 136 | + is the largest missed organic-search opportunity. |
| 137 | + |
| 138 | +### Goal |
| 139 | + |
| 140 | +Every tire gets a stable, server-rendered, indexable URL: |
| 141 | +`https://site/tires/{brand}-{model}-{size}/` (canonical), carrying dedicated |
| 142 | +`Product` + `Review` + `BreadcrumbList` schema, per-tire `<title>`/meta/OG, and |
| 143 | +a sitemap entry — while the existing catalog and `?tire=` deep links keep |
| 144 | +working (301 → the new URL). |
| 145 | + |
| 146 | +### Approach — mirror the existing standalone-page pattern |
| 147 | + |
| 148 | +`RTG_Tire_Review` (`includes/class-rtg-tire-review.php`) is the template to copy: |
| 149 | +`add_rewrite_rule` → `query_vars` → `template_redirect` (render + `exit`), a |
| 150 | +one-shot `rtg_flush_rewrite` option to flush on activation/settings change, and |
| 151 | +a settings-driven slug. The new work is the same shape, plus **server-rendered |
| 152 | +HTML** (SEO requires the tire content in the initial response, not injected by JS). |
| 153 | + |
| 154 | +### Phase 2A — Routing + server-rendered page (the core SEO win) |
| 155 | + |
| 156 | +**New:** `includes/class-rtg-tire-page.php` (`RTG_Tire_Page`) |
| 157 | +- Rewrite: `^{tires_slug}/([^/]+)/?$` → `index.php?rtg_tire=$matches[1]` (default slug `tires`, configurable like `tire_review_slug`). |
| 158 | +- Resolve the path segment to a tire. Add a stable, human-readable **`slug` |
| 159 | + column** to `wp_rtg_tires` (migration 17) generated from brand+model+size via |
| 160 | + `sanitize_title`, with a uniqueness suffix; keep resolving by raw `tire_id` as |
| 161 | + a fallback so nothing 404s during backfill. |
| 162 | +- On no match → proper WP 404 (`$wp_query->set_404()` + status header), never a |
| 163 | + soft-200. |
| 164 | +- Reuse the `RTG_Tire_Review` security-header block (X-Content-Type-Options, |
| 165 | + X-Frame-Options, CSP, Referrer-Policy). |
| 166 | + |
| 167 | +**New:** `frontend/templates/tire-page.php` — server-rendered |
| 168 | +- Full `<h1>` (brand model size), specs table, price/offer, category, warranty, |
| 169 | + Roamer real-world efficiency, and existing user reviews **in the initial HTML**. |
| 170 | +- Progressive enhancement: hydrate ratings/compare/favorite via the existing |
| 171 | + modules, but content must be present without JS. |
| 172 | +- Canonical `<link rel="canonical">` → the `/tires/{slug}/` URL. |
| 173 | +- Breadcrumb UI: Home → Tire Guide → {Category or Size} → {Tire}. |
| 174 | +- "Back to guide" + "Compare" + affiliate CTA (reuse click tracking). |
| 175 | + |
| 176 | +**Modify:** `frontend/js/modules/cards.js` — point each card's title/CTA at the |
| 177 | +new `/tires/{slug}/` permalink (progressive: real `<a href>` so crawlers follow |
| 178 | +it), instead of only the `?tire=` deep link. |
| 179 | + |
| 180 | +### Phase 2B — Structured data + meta per tire |
| 181 | + |
| 182 | +**New:** `RTG_Schema::output_single_product( $tire )` (or a small |
| 183 | +`RTG_Tire_Page` method) emitting a **single `Product`** node (not `ItemList`) |
| 184 | +with `offers`, `aggregateRating`, up to N `review`s, and `additionalProperty` |
| 185 | +specs — the builder logic already exists in `class-rtg-schema.php` L38–165 and |
| 186 | +can be factored out and reused. |
| 187 | +- Add **`BreadcrumbList`** schema matching the on-page breadcrumb. |
| 188 | +- Add **`VideoObject`** when `review_link` points to a video (YouTube etc.) — |
| 189 | + the review videos are already linked, just unmarked. |
| 190 | + |
| 191 | +**Modify:** `includes/class-rtg-meta.php` |
| 192 | +- Generate per-tire `<title>`, meta description, canonical, and `og:image` from |
| 193 | + the tire's own photo for the new route (today OG only fires on the shortcode |
| 194 | + page + `?tire=`). Point canonical at `/tires/{slug}/`. |
| 195 | + |
| 196 | +### Phase 2C — Sitemap + redirects + discoverability |
| 197 | + |
| 198 | +- **Sitemap:** register tire URLs. Prefer hooking the WP core sitemap provider |
| 199 | + (`wp_sitemaps_add_provider`) or Yoast/Rank Math if active; fall back to a |
| 200 | + custom `/tire-sitemap.xml`. Include `lastmod` from `updated_at`. |
| 201 | +- **301** legacy `?tire=<id>` (and `/tires/{raw_tire_id}/`) → the canonical |
| 202 | + `/tires/{slug}/` so existing shared links consolidate link equity. |
| 203 | +- Internal linking: link category/size facets and related tires from each page |
| 204 | + (crawl depth + topical clustering). |
| 205 | +- Flush rewrites once on upgrade via the existing `rtg_flush_rewrite` flag; add |
| 206 | + a settings field for the `tires` slug. |
| 207 | + |
| 208 | +### Files at a glance |
| 209 | + |
| 210 | +| Action | File | |
| 211 | +|--------|------| |
| 212 | +| New | `includes/class-rtg-tire-page.php` (routing, render, 404, redirects) | |
| 213 | +| New | `frontend/templates/tire-page.php` (server-rendered detail) | |
| 214 | +| New | `frontend/js/tire-page.js` + esbuild target (hydration only) | |
| 215 | +| Modify | `includes/class-rtg-schema.php` (factor out single-`Product`; add Breadcrumb/Video) | |
| 216 | +| Modify | `includes/class-rtg-meta.php` (per-tire title/canonical/OG on new route) | |
| 217 | +| Modify | `includes/class-rtg-activator.php` (migration 17: `slug` column + backfill; `DB_VERSION` 16→17) | |
| 218 | +| Modify | `includes/class-rtg-database.php` (slug generation, resolve-by-slug, sitemap query) | |
| 219 | +| Modify | `frontend/js/modules/cards.js` (link cards to `/tires/{slug}/`) | |
| 220 | +| Modify | `includes/class-rtg-admin.php` (settings: tires slug; show/edit slug in tire form) | |
| 221 | +| Modify | main plugin file (instantiate `RTG_Tire_Page`) | |
| 222 | +| Modify | `esbuild.config.mjs` (build `tire-page.js`) | |
| 223 | + |
| 224 | +### Risks & watch-items |
| 225 | + |
| 226 | +- **Rewrite-rule flushing** is a classic footgun — gate behind the one-shot flag; never flush on every request. |
| 227 | +- **Slug collisions / renames**: enforce uniqueness; when brand/model/size changes, keep the old slug 301-ing to avoid breaking indexed URLs. |
| 228 | +- **Thin-content risk**: a tire with no reviews and sparse specs is a weak page — consider `noindex` until it clears a minimum content threshold, and lean on Roamer data + spec richness. |
| 229 | +- **Duplicate content**: exactly one canonical per tire; the catalog `ItemList` and the per-tire `Product` must not compete — the per-tire page owns the `Product` node. |
| 230 | +- **Backfill correctness**: generate slugs for all existing rows in migration 17 and verify no collisions before shipping. |
| 231 | + |
| 232 | +### Acceptance criteria |
| 233 | + |
| 234 | +- `GET /tires/{slug}/` returns 200 with full tire content in the **initial HTML** (verify with JS disabled). |
| 235 | +- Google Rich Results Test passes for `Product` + `Review` + `BreadcrumbList`. |
| 236 | +- Legacy `?tire=<id>` 301s to the canonical URL; unknown slug returns a real 404. |
| 237 | +- Tire URLs appear in the sitemap with correct `lastmod`. |
| 238 | +- PHPCS clean; PHP 7.4/8.0/8.2 lint green; existing 83 JS tests pass; new tests for slug generation + route resolution. |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## Appendix — review gap inventory (condensed) |
| 243 | + |
| 244 | +Full detail lives in the review; headline counts: |
| 245 | + |
| 246 | +- **Accessibility:** 13 issues (focus traps, focus rings, ARIA live/busy, reduced-motion, contrast). |
| 247 | +- **Missing shopper features:** 13 (saved comparisons, size wizard, price history, availability, multi-axis reviews, alerts). |
| 248 | +- **Frontend tech debt:** fragile array row format, monolithic `ratings.js`/`filters.js`, circular-dep hacks, inline styles in JS, DOM cache staleness. |
| 249 | +- **Backend:** no AI layer (despite the claim), `RTG_Database` god class, no extensibility hooks, REST is public-only, no down-migrations. |
| 250 | +- **SEO:** no individual tire pages, `ItemList`-only schema, no breadcrumb/video schema, query-arg canonicals (→ Pillar 2 addresses all of these). |
0 commit comments