Seo ecommerce
Skill PIXARTSeu/Synapse/packages/codegraph/data/skill/seo-ecommerce
E-commerce SEO for product and category pages — Product/Offer/AggregateRating JSON-LD, faceted navigation & crawl control, variant/canonical handling, out-of-stock and pagination, category page optimization, and merchant feed basics. Includes Next.js 15 App Router + Payload code and a 0-100 audit rubric. Use when working on ecommerce SEO, product SEO, shop/store pages, product schema, category pages, faceted filters, merchant feed, or shopping visibility. Triggers on: ecommerce SEO, product SEO, product schema, Product JSON-LD, Offer, AggregateRating, faceted navigation, canonical variants, out of stock SEO, pagination SEO, category page SEO, merchant feed, shopping.From its SKILL.md
npx -y skills add PIXARTSeu/Synapse --skill seo-ecommerceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
17.3 KB, ~4.2k tokens by cl100k_base, as published. Nobody here has run it
E-commerce SEO
Product and category pages have failure modes that generic page SEO misses: schema gaps that kill rich results, faceted filters that explode crawl budget, variant URLs that fragment ranking signals, and out-of-stock handling that silently drops products. This skill is implementation-first for our stack (Next.js 15 App Router + RSC, Payload CMS) plus an audit rubric for assessing an existing store.
When to use
- Building or auditing product detail pages (PDPs) and category/listing pages (PLPs).
- A store has faceted filters (size, color, price, brand) and crawl-budget concerns.
- Products have variants (size/color) and you need canonical + indexing strategy.
- You need Product/Offer/AggregateRating JSON-LD that earns rich results.
- You're preparing a Merchant feed and want on-page data to match it.
1. Product schema (Product + Offer)
Google rich results need a small set of required fields and a larger set of
recommended ones. Render JSON-LD server-side so it's in the initial HTML — many
storefronts inject it client-side, which is risky for crawling. In the App Router,
emit a <script type="application/ld+json"> from the server component.
// app/[locale]/products/[slug]/ProductJsonLd.tsx (server component)
import type { Product } from '@/payload-types'
const AVAILABILITY = {
inStock: 'https://schema.org/InStock',
outOfStock: 'https://schema.org/OutOfStock',
preOrder: 'https://schema.org/PreOrder',
} as const
export function ProductJsonLd({ product, url }: { product: Product; url: string }) {
const ld = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.title,
description: product.seoDescription ?? product.description,
image: product.images?.map((i) => i.url).filter(Boolean), // array, absolute URLs, >=1
sku: product.sku,
...(product.gtin13 && { gtin13: product.gtin13 }),
...(product.mpn && { mpn: product.mpn }),
brand: { '@type': 'Brand', name: product.brand?.name },
offers: {
'@type': 'Offer',
url,
priceCurrency: product.currency, // ISO 4217: EUR, USD, GBP
price: product.price.toFixed(2), // number string, NO currency symbol
availability: AVAILABILITY[product.stockStatus],
itemCondition: 'https://schema.org/NewCondition',
// priceValidUntil (ISO 8601), shippingDetails, hasMerchantReturnPolicy: see below
},
...(product.reviewCount > 0 && {
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.ratingAvg.toFixed(1),
reviewCount: product.reviewCount,
},
}),
}
// < escape avoids breaking out of the <script> tag
const html = JSON.stringify(ld).replace(/</g, '\\u003c')
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: html }} />
}
AggregateRating + Review
Only emit aggregateRating when you have real, on-page reviews — fabricated or
off-page-only ratings violate Google's policy and risk a manual action. Require
both ratingValue and reviewCount. Optionally include individual review
objects with author and reviewRating.
Validation rules (these silently disqualify rich results when violated)
priceis a plain number string ("29.99"), never"$29.99"or"29,99".availabilityuses the full Schema.org URL enum, not"in stock".priceCurrencyis ISO 4217.imageis an array of absolute, high-res URLs (>= 1).brand.nameis non-empty and not a placeholder ("N/A").priceValidUntilis ISO 8601 if present (omit it rather than ship a stale past date).- If
aggregateRatingis present, bothratingValueandreviewCountexist and are > 0.
Shipping + returns (Merchant Center alignment)
For Shopping/free listings, add structured shipping and return data so on-page matches feed data:
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": { "@type": "MonetaryAmount", "value": "4.90", "currency": "EUR" },
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
"transitTime": { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 3, "unitCode": "DAY" }
}
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "IT",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
}
Schema scoring (0-100)
| Completeness | Score |
|---|---|
| All required fields valid (name, image, offers.price, priceCurrency, availability) | 50 |
+ valid aggregateRating with real reviews | 65 |
+ sku / gtin / mpn | 75 |
+ shippingDetails | 85 |
+ hasMerchantReturnPolicy | 90 |
+ 3+ review objects | 100 |
2. Variants and canonicalization
Variant handling is where ranking signals fragment most. Pick one model and apply it consistently across the store.
| Model | When | URL shape | Indexing |
|---|---|---|---|
| Single canonical PDP | Variants are minor (color/size of same product) | /p/jacket with client-side variant switch | Index the canonical; variant params self-canonical to it |
| Distinct variant URLs | Variants differ materially (separate demand, different images/specs/price) | /p/jacket-red, /p/jacket-blue | Each indexable, each self-canonical |
Default to single canonical PDP unless variants have meaningfully different search demand. Variant state goes in a query param or hash that canonicalizes back:
// app/[locale]/products/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug, locale } = await params
const product = await getProduct(slug)
const canonical = `${SITE_URL}/${locale}/products/${product.slug}`
return {
alternates: {
canonical, // variant query params (?color=red) resolve here
languages: buildHreflang(product, locale), // next-intl locales
},
openGraph: { images: product.images?.[0]?.url },
}
}
Rules:
- A
?color=redURL must carry<link rel="canonical">pointing to the clean PDP. - Never let the same product body live at multiple indexable URLs without canonicals.
- If you DO split variants into distinct URLs, each must be self-canonical (not pointing at a "parent") and have unique title, images, and (if applicable) price.
3. Faceted navigation and crawl control
Faceted filters (size + color + price + brand + sort) generate combinatorial URL
explosion. Left unmanaged, crawlers waste budget on near-duplicate, thin pages
and may index junk like ?sort=price&color=red&color=blue&page=3.
Decision: index, canonicalize, or block
| Facet state | Treatment | Mechanism |
|---|---|---|
| Single high-demand facet ("red dresses") | Index — it's a real landing page | Crawlable link, self-canonical, unique title/H1 |
| Multi-facet combos, sorts, pagination noise | Crawlable but not indexable | robots: noindex, follow OR canonical to base category |
Infinite / low-value params (sessionid, tracking, view=grid) | Block from crawl | robots.txt Disallow, or keep links non-crawlable |
Implementation
Curate which facet pages deserve indexing (a finite allowlist) rather than letting every combination in:
// app/[locale]/[category]/page.tsx
type Search = { color?: string; size?: string; sort?: string; page?: string }
export async function generateMetadata({ params, searchParams }): Promise<Metadata> {
const { category, locale } = await params
const sp = (await searchParams) as Search
const base = `${SITE_URL}/${locale}/${category}`
// Allowlist of indexable single-facet landing pages
const indexableFacet =
isCuratedLandingPage(category, sp) // e.g. only {color} alone, no sort/page
return {
alternates: { canonical: indexableFacet ? withFacets(base, sp) : base },
robots: indexableFacet ? undefined : { index: false, follow: true },
}
}
- Keep filter links real
<a href>(crawlable) only for facets you want crawled; render the rest as buttons/JS so crawlers don't discover infinite combinations. - Use
noindex, follow(notnofollow) so link equity still flows to products. - Put long-tail block rules in
robots.txtfor tracking/sort/view params:
# robots.txt
User-agent: *
Disallow: /*?*sort=
Disallow: /*?*view=
Disallow: /*&color=*&color= # multi-select duplicate noise
Allow: /
Sitemap: https://example.com/sitemap.xml
Caveat: robots.txt disallow prevents crawling but a URL can still appear in
results if linked externally. To guarantee de-indexing, allow the crawl and serve
noindex instead. Pick one per URL pattern; don't combine them on the same URL
(a noindex on a robots-blocked URL is never read).
4. Out-of-stock and discontinued products
Don't 404 or delete a temporarily out-of-stock product — you lose its accumulated ranking and inbound links.
| State | HTTP | Indexing | Schema availability | UX |
|---|---|---|---|---|
| Temporarily OOS, returning | 200 | keep indexed | OutOfStock (keep offers) | show "notify me", related/alternatives |
| Permanently discontinued, replacement exists | 301 | redirect | — | redirect to successor or category |
| Permanently gone, no replacement | 410 | de-index | — | helpful 410 page with search/category links |
| Seasonal, returns yearly | 200 | keep indexed | OutOfStock / PreOrder | keep page warm, link from category |
Keep the offers block with availability: OutOfStock so the product stays
eligible to re-enter rich results when restocked. Surface in-stock alternatives
above the fold to recover the visit.
5. Pagination on category pages
rel="next"/"prev" is no longer used by Google as an indexing signal. Current
guidance:
- Each paginated page (
?page=2) should be a crawlable, self-canonical, unique URL — do NOT canonicalize page 2..N to page 1 (that hides those products from crawl). - Ensure every product is reachable: either real paginated
<a>links or a crawlable "load more" that updates the URL. Pure JS infinite scroll with no crawlable URLs hides deep products. - Give paginated pages distinct titles (
Category — Page 2) to avoid duplicate-title warnings, but keep the primary keyword on page 1. - Page 1 of a category is the canonical landing page; deep pages are crawl paths, not landing targets.
// Reachability: emit crawlable pagination links even with JS load-more
<nav aria-label="Pagination">
{hasPrev && <a href={`?page=${page - 1}`} rel="prev">Previous</a>}
{hasNext && <a href={`?page=${page + 1}`} rel="next">Next</a>}
</nav>
6. Category (PLP) optimization
Category pages are usually a store's highest-traffic SEO targets — they rank for head terms ("running shoes") while PDPs rank long-tail.
- One H1 matching the category's primary keyword.
- A short, unique intro paragraph above or below the grid (not boilerplate across categories) — gives the page indexable text beyond product tiles.
- Breadcrumb JSON-LD (
BreadcrumbList) for the path: Home > Category > Subcategory. - Internal links to top sub-categories and complementary categories.
- Stable, keyword-clean URLs:
/running-shoes, not/c?id=482. - LCP is usually the first product image / hero — prioritize it (see CWV below).
// BreadcrumbList JSON-LD
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Shoes", "item": "https://example.com/shoes" },
{ "@type": "ListItem", "position": 3, "name": "Running Shoes", "item": "https://example.com/shoes/running" }
]
}
7. Product page on-page checklist
- Title:
[Product] - [Key feature] | [Brand], under ~60 chars, primary keyword + brand. - Meta description: keyword + benefit + price/CTA, under ~155 chars.
- H1: single, matches product name; H2s for Features, Specs, Reviews, Related.
- Images: descriptive alt text (product + distinguishing feature), descriptive
filenames (not
IMG_001.jpg), modern format (AVIF/WebP), >= 800px for Shopping eligibility, >= 3 images, lazy-load below-fold only (eager the LCP hero). - Content: unique description (NOT manufacturer copy-paste — that creates cross-site duplication), >= ~200 words, a real specs table, on-page UGC reviews.
- Internal linking: breadcrumbs, related/cross-sell, keyword-rich link back to category.
- Technical: server-rendered JSON-LD, correct canonical, mobile-clean, fast LCP.
8. Merchant feed basics
The on-page data and the Merchant feed must agree, or you get feed disapprovals and "price mismatch" / "availability mismatch" warnings.
| Feed field | Must match on-page | Notes |
|---|---|---|
id | stable SKU | never reuse across products |
title / description | PDP title/description | front-load attributes (brand, type, size, color) |
link | canonical PDP URL | not a variant param URL |
price | offers.price + visible price | currency must match |
availability | offers.availability | OOS in feed when OOS on page |
gtin / mpn / brand | schema identifiers | GTIN strongly improves match rate |
image_link | primary product image | >= 800px, no watermarks/promo overlays |
For AI-generated product imagery, follow the platform's transparency requirement
(IPTC DigitalSourceType: TrainedAlgorithmicMedia) to stay feed-compliant.
Audit methodology
When auditing an existing store (use your own crawler/tooling to fetch and render pages — many storefronts inject schema client-side, so compare raw HTML vs rendered HTML to confirm JSON-LD is server-rendered).
Scoring (0-100)
| Category | Weight | What it measures |
|---|---|---|
| Product schema completeness & validity | 25% | Required + recommended fields, validation rules pass |
| Crawl control (facets/params/pagination) | 20% | No combinatorial index bloat; products reachable |
| Variant/canonical handling | 15% | Consistent model, no fragmented duplicates |
| Image optimization | 15% | Alt text, format, sizing, count, LCP hero |
| Content quality | 15% | Unique descriptions, specs, on-page reviews |
| Category/internal linking & CWV | 10% | Breadcrumbs, related links, LCP < 2.5s |
Report priorities as Critical > High > Medium > Low, each with expected impact and the data source ("rendered HTML", "raw HTML", "feed export").
Core Web Vitals (current thresholds)
INP replaced FID as a Core Web Vital. Targets: LCP < 2.5s, INP < 200ms, CLS < 0.1. On PDPs the LCP element is the hero image (eager-load + sized); on PLPs it's the first product tile. CLS offenders: late-loading price/badges, unsized images, sticky add-to-cart bars injected after paint.
Falsifiability check (do this before claiming a fix worked)
For every recommendation, state how you'd know it FAILED and the leading indicator to watch — don't wait on rankings alone.
| Change | "How would we know it failed?" | Leading indicator (days, not weeks) |
|---|---|---|
| Added Product schema | Rich Results test still errors; no rich snippet eligibility | Search Console "Products" enhancement report: valid items count rises, errors drop |
| Faceted noindex/robots rules | Index bloat persists or, worse, real products dropped | Crawl stats + "Indexed, not submitted" count; verify product URLs still indexed via URL Inspection |
| OOS kept-indexed (vs 404) | Product loses position after restock | Position history for the product URL; impressions don't collapse during OOS window |
| Variant canonical consolidation | Duplicate variant URLs still indexed | site: / Search Console page count for variant params trends to ~0 |
| Unique category intro copy | Page still seen as thin/duplicate | Impressions for category head term; "Duplicate without user-selected canonical" disappears |
If you can't name a failure signal and a leading indicator, the recommendation isn't testable — sharpen it before shipping.
Cross-skill integration
| Need | Pair with |
|---|---|
| Generic JSON-LD generation/validation | seo-schema |
| Product image audit (alt, format, dimensions) | seo-images |
| Description E-E-A-T and uniqueness | seo-content |
| Page-level CWV / rendering / canonicals | seo-technical |
| Search Console enhancement reports | seo-google |
Parts adapted from claude-seo (MIT, © 2026 agricidaniel).
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.