Skip to content

Commit 321389a

Browse files
RivianTrackrclaude
andcommitted
Render individual tire pages inside the theme + AIOSEO integration (1.53.0)
Tire pages (/tires/{slug}/, URLs unchanged) now render within the active theme (header/nav/footer) instead of as standalone documents. - New theme-agnostic RTG_Theme_Render helper: presents the request as a singular page (classic + block themes) via a virtual post, injects the server-rendered tire content into the_content (wpautop disabled), and delegates title/description/canonical to the SEO plugin with core fallbacks. Utility pages can opt into noindex. - RTG_Tire_Page: resolve tire -> render in-theme; content moved to a new .rtg-tp-scoped partial (frontend/templates/tire-page-content.php) so the page CSS no longer resets/overrides the theme. Standalone tire-page.php removed. - All in One SEO: per-tire title/description/canonical via aioseo_* filters; Product + BreadcrumbList JSON-LD on wp_head; every tire URL registered in the AIOSEO sitemap via aioseo_sitemap_additional_pages. Build regenerated; 83/83 JS tests pass. STAGING-GATED: changes tire-page rendering + SEO tag production. Verify on staging (theme chrome, AIOSEO title/desc/canonical, sitemap inclusion, 404s) before deploy. Review + Compare pages unchanged here — next step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2c68cdf commit 321389a

7 files changed

Lines changed: 543 additions & 386 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to the Rivian Tire Guide plugin will be documented in this f
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [1.53.0] - 2026-07-02
8+
9+
### Changed
10+
- **Individual tire pages now render inside the active theme** (header/nav/footer) instead of as standalone documents, while keeping the same `/tires/{slug}/` URLs. A new theme-agnostic `RTG_Theme_Render` helper presents the request as a singular page (works for classic and block themes) and injects the server-rendered tire content into `the_content`; the tire template's CSS was re-scoped under a `.rtg-tp` container so it no longer resets/overrides theme styles.
11+
12+
### Added
13+
- **All in One SEO integration for tire pages.** Per-tire `<title>`, meta description, and canonical are delegated to AIOSEO via its filters (`aioseo_title` / `aioseo_description` / `aioseo_canonical_url`), with core `document_title` / canonical fallbacks when no SEO plugin is active. `Product` + `BreadcrumbList` JSON-LD is emitted on `wp_head`, and every tire URL is registered into the AIOSEO XML sitemap via `aioseo_sitemap_additional_pages` — closing the sitemap gap from the initial launch.
14+
15+
> **Staging note:** this release changes how tire pages render and how their SEO tags are produced. Verify on staging before deploying — check that the page shows the theme chrome, that AIOSEO outputs the correct per-tire title/description/canonical (view source + AIOSEO's tools), that `/tires/{slug}/` appears in the sitemap, and that unknown slugs still 404. Review and Compare pages are unchanged in this release (still standalone) and are the next step.
16+
717
## [1.52.1] - 2026-07-02
818

919
### Fixed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<?php
2+
/**
3+
* Individual tire page — CONTENT partial, rendered inside the active theme
4+
* (via RTG_Theme_Render, injected into the_content). No <html>/<head>/<body>
5+
* and no global CSS resets: everything is scoped under .rtg-tp so it doesn't
6+
* fight the theme. SEO tags (title/meta/canonical/JSON-LD) are handled by
7+
* RTG_Tire_Page + the SEO plugin, not here.
8+
*
9+
* Expects $GLOBALS['rtg_tire_page_tire'].
10+
*/
11+
if ( ! defined( 'ABSPATH' ) ) {
12+
exit;
13+
}
14+
15+
$tire = $GLOBALS['rtg_tire_page_tire'] ?? null;
16+
if ( ! is_array( $tire ) ) {
17+
return;
18+
}
19+
20+
$tire_id = $tire['tire_id'];
21+
$brand = $tire['brand'] ?? '';
22+
$model = $tire['model'] ?? '';
23+
$size = $tire['size'] ?? '';
24+
$diameter = $tire['diameter'] ?? '';
25+
$category = $tire['category'] ?? '';
26+
$image = ! empty( $tire['image'] ) ? esc_url( $tire['image'] ) : '';
27+
$link = ! empty( $tire['link'] ) ? esc_url( $tire['link'] ) : '';
28+
29+
$heading = trim( "$brand $model" ) ?: 'Tire';
30+
$heading .= $size ? " ($size)" : '';
31+
32+
$ratings_map = RTG_Database::get_tire_ratings( array( $tire_id ) );
33+
$rating = $ratings_map[ $tire_id ] ?? array( 'average' => 0, 'count' => 0 );
34+
$avg = (float) ( $rating['average'] ?? 0 );
35+
$rating_cnt = (int) ( $rating['count'] ?? 0 );
36+
$reviews = RTG_Database::get_tire_reviews( $tire_id, 10 );
37+
38+
$guide_url = RTG_Tire_Page::guide_url();
39+
$rtg_set = get_option( 'rtg_settings', array() );
40+
$review_slug = sanitize_title( $rtg_set['tire_review_slug'] ?? 'tire-review' );
41+
$review_url = add_query_arg( 'tire', rawurlencode( $tire_id ), home_url( '/' . $review_slug . '/' ) );
42+
43+
$breadcrumb_items = array(
44+
array( 'name' => 'Home', 'url' => home_url( '/' ) ),
45+
array( 'name' => 'Tire Guide', 'url' => $guide_url ),
46+
);
47+
if ( $category ) {
48+
$breadcrumb_items[] = array( 'name' => $category, 'url' => add_query_arg( 'category', rawurlencode( $category ), $guide_url ) );
49+
}
50+
$breadcrumb_items[] = array( 'name' => $heading, 'url' => RTG_Tire_Page::tire_url( $tire['slug'] ?? $tire_id ) );
51+
52+
if ( ! function_exists( 'rtg_tire_page_stars' ) ) {
53+
function rtg_tire_page_stars( $avg ) {
54+
$out = '';
55+
for ( $i = 1; $i <= 5; $i++ ) {
56+
$fill = $avg >= $i ? 'full' : ( $avg >= $i - 0.5 ? 'half' : 'empty' );
57+
$out .= '<span class="rtg-tp-star rtg-tp-star-' . $fill . '">&#9733;</span>';
58+
}
59+
return $out;
60+
}
61+
}
62+
63+
// Full spec sheet (efficiency score intentionally omitted — discontinued).
64+
$specs = array(
65+
'Size' => $size . ( $diameter ? " ($diameter)" : '' ),
66+
'Category' => $category,
67+
'Average Price' => ( $tire['price'] > 0 ) ? '$' . number_format( (float) $tire['price'], 2 ) : '',
68+
'Mileage Warranty' => ( $tire['mileage_warranty'] > 0 ) ? number_format( (int) $tire['mileage_warranty'] ) . ' miles' : '',
69+
'Weight' => ( $tire['weight_lb'] > 0 ) ? $tire['weight_lb'] . ' lb' : '',
70+
'3PMS Rated' => $tire['three_pms'] ?? '',
71+
'Load Index' => $tire['load_index'] ?? '',
72+
'Load Range' => $tire['load_range'] ?? '',
73+
'Max Load' => ( $tire['max_load_lb'] > 0 ) ? number_format( (int) $tire['max_load_lb'] ) . ' lb' : '',
74+
'Speed Rating' => $tire['speed_rating'] ?? '',
75+
'Tread Depth' => $tire['tread'] ?? '',
76+
'Max PSI' => $tire['psi'] ?? '',
77+
'UTQG' => ( strtolower( trim( $tire['utqg'] ?? '' ) ) === 'none' ) ? '' : ( $tire['utqg'] ?? '' ),
78+
);
79+
80+
$roamer_eff = (float) ( $tire['roamer_efficiency'] ?? 0 );
81+
?>
82+
<style>
83+
.rtg-tp {
84+
--rtg-tp-accent: #fba919;
85+
--rtg-tp-accent-hover: #ffbe4a;
86+
--rtg-tp-card: #16191e;
87+
--rtg-tp-deep: #121418;
88+
--rtg-tp-text: #ece9e4;
89+
--rtg-tp-heading: #f6f4f0;
90+
--rtg-tp-muted: #a19e97;
91+
--rtg-tp-border: #3a3e45;
92+
--rtg-tp-star-empty: #2c2f34;
93+
max-width: 900px;
94+
margin: 0 auto;
95+
color: var(--rtg-tp-text);
96+
font-size: 15px;
97+
line-height: 1.6;
98+
}
99+
.rtg-tp *, .rtg-tp *::before, .rtg-tp *::after { box-sizing: border-box; }
100+
.rtg-tp a { color: var(--rtg-tp-accent); text-decoration: none; }
101+
.rtg-tp a:hover { color: var(--rtg-tp-accent-hover); }
102+
.rtg-tp .rtg-tp-breadcrumb { font-size: 13px; color: var(--rtg-tp-muted); margin: 0 0 20px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
103+
.rtg-tp .rtg-tp-breadcrumb a { color: var(--rtg-tp-muted); }
104+
.rtg-tp .rtg-tp-breadcrumb a:hover { color: var(--rtg-tp-accent); }
105+
.rtg-tp .rtg-tp-breadcrumb span[aria-current] { color: var(--rtg-tp-text); }
106+
.rtg-tp .rtg-tp-hero { display: flex; gap: 28px; flex-wrap: wrap; margin: 0 0 32px; }
107+
.rtg-tp .rtg-tp-img { flex: 0 0 320px; max-width: 100%; background: var(--rtg-tp-card); border: 1px solid var(--rtg-tp-border); border-radius: 12px; overflow: hidden; aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center; }
108+
.rtg-tp .rtg-tp-img img { width: 100%; height: 100%; object-fit: cover; margin: 0; }
109+
.rtg-tp .rtg-tp-info { flex: 1 1 320px; min-width: 260px; }
110+
.rtg-tp .rtg-tp-brand { font-size: 14px; font-weight: 600; color: var(--rtg-tp-muted); text-transform: uppercase; letter-spacing: .5px; margin: 0; }
111+
.rtg-tp h1.rtg-tp-title { font-size: 28px; font-weight: 700; letter-spacing: -0.5px; color: var(--rtg-tp-heading); margin: 4px 0 10px; padding: 0; }
112+
.rtg-tp .rtg-tp-rating { display: flex; align-items: center; gap: 8px; margin: 0 0 20px; }
113+
.rtg-tp .rtg-tp-star { font-size: 18px; color: var(--rtg-tp-star-empty); }
114+
.rtg-tp .rtg-tp-star-full { color: var(--rtg-tp-accent); }
115+
.rtg-tp .rtg-tp-star-half { background: linear-gradient(90deg, var(--rtg-tp-accent) 50%, var(--rtg-tp-star-empty) 50%); -webkit-background-clip: text; background-clip: text; color: transparent; }
116+
.rtg-tp .rtg-tp-rating-meta { font-size: 13px; color: var(--rtg-tp-muted); }
117+
.rtg-tp .rtg-tp-cta { display: inline-flex; align-items: center; justify-content: center; background: var(--rtg-tp-accent); color: #15130e; font-weight: 600; padding: 12px 22px; border-radius: 8px; font-size: 15px; }
118+
.rtg-tp .rtg-tp-cta:hover { background: var(--rtg-tp-accent-hover); color: #15130e; }
119+
.rtg-tp .rtg-tp-roamer { margin: 12px 0 20px; padding: 12px 16px; border-radius: 10px; background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.3); font-size: 14px; }
120+
.rtg-tp .rtg-tp-roamer strong { color: #60a5fa; }
121+
.rtg-tp h2.rtg-tp-section { font-size: 20px; font-weight: 700; color: var(--rtg-tp-heading); margin: 36px 0 14px; padding: 0; }
122+
.rtg-tp .rtg-tp-specs { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1px; background: var(--rtg-tp-border); border: 1px solid var(--rtg-tp-border); border-radius: 12px; overflow: hidden; }
123+
.rtg-tp .rtg-tp-spec { background: var(--rtg-tp-card); padding: 12px 16px; display: flex; justify-content: space-between; gap: 12px; }
124+
.rtg-tp .rtg-tp-spec-label { color: var(--rtg-tp-muted); font-size: 14px; }
125+
.rtg-tp .rtg-tp-spec-value { color: var(--rtg-tp-text); font-size: 14px; font-weight: 600; text-align: right; }
126+
.rtg-tp .rtg-tp-review { background: var(--rtg-tp-card); border: 1px solid var(--rtg-tp-border); border-radius: 10px; padding: 16px; margin: 0 0 12px; }
127+
.rtg-tp .rtg-tp-review-head { display: flex; justify-content: space-between; gap: 12px; margin: 0 0 6px; }
128+
.rtg-tp .rtg-tp-review-author { font-weight: 600; color: var(--rtg-tp-heading); font-size: 14px; }
129+
.rtg-tp .rtg-tp-review-title { font-weight: 600; font-size: 14px; margin: 4px 0; }
130+
.rtg-tp .rtg-tp-review-body { font-size: 14px; color: var(--rtg-tp-text); margin: 0; }
131+
.rtg-tp .rtg-tp-empty { color: var(--rtg-tp-muted); font-size: 14px; }
132+
@media (max-width: 600px) { .rtg-tp h1.rtg-tp-title { font-size: 23px; } }
133+
</style>
134+
135+
<div class="rtg-tp">
136+
<nav class="rtg-tp-breadcrumb" aria-label="Breadcrumb">
137+
<?php
138+
$last = count( $breadcrumb_items ) - 1;
139+
foreach ( $breadcrumb_items as $i => $crumb ) {
140+
if ( $i === $last ) {
141+
echo '<span aria-current="page">' . esc_html( $crumb['name'] ) . '</span>';
142+
} else {
143+
echo '<a href="' . esc_url( $crumb['url'] ) . '">' . esc_html( $crumb['name'] ) . '</a>';
144+
echo '<span aria-hidden="true">&rsaquo;</span>';
145+
}
146+
}
147+
?>
148+
</nav>
149+
150+
<div class="rtg-tp-hero">
151+
<?php if ( $image ) : ?>
152+
<div class="rtg-tp-img">
153+
<img src="<?php echo esc_url( $image ); ?>" alt="<?php echo esc_attr( $heading ); ?>" />
154+
</div>
155+
<?php endif; ?>
156+
157+
<div class="rtg-tp-info">
158+
<?php if ( $brand ) : ?><div class="rtg-tp-brand"><?php echo esc_html( $brand ); ?></div><?php endif; ?>
159+
<h1 class="rtg-tp-title"><?php echo esc_html( $heading ); ?></h1>
160+
161+
<div class="rtg-tp-rating">
162+
<span aria-hidden="true"><?php echo rtg_tire_page_stars( $avg ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup ?></span>
163+
<span class="rtg-tp-rating-meta">
164+
<?php
165+
echo $rating_cnt > 0
166+
? esc_html( number_format( $avg, 1 ) . ' (' . $rating_cnt . ' rating' . ( 1 === $rating_cnt ? '' : 's' ) . ')' )
167+
: 'No ratings yet';
168+
?>
169+
</span>
170+
</div>
171+
172+
<?php if ( $roamer_eff > 0 ) : ?>
173+
<div class="rtg-tp-roamer">
174+
<strong><?php echo esc_html( number_format( $roamer_eff, 2 ) ); ?> mi/kWh</strong> real-world efficiency
175+
<?php
176+
$r_mi = round( ( (float) ( $tire['roamer_total_km'] ?? 0 ) ) * 0.621371 );
177+
$r_veh = (int) ( $tire['roamer_vehicle_count'] ?? 0 );
178+
if ( $r_mi > 0 || $r_veh > 0 ) {
179+
echo ' &middot; <span style="color:var(--rtg-tp-muted)">' . esc_html( number_format( $r_mi ) . ' mi tracked across ' . $r_veh . ' vehicle' . ( 1 === $r_veh ? '' : 's' ) ) . '</span>';
180+
}
181+
?>
182+
</div>
183+
<?php endif; ?>
184+
185+
<?php if ( $link ) : ?>
186+
<a class="rtg-tp-cta" href="<?php echo esc_url( $link ); ?>" target="_blank" rel="nofollow sponsored noopener">View Tire &rarr;</a>
187+
<?php endif; ?>
188+
</div>
189+
</div>
190+
191+
<h2 class="rtg-tp-section">Specifications</h2>
192+
<div class="rtg-tp-specs">
193+
<?php foreach ( $specs as $label => $value ) : ?>
194+
<?php if ( '' !== trim( (string) $value ) ) : ?>
195+
<div class="rtg-tp-spec">
196+
<span class="rtg-tp-spec-label"><?php echo esc_html( $label ); ?></span>
197+
<span class="rtg-tp-spec-value"><?php echo esc_html( $value ); ?></span>
198+
</div>
199+
<?php endif; ?>
200+
<?php endforeach; ?>
201+
</div>
202+
203+
<h2 class="rtg-tp-section">Owner Reviews</h2>
204+
<?php if ( ! empty( $reviews ) ) : ?>
205+
<?php foreach ( $reviews as $review ) : ?>
206+
<div class="rtg-tp-review">
207+
<div class="rtg-tp-review-head">
208+
<span class="rtg-tp-review-author"><?php echo esc_html( $review['display_name'] ); ?></span>
209+
<span aria-hidden="true"><?php echo rtg_tire_page_stars( (float) $review['rating'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup ?></span>
210+
</div>
211+
<?php if ( ! empty( $review['review_title'] ) ) : ?>
212+
<div class="rtg-tp-review-title"><?php echo esc_html( $review['review_title'] ); ?></div>
213+
<?php endif; ?>
214+
<?php if ( ! empty( $review['review_text'] ) ) : ?>
215+
<div class="rtg-tp-review-body"><?php echo esc_html( $review['review_text'] ); ?></div>
216+
<?php endif; ?>
217+
</div>
218+
<?php endforeach; ?>
219+
<?php else : ?>
220+
<p class="rtg-tp-empty">No reviews yet. <a href="<?php echo esc_url( $review_url ); ?>">Be the first to review this tire.</a></p>
221+
<?php endif; ?>
222+
223+
<p style="margin-top:20px;"><a href="<?php echo esc_url( $review_url ); ?>">Write a review &rarr;</a></p>
224+
</div>
225+
<?php
226+
// End tire page content partial.

0 commit comments

Comments
 (0)