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>
190 lines
9.4 KiB
PHP
190 lines
9.4 KiB
PHP
<?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;
|
||
}
|
||
}
|