also looking at this
umbrella: pet-store storefront fixes (rolling)
#3
586 files
+38082
-7487
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -57,9 +57,40 @@ const principles = [ | ||
| 57 | 57 | body: 'Every bag has a batch code. Scan it, and you see the farm, the processing date, and the third-party lab report.', |
| 58 | 58 | }, |
| 59 | 59 | ] |
| 60 | ||
| 61 | // Org + WebSite JSON-LD for the homepage. Search engines read this to | |
| 62 | // render the brand name + logo as a knowledge panel and to wire | |
| 63 | // sitelinks search if Google decides to offer it. Sticking to one | |
| 64 | // `@graph` keeps the two nodes referenceable from each other (the | |
| 65 | // website's `publisher` points at the org's `@id`). Kept on the | |
| 66 | // homepage only — that's where Google reads org-level signals from. | |
| 67 | const orgSchema = JSON.stringify({ | |
| 68 | '@context': 'https://schema.org', | |
| 69 | '@graph': [ | |
| 70 | { | |
| 71 | '@type': 'Organization', | |
| 72 | '@id': 'https://petstore.example/#org', | |
| 73 | 'name': 'PetStore', | |
| 74 | 'url': 'https://petstore.example/', | |
| 75 | 'logo': 'https://petstore.example/favicons/favicon-512x512.png', | |
| 76 | 'sameAs': [], | |
| 77 | }, | |
| 78 | { | |
| 79 | '@type': 'WebSite', | |
| 80 | '@id': 'https://petstore.example/#site', | |
| 81 | 'url': 'https://petstore.example/', | |
| 82 | 'name': 'PetStore', | |
| 83 | 'publisher': { '@id': 'https://petstore.example/#org' }, | |
| 84 | }, | |
| 85 | ], | |
| 86 | }) | |
| 60 | 87 | </script> |
| 61 | 88 | |
| 62 | 89 | @section('content') |
| 90 | {{-- Org + Website JSON-LD. Mirrors the inline placement of the | |
| 91 | Product schema on /products/[slug]. STX's script rewriter passes | |
| 92 | `application/ld+json` through untouched. --}} | |
| 93 | <script type="application/ld+json">{!! orgSchema !!}</script> | |
| 63 | 94 | <!-- Hero --> |
| 64 | 95 | <section class="overflow-hidden relative"> |
| 65 | 96 | <div class="grid gap-12 items-center md:grid-cols-2 mx-auto pb-24 pt-20 px-6 max-w-6xl"> |
| @@ -36,10 +36,35 @@ const products = db.query(` | ||
| 36 | 36 | imageSrcset: productSrcset(p.image_url), |
| 37 | 37 | badge: p.inventory_count < 30 ? 'Limited' : '', |
| 38 | 38 | })) |
| 39 | ||
| 40 | // CollectionPage + ItemList JSON-LD. Tells Google this URL is a | |
| 41 | // category/collection rather than a single product, and provides | |
| 42 | // the catalogue order so the breadcrumb + search-result list show | |
| 43 | // items in the same order shoppers see them. Generated only when | |
| 44 | // there's at least one product so we don't emit an empty ItemList. | |
| 45 | const collectionSchema = products.length > 0 ? JSON.stringify({ | |
| 46 | '@context': 'https://schema.org', | |
| 47 | '@type': 'CollectionPage', | |
| 48 | 'name': 'PetStore — All products', | |
| 49 | 'description': 'Single-ingredient, slow-dehydrated treats for dogs.', | |
| 50 | 'mainEntity': { | |
| 51 | '@type': 'ItemList', | |
| 52 | 'numberOfItems': products.length, | |
| 53 | 'itemListElement': products.map((p, i) => ({ | |
| 54 | '@type': 'ListItem', | |
| 55 | 'position': i + 1, | |
| 56 | 'url': `/products/${p.slug}`, | |
| 57 | 'name': p.name, | |
| 58 | })), | |
| 59 | }, | |
| 60 | }) : '' | |
| 39 | 61 | </script> |
| 40 | 62 | |
| 41 | 63 | @section('content') |
| 42 | 64 | <div class="mx-auto px-6 py-12 max-w-6xl"> |
| 65 | @if (collectionSchema) | |
| 66 | <script type="application/ld+json">{!! collectionSchema !!}</script> | |
| 67 | @endif | |
| 43 | 68 | |
| 44 | 69 | <div class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between mb-10"> |
| 45 | 70 | <div> |
| @@ -156,6 +156,18 @@ const productSchema = product ? JSON.stringify({ | ||
| 156 | 156 | non-JS script types (application/ld+json) unchanged. --}} |
| 157 | 157 | @if (productSchema) |
| 158 | 158 | <script type="application/ld+json">{!! productSchema !!}</script> |
| 159 | {{-- BreadcrumbList: Home → Products → <product>. Google uses | |
| 160 | this to render the trail under the title in search results | |
| 161 | instead of the raw URL. --}} | |
| 162 | <script type="application/ld+json">{!! JSON.stringify({ | |
| 163 | '@context': 'https://schema.org', | |
| 164 | '@type': 'BreadcrumbList', | |
| 165 | 'itemListElement': [ | |
| 166 | { '@type': 'ListItem', position: 1, name: 'Home', item: '/' }, | |
| 167 | { '@type': 'ListItem', position: 2, name: 'Shop', item: '/products' }, | |
| 168 | { '@type': 'ListItem', position: 3, name: product.name, item: `/products/${product.slug}` }, | |
| 169 | ], | |
| 170 | }) !!}</script> | |
| 159 | 171 | @endif |
| 160 | 172 | |
| 161 | 173 | @if (!found) |