diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa4b69..cf85d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The GitHub release body is a summary, not a copy of the changelog section. Write ## Unreleased +- Product pages now render their own head metadata and schema.org JSON-LD from `partials/product_structured_data.html` instead of the platform `{% seo %}` output. A product with variants is marked up as a `ProductGroup` with one `Product` and `Offer` per variant, each with its own price, currency, availability, SKU and GTIN. Brand, GTIN and MPN come from product metadata keys `brand`, `gtin` and `mpn` when the store defines them; without `brand` the store name is used, and the UPC is used as the GTIN only when it is 8, 12, 13 or 14 digits. The UPC is no longer emitted as `mpn`, the description is no longer cut at 160 characters in the JSON-LD, the offer price is the price shown on the page, and the rating is included only when Product Reviews is on. Every string is escaped for JSON, so a product title containing a double quote (`12" Boot`) no longer produces markup that search engines and shopping agents reject. A product or variant without a price gets no offer, and the image list is capped at three. The meta and Open Graph tags match the platform's apart from the brand and two corrected values: `og:type` is now `product`, and `product:availability` is `instock` or `outofstock`. See `docs/pdp-customization.md`, "Structured data". - Added Product Pages > Shop All > Show Filters on Shop All (`shop_all_filters`, off by default). When on, the Shop All page shows the same filter rail, mobile drawer, and sticky filter bar as category pages, using the filters created under Storefront > Filters. The platform already applied filter parameters on Shop All; only the controls were missing. - Fixed category pages without any Storefront filters squeezing the product grid into the 15rem filter-rail column from 1024px up (240px wide at a 1280px viewport). The two-column layout now applies only when the filter rail renders. Introduced in 1.2.0. - Fixed Pages, Support Articles, and Blog posts showing editor-written content without paragraph, heading, list, separator, and table spacing. Tailwind's reset had removed it, and the `content-body` class the Article and Blog templates already carried had no styles. Editor headings now follow the Heading Scale setting and sit below the page title, which now uses the same heading scale on Pages, Articles, and Blog posts. Wide editor tables scroll sideways inside their own box instead of widening the page; without JavaScript they stay plain tables and may overflow on narrow screens (#85). diff --git a/docs/pdp-customization.md b/docs/pdp-customization.md index 5ffc1e9..010c126 100644 --- a/docs/pdp-customization.md +++ b/docs/pdp-customization.md @@ -157,3 +157,22 @@ Spark's current PDP is one template plus helper scripts. When repeated custom PD | `partials/product_related.html` | Recommended and related products. | Do not split the PDP just to satisfy one custom design. Split when the same behavior-preserving modules will be reused across multiple stores or when an app integration needs a stable override surface. + +## Structured data + +`templates/catalogue/product.html` overrides the `seo` block with `partials/product_structured_data.html`, which renders the product page's meta tags, Open Graph tags and schema.org JSON-LD. The platform `{% seo %}` tag is not called on product pages, so a fork that restores it gets the platform markup back and loses the variant and identifier fields below. + +| JSON-LD field | Source | +|---|---| +| `@type` | `ProductGroup` for a parent product, `Product` otherwise | +| `hasVariant[]` | One `Product` per child, each with its own `Offer` from `purchase_info_for_product`, the child's first image (or the parent's when the child has none) and the product description. Google's merchant listings report rejects a variant without `image`. | +| `image` | The first three product images at 1200x1200, else the meta image; omitted when there is no image at all | +| `brand.name` | Product metadata `brand`, else the store name | +| `gtin` | Variant metadata `gtin` on each variant, or product metadata `gtin` on a product without variants; else the UPC when it is all digits and 8, 12, 13 or 14 long. Other UPCs are left out without a warning. A parent product carries no `gtin`. | +| `mpn` | Product or variant metadata `mpn`; omitted otherwise | +| `offers` | `session.price.price`, the price the page shows. A product or variant with no price record gets no `offers`, since Google rejects an offer without a price. | +| `aggregateRating` | Only when `settings.product_reviews` is on and the product has approved reviews | + +To use the metadata keys, create product metadata definitions with the keys `brand`, `gtin` and `mpn` in the Dashboard and fill them per product or variant. + +Every string written into the JSON-LD goes through `escapejs`, including the UPC. Keep that when editing the partial: a value written with `|safe` or without escaping breaks the JSON on the first title with a double quote, and a crawler then drops the whole block. Check a changed partial against a real product with a structured-data validator such as Google's Rich Results Test. diff --git a/partials/product_structured_data.html b/partials/product_structured_data.html new file mode 100644 index 0000000..933dc2a --- /dev/null +++ b/partials/product_structured_data.html @@ -0,0 +1,88 @@ +{# Spark Theme — PDP head metadata. Rendered from the seo block in templates/catalogue/product.html. #} +{# Replaces the platform {% seo %} output on product pages: meta and Open Graph tags plus schema.org JSON-LD. #} +{# Every string inside the JSON-LD goes through escapejs, so quotes, backslashes and in merchant text stay valid JSON. #} +{# Identifiers come from product metadata when the store defines them: brand, gtin, mpn. Without brand, the store name is used. #} +{# gtin falls back to the UPC only when the UPC is 8, 12, 13 or 14 digits. The UPC is never emitted as mpn. #} +{% image_thumbnail product.get_meta_image "1200x630" upscale=True crop="center" as thumb %} +{% purchase_info_for_product request product as session %} + + + +{% if not product.is_public %} + +{% endif %} + + + + + + + +{% if session.price.exists %} + + +{% endif %} + + + diff --git a/templates/catalogue/product.html b/templates/catalogue/product.html index c39c35a..336b70f 100644 --- a/templates/catalogue/product.html +++ b/templates/catalogue/product.html @@ -3,7 +3,7 @@ {% block title %}{{ product.get_meta_title }} | {{ block.super }}{% endblock %} {% block seo %} - {% seo %} + {% include 'partials/product_structured_data.html' %} {% endblock %} {% block critical_preloads %}