RECORDGOD/wp-bridge/includes/class-rg-storefront.php
type-two 5ad4de1d2c feat(bridge): RecordGod WP bridge plugin (WowPlatter successor) + thumb on browse/release
Thin WordPress/Woo bridge — RecordGod owns catalog/stock/pricing/shipping; the plugin is a storefront
skin + checkout adapter over /shop:
- storefront: SSR /records (browse) + /release/{id} inside the theme, with title/meta/JSON-LD MusicAlbum
- on-the-fly Woo product: /?rg_add=<sku> mints a hidden WC_Product_Simple from /shop/item (the WowPlatter
  trick — 25k catalog, only sold items become Woo products)
- shipping: WC_Shipping_Method quoting /shop/shipping/quote by cart item count (AusPost flat rate)
- orders: order_status_completed -> POST /shop/woo-order (X-Bridge-Key), idempotent, marks SKUs sold
- settings page: base URL + bridge key + store id
Backend: added thumb to /shop/browse + /shop/release so storefront pages have cover art.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:43:30 +10:00

190 lines
9.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
if (!defined('ABSPATH')) exit;
/**
* SEO storefront, server-rendered from RecordGod's /shop API inside the active theme's chrome.
* /records → browse (release-grouped, ?q= ?genre= ?page=)
* /release/{id} → release detail + in-stock copies + JSON-LD MusicAlbum/Product
* Add-to-cart points at /?rg_add=<sku>, which mints the Woo product on the fly (RG_Cart).
*/
class RG_Storefront {
static function init() {
add_action('init', [__CLASS__, 'rewrites']);
add_filter('query_vars', function ($v) {
return array_merge($v, ['rg_view', 'rg_id']);
});
add_action('template_redirect', [__CLASS__, 'render']);
}
static function rewrites() {
add_rewrite_rule('^records/?$', 'index.php?rg_view=records', 'top');
add_rewrite_rule('^release/([0-9]+)/?', 'index.php?rg_view=release&rg_id=$matches[1]', 'top');
}
static function render() {
$view = get_query_var('rg_view');
if (!$view) return;
if ($view === 'release') {
$id = (int) get_query_var('rg_id');
$data = RG_API::get('/shop/release/' . $id, [], 120);
if (!$data || empty($data['release'])) { self::not_found(); return; }
self::head_for_release($data);
get_header();
self::release_html($data);
get_footer();
exit;
}
// browse
$q = isset($_GET['q']) ? sanitize_text_field(wp_unslash($_GET['q'])) : '';
$genre = isset($_GET['genre']) ? sanitize_text_field(wp_unslash($_GET['genre'])) : '';
$page = max(1, (int) ($_GET['page'] ?? 1));
$args = array_filter(['q' => $q, 'genre' => $genre, 'page' => $page]);
$data = RG_API::get('/shop/browse', $args, 60);
self::head_for_browse($q, $genre);
get_header();
self::browse_html($data ?: ['items' => [], 'total' => 0, 'page' => 1, 'per' => 24], $q, $genre, $page);
get_footer();
exit;
}
// --- <head>: title / description / JSON-LD --------------------------------
static function head_for_browse($q, $genre) {
$title = trim(($q ?: $genre) ? ($q ?: $genre) . ' — Records' : 'Records') . ' · ' . get_bloginfo('name');
add_filter('pre_get_document_title', fn() => $title);
add_action('wp_head', function () {
echo '<meta name="description" content="Browse vinyl, CDs and cassettes in stock.">' . "\n";
});
}
static function head_for_release($data) {
$r = $data['release'];
$copies = $data['copies'];
$title = trim($r['artist'] . ' ' . $r['title']) . ' · ' . get_bloginfo('name');
add_filter('pre_get_document_title', fn() => $title);
add_action('wp_head', function () use ($r, $copies) {
$price = $copies ? min(array_column($copies, 'price')) : null;
$desc = trim(implode(' · ', array_filter([$r['artist'], $r['format'], $r['year'], $r['label']])));
echo '<meta name="description" content="' . esc_attr($desc) . '">' . "\n";
$ld = [
'@context' => 'https://schema.org',
'@type' => 'MusicAlbum',
'name' => $r['title'],
'byArtist' => ['@type' => 'MusicGroup', 'name' => $r['artist']],
];
if (!empty($r['thumb'])) $ld['image'] = $r['thumb'];
if (!empty($r['genre'])) $ld['genre'] = $r['genre'];
if ($price !== null) {
$ld['offers'] = [
'@type' => 'Offer', 'priceCurrency' => 'AUD', 'price' => $price,
'availability' => $copies ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
'url' => home_url('/release/' . $r['id']),
];
}
echo '<script type="application/ld+json">' . wp_json_encode($ld) . '</script>' . "\n";
});
}
// --- body -----------------------------------------------------------------
static function browse_html($d, $q, $genre, $page) {
$heading = $q ?: ($genre ?: 'Records');
echo '<main class="rg-shop" style="max-width:1100px;margin:2rem auto;padding:0 1rem">';
echo '<form method="get" action="' . esc_url(home_url('/records')) . '" style="margin-bottom:1.5rem">';
echo '<input type="search" name="q" value="' . esc_attr($q) . '" placeholder="Search artist or title"
style="padding:.6rem;width:60%;max-width:420px"> <button type="submit">Search</button></form>';
echo '<h1>' . esc_html($heading) . ' <small style="font-weight:400;color:#888">(' . (int) $d['total'] . ')</small></h1>';
if (!$d['items']) { echo '<p>Nothing in stock matching that.</p></main>'; return; }
echo '<div class="rg-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:1.2rem">';
foreach ($d['items'] as $it) {
$url = home_url('/release/' . (int) $it['release_id']);
echo '<a href="' . esc_url($url) . '" style="text-decoration:none;color:inherit">';
if (!empty($it['thumb']))
echo '<img src="' . esc_url($it['thumb']) . '" alt="' . esc_attr($it['title']) . '" loading="lazy"
style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px">';
echo '<div style="font-weight:600;margin-top:.4rem;font-size:.9rem">' . esc_html($it['title']) . '</div>';
echo '<div style="color:#888;font-size:.85rem">' . esc_html($it['artist']) . '</div>';
echo '<div style="margin-top:.2rem">$' . esc_html(number_format((float) $it['price'], 2));
if ((int) $it['copies'] > 1) echo ' <small style="color:#888">· ' . (int) $it['copies'] . ' copies</small>';
echo '</div></a>';
}
echo '</div>';
self::pager($d, $page, $q, $genre);
echo '</main>';
}
static function pager($d, $page, $q, $genre) {
$pages = (int) ceil($d['total'] / max(1, $d['per']));
if ($pages <= 1) return;
$link = fn($p) => esc_url(add_query_arg(array_filter(['q' => $q, 'genre' => $genre, 'page' => $p]), home_url('/records')));
echo '<div style="margin:2rem 0;text-align:center">';
if ($page > 1) echo '<a href="' . $link($page - 1) . '">← Prev</a> ';
echo '<span style="margin:0 1rem">Page ' . $page . ' of ' . $pages . '</span>';
if ($page < $pages) echo '<a href="' . $link($page + 1) . '">Next →</a>';
echo '</div>';
}
static function release_html($data) {
$r = $data['release'];
echo '<main class="rg-release" style="max-width:900px;margin:2rem auto;padding:0 1rem">';
echo '<p><a href="' . esc_url(home_url('/records')) . '">← Records</a></p>';
echo '<div style="display:flex;gap:2rem;flex-wrap:wrap">';
if (!empty($r['thumb']))
echo '<img src="' . esc_url($r['thumb']) . '" alt="' . esc_attr($r['title']) . '"
style="width:320px;max-width:100%;border-radius:8px">';
echo '<div style="flex:1;min-width:280px">';
echo '<h1 style="margin:0">' . esc_html($r['title']) . '</h1>';
echo '<h2 style="margin:.2rem 0 1rem;font-weight:400;color:#666">' . esc_html($r['artist']) . '</h2>';
foreach (['label' => 'Label', 'format' => 'Format', 'country' => 'Country',
'year' => 'Year', 'genre' => 'Genre', 'style' => 'Style'] as $k => $lbl)
if (!empty($r[$k]))
echo '<div><strong>' . $lbl . ':</strong> ' . esc_html($r[$k]) . '</div>';
echo '<h3 style="margin-top:1.5rem">In stock</h3>';
if (empty($data['copies'])) {
echo '<p>No copies in stock right now.</p>';
} else {
echo '<ul style="list-style:none;padding:0">';
foreach ($data['copies'] as $c) {
$cond = trim(implode(' / ', array_filter([$c['condition'], $c['sleeve_cond'] ?? null])));
echo '<li style="display:flex;justify-content:space-between;align-items:center;
border:1px solid #eee;border-radius:6px;padding:.6rem .8rem;margin-bottom:.5rem">';
echo '<span>$' . esc_html(number_format((float) $c['price'], 2));
if ($cond) echo ' <small style="color:#888">· ' . esc_html($cond) . '</small>';
echo '</span>';
echo '<a class="button" href="' . esc_url(home_url('/?rg_add=' . rawurlencode($c['sku']))) . '"
style="background:#111;color:#fff;padding:.4rem .9rem;border-radius:5px;text-decoration:none">Add to cart</a>';
echo '</li>';
}
echo '</ul>';
}
echo '</div></div>';
if (!empty($data['tracks'])) {
echo '<h3 style="margin-top:2rem">Tracklist</h3><ol style="columns:2;max-width:600px">';
foreach ($data['tracks'] as $t) {
echo '<li>' . esc_html($t['title']);
if (!empty($t['duration'])) echo ' <small style="color:#999">' . esc_html($t['duration']) . '</small>';
echo '</li>';
}
echo '</ol>';
}
echo '</main>';
}
static function not_found() {
status_header(404);
get_header();
echo '<main style="max-width:700px;margin:3rem auto;text-align:center"><h1>Record not found</h1>'
. '<p><a href="' . esc_url(home_url('/records')) . '">Browse the shop →</a></p></main>';
get_footer();
exit;
}
}