Repository navigation
feat(seo): storefront metadata, structured data, sitemap and robots - #26
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ories Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The category header shows its description as plain text, so parsing it as Markdown dropped characters the page keeps (*, #, [x](y)) and the meta description no longer matched the page. textExcerpt applies the AC-6 cut to plain text; plainExcerpt now builds on it for product Markdown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own module - brand-assets.ts holds the brand card and logo paths and sizes, so metadata.ts is only the page metadata builder - brandCardResponse and logoResponse share one renderer - pageMetadata builds the description field once - drop twitter title and description: spec 0018 AC-2 asks only for the card and images, and Next fills the text from openGraph (e2e checks it) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the site facts Every storefront route now calls storePageMetadata(page) from site.ts instead of importing both pageMetadata and site; metadata.ts stays pure for its tests. The product page takes the store facts from site.ts instead of building them from env inline, and StoreFacts types the country as StoreCountry. Also fixes the import order in legal-page.tsx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildListing branched on scope.kind again after inScopeProducts had, and split the search words twice. scopeOf now returns the in scope products with their featured and default orders; inScopeProducts reads from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge's noindex - spec 0018 build plan 5: a category shown and hidden in the admin appears in and leaves the sitemap on the next request (AC-19) - AC-22: checkout complete, the account and admin auth pages, every signed in account page and every admin panel page keep noindex with no canonical or JSON-LD; /checkout waits for its redirect to the cart Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… AGENTS.md - AC-6 says the 160 characters include the ellipsis; AC-5 and AC-14 say a blank meta field counts as unset; AC-3 names the brand colour test - verify: AC-19 ticked (dev locally, the build through CI e2e), the AC-22 step widened; the Rich Results Test stays open until a preview deploy, now a spec follow-up and noted on the scope's Verify step - catalog, categories and legal AGENTS.md: the canonical and SEO files, the sitemap reads, storePageMetadata, the plain text category rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every public storefront page now has full search and share metadata: a unique title and description, an absolute canonical link, and an Open Graph share card. Product pages also carry Product structured data (JSON-LD) with one Offer per variant, the "Was" price and the delivery fee. The store gains a
robots.txtthat blocks crawling everywhere except production, a sitemap of every public page, and a generated brand share card and logo. Nothing in the database changes.Why
Implements spec 0018 (scope feature 17, Launch readiness). Shoppers should see real price, availability and sale in search results and a proper image when a link is shared; the store owner wants every public product and category found and nothing hidden indexed; and preview deploys must never compete with the real store in search. Options and reasoning are in rationale.md.
Changes
Shared SEO helpers (
src/lib/seo/)pageMetadatabuilds title, description, absolute canonical, Open Graph and Twitter card for every indexable page. It replaceslegalMetadataand the category page's hand written metadata.og:localecomes fromSTORE_LOCALE(pl_PLstyle, omitted for a bare language).metadataBasefromNEXT_PUBLIC_SITE_URL, so every URL is absolute on the site's own origin.plainExcerptturns Markdown into plain text and cuts it at a word boundary at 160 characters with…. It usesmdast-util-from-markdownandmdast-util-to-string(new direct dependencies, already inside thereact-markdowntree).JsonLdScriptis the one place JSON-LD is written, and it escapes every<, so admin text holding</script>cannot break out of the tag. JSON-LD is typed withschema-dts(dev dependency, types only).robotsRules(vercelEnv, siteUrl): underVERCEL_ENV=production, allow/, disallow/admin,/api,/auth, and name the sitemap. Anything else (preview, local, CI, unset) isDisallow: /.Product pages
meta_title/meta_description, falling back to the name and the description's plain text.og:imageis the first gallery photo, or the brand card when there is none.shippingDetailsfrom the sameshippingCentsrule the cart uses, read from the cached settings so an admin save shows on the next request.getProductBySlugnow also returnsmetaTitle,metaDescriptionand each variant'ssku.Listings (home and category)
listingCanonicalrule on the parsed listing state: any filter or non default sort points at the plain path; an unfiltered paged view points at?page=N, where N is the page actually shown.shownPageis now exported fromlisting.tsand used by bothbuildListingand the canonical rule, so they cannot drift. Unknown params like?utm_source=xare ignored., page N. No search or filter text ever reaches metadata.plainExcerpt. Category pages render a BreadcrumbList; the home page renders Organization and WebSite JSON-LD in its static shell./searchstaysnoindex, follow, with no canonical, and now uses the brand card.Brand images
/brand-card.png(1200x630) and/logo.png(512x512) are fixed pathnext/ogroute handlers, prerendered at build.app/icon.tsxreuses the logo as the tab icon;favicon.icostays.brand.colorsinsrc/lib/brand/brand.ts: sRGB hex copies of the background and foreground tokens, sincenext/ogcannot readoklch.Sitemap
/sitemap.xmllists home, the five legal pages (dated by theirupdatedfront matter), every visible category, and every active product with a live variant plus its image URLs. Nothing else.'use cache'functions taggedcatalogandcategories, so an existing product or category write shows up on the next request with no deploy.Docs: spec 0018 with rationale and verify steps, scope feature 17 marked done (plus a new parked item for return window and delivery time as data), and
src/features/legal/AGENTS.mdupdated forlegalSeo.How to test / verify
pnpm install(two new dependencies, one new dev dependency). No migration.pnpm testfor the metadata helper, excerpt, JSON-LD escaping, structured data builders, robots rule (both branches), canonical rule, product SEO and the sitemap composition.pnpm test:db -- tests/db/sitemap.db.test.tsfor drafts, archived and variant less products and hidden categories staying out of the sitemap.pnpm test:e2e tests/e2e/seofor product, category, home, search and legal heads, JSON-LD, paged canonicals, the sitemap, robots, and hidden and private pages stayingnoindex.curl -s localhost:3000/robots.txtshowsDisallow: /locally. Open/brand-card.pngand/logo.png. When reading a page's head, send a Googlebot user agent, since Next only blocks on metadata for known bots.The full checklist with results is in verify.md.
Risk & rollout
searchParamsfor the canonical page. Normal browsers get the head streamed after the shell; crawlers still get it up front. The results were already request time, so the shells stay as they were.VERCEL_ENV=productionallows crawling, androbots.tsprerenders at build, which matches how Vercel builds production deployments.pnpm build && pnpm start(the e2e test "the sitemap follows a publish and a hide on the next request" already covers it).rationale.md.Notes for reviewers
src/features/catalog/listing/canonical.tsand its use ofshownPage, and the Offer building insrc/features/catalog/seo.ts.opengraph-imagefile convention on purpose: the convention gives a hashed URL the Organization logo cannot name, and Next mergesopenGraphshallowly, so a child page could drop the inherited image./check reviewrecord exists yet for this branch indocs/reviews/.🤖 Generated with Claude Code