[{"data":1,"prerenderedAt":790},["ShallowReactive",2],{"blog-schema-org-structured-data-b2b-shop-en":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":26,"sources":35,"cover":72,"og":73,"expertise":74,"locales":75,"lang":76,"title":79,"description":80,"coverAlt":81,"metaTitle":82,"takeaways":83,"faq":89,"toc":105,"blocks":139,"others":562},"schema-org-structured-data-b2b-shop","2026-10-17",10,"web",[9,10,11,12,13],"Schema.org","Structured data","B2B e-commerce","Technical SEO","Nuxt",[15,16,17,18,19,20,21,22,23,24,25],"schema.org b2b shop","product structured data b2b","merchant listing structured data","product snippet vs merchant listing","unitpricespecification referencequantity","productgroup variants structured data","organization schema vatid","b2b prices behind login seo","json-ld nuxt schema org","merchant return policy structured data","mpn gtin structured data",[27,29,32],{"name":9,"url":28},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSchema.org",{"name":30,"url":31},"JSON-LD","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON-LD",{"name":33,"url":34},"Business-to-business","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBusiness-to-business",[36,39,42,45,48,51,54,57,60,63,66,69],{"title":37,"url":38},"Google Search Central: Introduction to Product structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct",{"title":40,"url":41},"Google Search Central: Merchant listing (Product, Offer) structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fmerchant-listing",{"title":43,"url":44},"Google Search Central: Product snippet structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-snippet",{"title":46,"url":47},"Google Search Central: Product variant structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-variants",{"title":49,"url":50},"Google Search Central: Organization structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Forganization",{"title":52,"url":53},"Google Search Central: Merchant return policy structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Freturn-policy",{"title":55,"url":56},"Google Search Central: General structured data guidelines","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fsd-policies",{"title":58,"url":59},"Google Search Central: Generate structured data with JavaScript","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fgenerate-structured-data-with-javascript",{"title":61,"url":62},"Schema.org: Offer","https:\u002F\u002Fschema.org\u002FOffer",{"title":64,"url":65},"Schema.org: UnitPriceSpecification","https:\u002F\u002Fschema.org\u002FUnitPriceSpecification",{"title":67,"url":68},"Unhead: Product schema (defineProduct)","https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fschema-org\u002Fapi\u002Fschema\u002Fproduct",{"title":70,"url":71},"Nuxt SEO: Schema.org identity setup","https:\u002F\u002Fnuxtseo.com\u002Fdocs\u002Fschema-org\u002Fguides\u002Fsetup-identity","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fog.jpg","b2b-ecommerce-developer",[76,77,78],"en","de","hu","Schema.org structured data for B2B shops: prices behind logins, units and variants","Product markup for B2B shops: snippet or merchant listing, which price to publish, part numbers, unit pricing, variants, Organization and SSR in Nuxt.","A product rectangle with a framed outline and a small price slot hidden behind a low wall.","Schema.org structured data for B2B shops · Balázs Csorba",[84,85,86,87,88],"Google has two product features: merchant listings need a purchasable page with a price above zero, product snippets do not.","Only mark up what the visitor sees; a customer-specific price behind a login does not belong in public JSON-LD.","Part numbers are the B2B advantage: put sku, mpn, gtin and brand in the markup, from the same PIM fields the page shows.","Use UnitPriceSpecification with referenceQuantity for prices per metre, kilogram or pack, and ProductGroup for real variant families.","Render JSON-LD on the server; Google warns that JavaScript-generated Product markup makes Shopping crawls less frequent and less reliable.",[90,93,96,99,102],{"q":91,"a":92},"Can a B2B shop with login-only prices use Product structured data?","Yes, but usually as product snippets rather than merchant listings. Product snippets need a name plus one of review, aggregateRating or offers. Merchant listings need an offer with a price greater than zero that the visitor can see, so a page that shows no price cannot qualify.",{"q":94,"a":95},"Which price should go into the markup?","The price that an anonymous visitor sees on the page, in the currency shown. Google requires the markup to describe visible content. Do not publish negotiated or customer-group prices that only appear after login.",{"q":97,"a":98},"How do I mark up a price per metre or per pack?","Put the active price inside a UnitPriceSpecification and add referenceQuantity with a value and a UN\u002FCEFACT unitCode. Google documents this for unit pricing, which matters in the EU for goods sold by volume, length or weight.",{"q":100,"a":101},"Should I use ProductGroup for every product with options?","Use it when the options are real variants of one product, such as sizes or colours with their own SKU. Each variant needs a unique ID such as sku or gtin, and the group needs productGroupID. Configurable products with thousands of combinations are better described as one product with a starting offer.",{"q":103,"a":104},"Does structured data help AI assistants and answer engines?","Google documents what its own search features use. Other systems may read the same JSON-LD, but there is no public specification of how they weight it. Clean visible content and consistent identifiers are the part you control.",[106,109,112,115,118,121,124,127,130,133,136],{"id":107,"title":108},"why-b2b-is-different","Why B2B product pages need different markup decisions",{"id":110,"title":111},"snippet-or-merchant-listing","Product snippet or merchant listing",{"id":113,"title":114},"which-price","Which price to publish, and how",{"id":116,"title":117},"identifiers","Identifiers: the part B2B shops get right for free",{"id":119,"title":120},"units-and-packs","Units, packs and prices per metre",{"id":122,"title":123},"variants","Variants without exploding the markup",{"id":125,"title":126},"organization","Organization, return and shipping policies",{"id":128,"title":129},"nuxt-ssr","Rendering JSON-LD in Nuxt on the server",{"id":131,"title":132},"data-pipeline","One source of truth from PIM to markup",{"id":134,"title":135},"testing-markup","Testing and monitoring",{"id":137,"title":138},"sources","Sources",[140,144,147,150,153,198,199,236,242,249,250,265,276,277,313,322,323,346,348,351,352,377,385,386,423,441,442,457,459,466,467,470,479,487,488,491,508,521,522],{"type":141,"content":142},"paragraph",[143],"Most structured data guides are written for consumer shops: one public price, a star rating, free shipping. A B2B shop breaks most of those assumptions. Prices depend on the customer and often appear only after login, products are sold per metre or in packs of 50, a single article can have hundreds of variants, and the most valuable search terms are manufacturer part numbers rather than product names.",{"type":141,"content":145},[146],"This post goes through the decisions I make when adding schema.org markup to B2B shops built on Nuxt with a commerce or PIM back end. It sticks to what Google documents for its product features, because that is the part with a public specification, and it shows where B2B data needs a different answer from the consumer default.",{"type":148,"level":149,"id":107,"text":108},"heading",2,{"type":141,"content":151},[152],"Structured data has one rule that drives nearly every B2B decision. Google’s general guidelines say: do not mark up content that is not visible to readers of the page. Markup is a description of the page, not a side channel for data you would rather not show. Everything below follows from asking, for each property, what an anonymous visitor and Googlebot actually see.",{"type":154,"head":155,"rows":162},"table",[156,158,160],[157],"B2B trait",[159],"Consumer default",[161],"B2B answer",[163,170,177,184,191],[164,166,168],[165],"Prices after login",[167],"Offer with public price",[169],"Snippet without price, or a public list price if one is shown",[171,173,175],[172],"Sold per metre, kg or pack",[174],"Price per item",[176],"UnitPriceSpecification with referenceQuantity",[178,180,182],[179],"Searched by part number",[181],"Name and brand",[183],"sku, mpn, gtin, brand from the PIM",[185,187,189],[186],"Hundreds of variants",[188],"Size and colour",[190],"ProductGroup only for real SKU variants",[192,194,196],[193],"Company identity matters",[195],"Logo and social links",[197],"Organization with legal name, VAT ID, return policy",{"type":148,"level":149,"id":110,"text":111},{"type":141,"content":200},[201,202,206,207,210,211,214,215,218,219,210,221,224,225,227,228,231,232,235],"Google splits Product markup into two features. Product snippets are for pages where people cannot buy directly; they need a ",{"tag":203,"children":204},"code",[205],"name"," plus at least one of ",{"tag":203,"children":208},[209],"review",", ",{"tag":203,"children":212},[213],"aggregateRating"," or ",{"tag":203,"children":216},[217],"offers",". Merchant listings are for pages where customers can buy from you; they need ",{"tag":203,"children":220},[205],{"tag":203,"children":222},[223],"image"," and an ",{"tag":203,"children":226},[217]," object with ",{"tag":203,"children":229},[230],"price"," and ",{"tag":203,"children":233},[234],"priceCurrency",", and unlike snippets they require a price greater than zero. Google notes that adding the merchant listing properties usually makes a page eligible for snippets as well.",{"type":141,"content":237},[238,239,241],"For a B2B shop the choice follows from what the public page shows. If anonymous visitors see a list price and can order, or at least see what they would pay, aim for merchant listings. If the price says \"log in to see your price\", you are in snippet territory: mark up name, image, identifiers, brand and description, and leave ",{"tag":203,"children":240},[217]," out rather than inventing a number. B2B buyers rarely leave reviews in a shop, so in practice those pages often carry no rich result at all, and that is fine; the identifiers still help Google match the page to part-number searches.",{"type":243,"variant":244,"title":245,"body":246},"callout","warn","Never put customer prices into public markup",[247],[248],"Server-side rendering with a logged-in session can leak a negotiated price into JSON-LD that a cache then serves to everyone. Generate markup from the anonymous price context only, and exclude logged-in responses from shared caches.",{"type":148,"level":149,"id":113,"text":114},{"type":141,"content":251},[252,253,256,257,260,261,264],"If you publish a price, publish the one on the page, in the currency on the page, with the tax treatment the page shows. Many B2B shops display net prices to businesses; the markup simply repeats that number. Google recognises three kinds of price: the active price, a strikethrough price marked with ",{"tag":203,"children":254},[255],"priceType"," set to ",{"tag":203,"children":258},[259],"https:\u002F\u002Fschema.org\u002FStrikethroughPrice",", and a member price marked with ",{"tag":203,"children":262},[263],"validForMemberTier",". The two markers must not be combined in one price specification, and the active price carries neither.",{"type":141,"content":266},[267,268,271,272,275],"Tiered prices, the \"from 100 pieces\" table on many B2B pages, have no dedicated Google feature. Schema.org offers ",{"tag":203,"children":269},[270],"eligibleQuantity"," on Offer and price specifications to express the quantity interval a price applies to, and ",{"tag":203,"children":273},[274],"eligibleCustomerType"," for the type of customer an offer is valid for. They are valid vocabulary, but Google’s merchant listing documentation does not list them, so I add them only when the PIM has clean data and expect no visible effect. The active price in the markup should be the single-unit price the page shows first.",{"type":148,"level":149,"id":116,"text":117},{"type":141,"content":278},[279,280,283,284,287,288,291,292,295,296,210,299,210,302,305,306,309,310,312],"B2B buyers search for ",{"tag":203,"children":281},[282],"6ES7 214-1AG40-0XB0",", not for \"small PLC\". Your PIM already holds the identifiers that make those searches work, so publish them: ",{"tag":203,"children":285},[286],"sku"," for your own article number, ",{"tag":203,"children":289},[290],"mpn"," for the manufacturer part number, ",{"tag":203,"children":293},[294],"gtin"," (or ",{"tag":203,"children":297},[298],"gtin8",{"tag":203,"children":300},[301],"gtin13",{"tag":203,"children":303},[304],"gtin14",") where the product has one, and ",{"tag":203,"children":307},[308],"brand"," as a nested Brand. Google lists ",{"tag":203,"children":311},[290]," as uniquely identifying the product for a given manufacturer. Show the same values in the visible product details; markup and page must agree.",{"type":141,"content":314},[315,316,321],"Part numbers also drive internal search. If you are working on that side, ",{"tag":317,"to":318,"children":319},"link","\u002Fblog\u002Fsemantic-product-search-b2b",[320],"semantic product search for B2B"," covers how exact identifiers and semantic retrieval fit together, and both depend on the same clean PIM fields.",{"type":148,"level":149,"id":119,"text":120},{"type":141,"content":324},[325,326,329,330,333,334,337,338,341,342,345],"Cable by the metre, screws in boxes of 200, granulate by the kilogram: unit pricing is everyday B2B. Google documents it for merchant listings and notes that it is particularly important in the EU, New Zealand and Australia for products sold by volume, length or weight. When the page shows a unit price, put the active price inside a ",{"tag":203,"children":327},[328],"UnitPriceSpecification"," and use ",{"tag":203,"children":331},[332],"referenceQuantity"," with a ",{"tag":203,"children":335},[336],"QuantitativeValue",", where ",{"tag":203,"children":339},[340],"unitCode"," is a UN\u002FCEFACT common code and an optional ",{"tag":203,"children":343},[344],"valueReference"," gives the base quantity.",{"type":203,"code":347},"{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"Product\",\n  \"name\": \"Control cable 5G1.5, grey, sold by the metre\",\n  \"image\": \"https:\u002F\u002Fshop.example.at\u002Fmedia\u002Fcable-5g15.jpg\",\n  \"sku\": \"KAB-5G15-GR\",\n  \"mpn\": \"1119305\",\n  \"brand\": { \"@type\": \"Brand\", \"name\": \"ExampleCable\" },\n  \"offers\": {\n    \"@type\": \"Offer\",\n    \"url\": \"https:\u002F\u002Fshop.example.at\u002Fp\u002Fkab-5g15-gr\",\n    \"availability\": \"https:\u002F\u002Fschema.org\u002FInStock\",\n    \"itemCondition\": \"https:\u002F\u002Fschema.org\u002FNewCondition\",\n    \"priceSpecification\": {\n      \"@type\": \"UnitPriceSpecification\",\n      \"price\": 145.00,\n      \"priceCurrency\": \"EUR\",\n      \"referenceQuantity\": {\n        \"@type\": \"QuantitativeValue\",\n        \"value\": 100,\n        \"unitCode\": \"MTR\",\n        \"valueReference\": { \"@type\": \"QuantitativeValue\", \"value\": 1, \"unitCode\": \"MTR\" }\n      }\n    }\n  }\n}",{"type":141,"content":349},[350],"That example says: a 100-metre ring costs 145 euros, shown per metre. The page must display the same: price, ring length and the per-metre value. If your shop sells only full rings, say so in the name and description, because a buyer who expects to order 7 metres will bounce.",{"type":148,"level":149,"id":122,"text":123},{"type":141,"content":353},[354,355,358,359,210,362,231,365,368,369,214,371,373,374,376],"For real variant families, sizes or colours with their own SKU, Google supports ",{"tag":203,"children":356},[357],"ProductGroup"," with ",{"tag":203,"children":360},[361],"hasVariant",{"tag":203,"children":363},[364],"variesBy",{"tag":203,"children":366},[367],"productGroupID",". Each variant needs a unique ID such as ",{"tag":203,"children":370},[286],{"tag":203,"children":372},[294],", each group a unique ID, and each variant still needs the normal merchant listing properties. On single-page variant selectors there must be one canonical URL for the group, typically the page with no variant preselected. Google recommends nesting the variants under the ",{"tag":203,"children":375},[357]," as the most compact representation.",{"type":141,"content":378},[379,380,384],"Configurable B2B products are a different case. A valve with 12 options and thousands of valid combinations is not a variant family; it is one product with a configurator. Mark it up as one Product with the starting price the page shows, and let the configurator, as described in ",{"tag":317,"to":381,"children":382},"\u002Fblog\u002Fheadless-product-configurator-b2b",[383],"headless B2B product configurators",", handle the rest. Listing generated combinations as variants creates markup for products nobody can find a page for.",{"type":148,"level":149,"id":125,"text":126},{"type":141,"content":387},[388,389,210,392,210,395,214,398,210,401,404,405,408,409,412,413,210,416,231,419,422],"In B2B the seller matters as much as the product. Google’s Organization markup has no required properties and recommends adding as many relevant ones as you can. For a B2B merchant the useful ones are ",{"tag":203,"children":390},[391],"legalName",{"tag":203,"children":393},[394],"address",{"tag":203,"children":396},[397],"vatID",{"tag":203,"children":399},[400],"taxID",{"tag":203,"children":402},[403],"iso6523Code"," (Google encourages it with prefix 0199 instead of ",{"tag":203,"children":406},[407],"leiCode",", and with prefix 0060 instead of ",{"tag":203,"children":410},[411],"duns","), ",{"tag":203,"children":414},[415],"logo",{"tag":203,"children":417},[418],"contactPoint",{"tag":203,"children":420},[421],"sameAs",". Put it once, site-wide, typically on the home or about page.",{"type":141,"content":424},[425,426,333,429,432,433,436,437,440],"Return and shipping policies can now live at Organization level: ",{"tag":203,"children":427},[428],"hasMerchantReturnPolicy",{"tag":203,"children":430},[431],"MerchantReturnPolicy",", and ",{"tag":203,"children":434},[435],"hasShippingService"," for a shipping policy. Google recommends a global shipping policy under Organization instead of repeating ",{"tag":203,"children":438},[439],"shippingDetails"," on every offer, and keeps the offer-level properties for exceptions. B2B return rules are often stricter than consumer ones, with no statutory withdrawal right for businesses, so publish the policy you actually apply, with the real number of days and fee type.",{"type":148,"level":149,"id":128,"text":129},{"type":141,"content":443},[444,445,448,449,452,453,456],"Google can process structured data that JavaScript adds to the DOM, but its JavaScript guide has a specific warning for Product markup: dynamically generated markup can make Shopping crawls less frequent and less reliable, which hurts fast-changing data like price and availability. In Nuxt that means rendering the JSON-LD during SSR, from the same data the template uses. The Nuxt Schema.org module (built on Unhead) does this with ",{"tag":203,"children":446},[447],"useSchemaOrg"," and helpers such as ",{"tag":203,"children":450},[451],"defineProduct","; the site identity goes into ",{"tag":203,"children":454},[455],"nuxt.config.ts",".",{"type":203,"code":458},"\u003C!-- pages\u002Fp\u002F[slug].vue -->\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\u002F\u002F anonymous price context only: never the logged-in customer's price list\nconst { data: p } = await useFetch(`\u002Fapi\u002Fcatalog\u002F${route.params.slug}`, { query: { priceContext: 'public' } })\n\nuseSchemaOrg([\n  defineProduct({\n    name: p.value.name,\n    image: p.value.images.map(i => i.url),\n    sku: p.value.sku,\n    mpn: p.value.mpn,\n    ...(p.value.gtin ? { gtin: p.value.gtin } : {}),\n    brand: { '@type': 'Brand', name: p.value.brand },\n    \u002F\u002F publish an offer only when the page shows a public price\n    ...(p.value.publicPrice ? {\n      offers: {\n        price: p.value.publicPrice.net,\n        priceCurrency: 'EUR',\n        availability: p.value.inStock ? 'https:\u002F\u002Fschema.org\u002FInStock' : 'https:\u002F\u002Fschema.org\u002FBackOrder',\n      },\n    } : {}),\n  }),\n])\n\u003C\u002Fscript>\n",{"type":141,"content":460},[461,462,456],"The important line is the price context. Fetch catalogue data for markup with the public price list explicitly, and make sure CDN and route caching never store a response rendered for a logged-in customer. Unhead does not check Google’s eligibility rules at runtime, so validation still belongs in your pipeline. If your shop has several languages, each locale should mark up its own URL and currency display, consistent with your ",{"tag":317,"to":463,"children":464},"\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang",[465],"hreflang and canonical setup",{"type":148,"level":149,"id":131,"text":132},{"type":141,"content":468},[469],"Markup errors in B2B shops are rarely schema errors. They are data errors: a GTIN field that holds an internal number, a price in the markup that comes from a different list than the one on the page, availability from yesterday’s ERP export. The fix is architectural: generate the visible page and the JSON-LD from the same API response, and fix data at the source rather than in a template.",{"type":471,"attrs":472,"inner":476,"caption":477},"diagram",{"viewBox":473,"role":474,"aria-labelledby":475},"0 0 720 180","img","sob2b-flow-t sob2b-flow-d","\u003Ctitle id=\"sob2b-flow-t\">From PIM and ERP to structured data\u003C\u002Ftitle>\u003Cdesc id=\"sob2b-flow-d\">PIM and ERP feed a catalogue API; the Nuxt page renders HTML and JSON-LD from one response; a CI check compares both.\u003C\u002Fdesc>\u003Cdefs>\u003Cmarker id=\"sob2b-flow-ah\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\u003Cpath d=\"M0 0L10 5L0 10z\" class=\"d-head\" \u002F>\u003C\u002Fmarker>\u003C\u002Fdefs>\u003Crect x=\"20\" y=\"52\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"79\" y=\"86\" text-anchor=\"middle\" class=\"d-text\">PIM + ERP\u003C\u002Ftext>\u003Ctext x=\"79\" y=\"108\" text-anchor=\"middle\" class=\"d-small\">ids, units, stock\u003C\u002Ftext>\u003Cpath d=\"M138 90H158\" class=\"d-line\" marker-end=\"url(#sob2b-flow-ah)\" \u002F>\u003Crect x=\"160\" y=\"52\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"220\" y=\"86\" text-anchor=\"middle\" class=\"d-text\">Catalog API\u003C\u002Ftext>\u003Ctext x=\"220\" y=\"108\" text-anchor=\"middle\" class=\"d-small\">public prices\u003C\u002Ftext>\u003Cpath d=\"M279 90H299\" class=\"d-line\" marker-end=\"url(#sob2b-flow-ah)\" \u002F>\u003Crect x=\"301\" y=\"52\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-fill-accent\" \u002F>\u003Ctext x=\"360\" y=\"86\" text-anchor=\"middle\" class=\"d-text\">Nuxt SSR\u003C\u002Ftext>\u003Ctext x=\"360\" y=\"108\" text-anchor=\"middle\" class=\"d-small\">HTML + JSON-LD\u003C\u002Ftext>\u003Cpath d=\"M419 90H439\" class=\"d-line\" marker-end=\"url(#sob2b-flow-ah)\" \u002F>\u003Crect x=\"441\" y=\"52\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"500\" y=\"86\" text-anchor=\"middle\" class=\"d-text\">CI check\u003C\u002Ftext>\u003Ctext x=\"500\" y=\"108\" text-anchor=\"middle\" class=\"d-small\">markup = page\u003C\u002Ftext>\u003Cpath d=\"M560 90H580\" class=\"d-line\" marker-end=\"url(#sob2b-flow-ah)\" \u002F>\u003Crect x=\"582\" y=\"52\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"641\" y=\"86\" text-anchor=\"middle\" class=\"d-text\">Search Console\u003C\u002Ftext>\u003Ctext x=\"641\" y=\"108\" text-anchor=\"middle\" class=\"d-small\">reports\u003C\u002Ftext>",[478],"Generate markup and visible content from the same response, then verify that they agree before and after deploy.",{"type":141,"content":480},[481,482,486],"This is the same discipline as any PIM-to-shop integration; if your product data arrives via delta syncs from SAP or Infor, the notes in ",{"tag":317,"to":483,"children":484},"\u002Fblog\u002Fpimcore-erp-delta-sync",[485],"Pimcore ERP delta sync"," apply directly, because a stale price in markup is just a stale price.",{"type":148,"level":149,"id":134,"text":135},{"type":141,"content":489},[490],"Google’s own workflow is the Rich Results Test for individual URLs, then the URL Inspection tool to see how Google renders a deployed page, then the Search Console reports. For a shop with tens of thousands of products I add one automated layer that the tools do not provide: a CI job that renders a sample of product pages and compares the JSON-LD with the visible DOM.",{"type":492,"items":493},"list",[494,496,498,504,506],[495],"The markup price equals the visible price, and the currency matches.",[497],"No offer is present on pages that show \"log in for price\".",[499,231,501,503],{"tag":203,"children":500},[286],{"tag":203,"children":502},[290]," appear in the visible product details.",[505],"GTIN values have a valid check digit; internal numbers in GTIN fields are a common PIM error.",[507],"A page rendered with a logged-in cookie is never served from the shared cache.",{"type":141,"content":509},[510,511,515,516,520],"Structured data will not rescue thin product pages, and it will not make a login-only catalogue rank like a consumer shop. What it does is describe your products in a format that search engines and other machines can parse without guessing. For the wider question of how AI systems read your pages, see my ",{"tag":317,"to":512,"children":513},"\u002Fblog\u002Fgenerative-engine-optimization-audit",[514],"GEO audit"," and the overview of ",{"tag":317,"to":517,"children":518},"\u002Fblog\u002Fagentic-commerce-protocols-ucp-acp-guide",[519],"agentic commerce protocols",", which build on the same clean product data.",{"type":148,"level":149,"id":137,"text":138},{"type":492,"ordered":523,"items":524},true,[525,529,532,535,538,541,544,547,550,553,556,559],[526],{"tag":527,"href":38,"children":528},"a",[37],[530],{"tag":527,"href":41,"children":531},[40],[533],{"tag":527,"href":44,"children":534},[43],[536],{"tag":527,"href":47,"children":537},[46],[539],{"tag":527,"href":50,"children":540},[49],[542],{"tag":527,"href":53,"children":543},[52],[545],{"tag":527,"href":56,"children":546},[55],[548],{"tag":527,"href":59,"children":549},[58],[551],{"tag":527,"href":62,"children":552},[61],[554],{"tag":527,"href":65,"children":555},[64],[557],{"tag":527,"href":68,"children":558},[67],[560],{"tag":527,"href":71,"children":561},[70],[563,619,671,739],{"slug":564,"published":5,"minutes":565,"category":7,"tags":566,"keywords":571,"about":583,"sources":591,"cover":613,"og":614,"expertise":74,"locales":615,"lang":76,"title":616,"description":617,"coverAlt":618},"ai-assistant-b2b-shop-customer-support",9,[11,567,568,569,570],"AI assistant","Customer support","LLM agents","EU AI Act",[572,573,574,575,576,577,578,579,580,581,582],"ai assistant b2b shop","ai customer support b2b ecommerce","llm chatbot order status","ai agent customer service tools","chatbot liability air canada","klarna ai assistant results","human handoff chatbot design","eu ai act chatbot disclosure","owasp excessive agency","ai support resolution rate","b2b customer portal chatbot",[584,587,590],{"name":585,"url":586},"Customer service","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_service",{"name":588,"url":589},"Chatbot","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChatbot",{"name":33,"url":34},[592,595,598,601,604,607,610],{"title":593,"url":594},"Klarna: AI assistant handles two-thirds of customer service chats in its first month (press release, 2024)","https:\u002F\u002Fwww.klarna.com\u002Finternational\u002Fpress\u002Fklarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month\u002F",{"title":596,"url":597},"Entrepreneur: Klarna is hiring customer service agents again, according to its CEO (May 2025)","https:\u002F\u002Fwww.entrepreneur.com\u002Fbusiness-news\u002Fklarna-ceo-reverses-course-by-hiring-more-humans-not-ai\u002F491396",{"title":599,"url":600},"ABA Business Law Today: BC tribunal confirms companies remain liable for information provided by AI chatbot","https:\u002F\u002Fwww.americanbar.org\u002Fgroups\u002Fbusiness_law\u002Fresources\u002Fbusiness-law-today\u002F2024-february\u002Fbc-tribunal-confirms-companies-remain-liable-information-provided-ai-chatbot\u002F",{"title":602,"url":603},"OWASP GenAI Security Project: LLM06:2025 Excessive Agency","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm062025-excessive-agency\u002F",{"title":605,"url":606},"OWASP GenAI Security Project: LLM05:2025 Improper Output Handling","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm052025-improper-output-handling\u002F",{"title":608,"url":609},"EU AI Act, Article 50: Transparency obligations","https:\u002F\u002Fartificialintelligenceact.eu\u002Farticle\u002F50\u002F",{"title":611,"url":612},"Intercom: Fin pricing and billable outcomes","https:\u002F\u002Ffin.ai\u002Fpricing","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fog.jpg",[76,77,78],"An AI assistant for B2B shop support: tools, permissions and the human handoff","How to build an AI support assistant for a B2B shop: which requests to automate, tools that run in the customer context, handoff, AI Act and metrics.","A tall wall with one narrow gap; an arrow passes through it into a boxed enclosure of three squares, and another leads down to a tray.",{"slug":620,"published":621,"minutes":622,"category":7,"tags":623,"keywords":628,"about":639,"sources":648,"cover":664,"og":665,"expertise":666,"locales":667,"lang":76,"title":668,"description":669,"coverAlt":670},"nuxt-i18n-multilingual-seo-hreflang","2026-10-16",8,[13,624,625,626,627],"i18n","SEO","hreflang","Multilingual",[629,630,631,632,633,634,635,636,637,638],"nuxt i18n seo","nuxt hreflang","useLocaleHead","nuxt i18n canonical","hreflang x-default","prefix_except_default","multilingual seo nuxt","nuxt i18n sitemap hreflang","hreflang self-referencing","nuxt i18n baseUrl",[640,642,645],{"name":13,"url":641},"https:\u002F\u002Fnuxt.com\u002F",{"name":643,"url":644},"Hreflang","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHreflang",{"name":646,"url":647},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",[649,652,655,658,661],{"title":650,"url":651},"Nuxt i18n docs: SEO (useLocaleHead)","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide\u002Fseo",{"title":653,"url":654},"Nuxt i18n docs: Routing strategies","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide",{"title":656,"url":657},"Google Search Central: Tell Google about localized versions of your page","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Flocalized-versions",{"title":659,"url":660},"Google Search Central: Managing multi-regional and multilingual sites","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Fmanaging-multi-regional-sites",{"title":662,"url":663},"Google Search Central: How to specify a canonical URL","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fconsolidate-duplicate-urls","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fog.jpg","vue-nuxt-developer",[76,77,78],"Multilingual SEO with Nuxt i18n: URL strategy, hreflang, canonicals and sitemaps","How to set up Nuxt i18n so search engines index every language: routing strategy, useLocaleHead, hreflang rules, partial translations and sitemaps.","A large sheet linked by three horizontal connectors to a column of three smaller sheets.",{"slug":672,"published":621,"minutes":673,"category":7,"tags":674,"keywords":679,"about":692,"sources":699,"cover":733,"og":734,"expertise":666,"locales":735,"lang":76,"title":736,"description":737,"coverAlt":738},"llm-endpoint-abuse-denial-of-wallet",14,[13,675,676,677,678],"LLM security","Rate limiting","OWASP","Cost control",[680,681,682,683,684,685,686,687,688,689,690,691],"denial of wallet llm","owasp llm10 unbounded consumption","rate limit llm api nuxt","nuxt security rate limiter","protect openai api key frontend","llm token budget per user","limit llm cost per user","nitro api rate limiting","cloudflare turnstile nuxt","anthropic spend limit","openai hard spend limit","llm prompt size limit",[693,695,698],{"name":676,"url":694},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRate_limiting",{"name":696,"url":697},"Denial-of-service attack","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDenial-of-service_attack",{"name":13,"url":641},[700,703,706,709,712,715,718,721,724,727,730],{"title":701,"url":702},"OWASP GenAI Security Project: LLM10:2025 Unbounded Consumption","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm102025-unbounded-consumption\u002F",{"title":704,"url":705},"Nuxt Security: Rate Limiter middleware","https:\u002F\u002Fnuxt-security.vercel.app\u002Fmiddleware\u002Frate-limiter",{"title":707,"url":708},"Nuxt Security: Request Size Limiter middleware","https:\u002F\u002Fnuxt-security.vercel.app\u002Fmiddleware\u002Frequest-size-limiter",{"title":710,"url":711},"nuxt-auth-utils: README (GitHub)","https:\u002F\u002Fgithub.com\u002Fatinux\u002Fnuxt-auth-utils",{"title":713,"url":714},"h3: request utilities (getRequestIP, readValidatedBody)","https:\u002F\u002Fh3.dev\u002Futils\u002Frequest",{"title":716,"url":717},"Upstash: Ratelimiting algorithms","https:\u002F\u002Fupstash.com\u002Fdocs\u002Fredis\u002Fsdks\u002Fratelimit-ts\u002Falgorithms",{"title":719,"url":720},"Cloudflare Turnstile: Validate the token","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fturnstile\u002Fget-started\u002Fserver-side-validation\u002F",{"title":722,"url":723},"Claude Platform Docs: Rate limits and spend limits","https:\u002F\u002Fplatform.claude.com\u002Fdocs\u002Fen\u002Fapi\u002Frate-limits",{"title":725,"url":726},"Claude Platform Docs: Token counting","https:\u002F\u002Fplatform.claude.com\u002Fdocs\u002Fen\u002Fbuild-with-claude\u002Ftoken-counting",{"title":728,"url":729},"OpenAI API Docs: Spend limits","https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fguides\u002Fspend-limits",{"title":731,"url":732},"OpenAI API Docs: Rate limits","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Frate-limits","\u002Fimages\u002Fblog\u002Fllm-endpoint-abuse-denial-of-wallet\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-endpoint-abuse-denial-of-wallet\u002Fog.jpg",[76,77,78],"Denial of wallet: protecting public LLM endpoints in a Nuxt app","How to stop abuse and runaway cost on a public LLM route in Nuxt: auth, per-user rate limits, prompt caps, token budgets, Turnstile and spend limits.","Tiny dots crowding at a narrow slot in a wall, two through, a rail capping the far side.",{"slug":740,"published":741,"minutes":565,"category":7,"tags":742,"keywords":746,"about":754,"sources":761,"cover":783,"og":784,"expertise":785,"locales":786,"lang":76,"title":787,"description":788,"coverAlt":789},"llms-txt-vs-markdown-content-negotiation","2026-10-07",[743,744,745,13],"llms.txt","Content negotiation","AI agents",[743,747,748,749,750,751,752,753],"markdown for agents","content negotiation","AI crawlers","generative engine optimization","Accept text\u002Fmarkdown header","llms-full.txt","agent-ready website",[755,758],{"name":756,"url":757},"Markdown","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMarkdown",{"name":759,"url":760},"Hypertext Transfer Protocol","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHypertext_Transfer_Protocol",[762,765,768,771,774,777,780],{"title":763,"url":764},"llmstxt.org: The \u002Fllms.txt file (proposal, version 2)","https:\u002F\u002Fllmstxt.org\u002F",{"title":766,"url":767},"llmstxt.org: Changes from v1 to v2","https:\u002F\u002Fllmstxt.org\u002Fchanges.html",{"title":769,"url":770},"Cloudflare changelog: Markdown for Agents (12 February 2026)","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fchangelog\u002Fpost\u002F2026-02-12-markdown-for-agents\u002F",{"title":772,"url":773},"Cloudflare docs: Markdown for Agents","https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fmarkdown-for-agents\u002F",{"title":775,"url":776},"Ahrefs: 137K sites analyzed, 97% of llms.txt files never get read (June 2026)","https:\u002F\u002Fahrefs.com\u002Fblog\u002Fllmstxt-study\u002F",{"title":778,"url":779},"Checkly: The current state of content negotiation for AI agents (February 2026)","https:\u002F\u002Fwww.checklyhq.com\u002Fblog\u002Fstate-of-ai-agent-content-negotation\u002F",{"title":781,"url":782},"Suganthan: tracking Cloudflare Markdown for Agents on one site","https:\u002F\u002Fsuganthan.com\u002Fblog\u002Fcloudflare-markdown-for-agents\u002F","\u002Fimages\u002Fblog\u002Fllms-txt-vs-markdown-content-negotiation\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllms-txt-vs-markdown-content-negotiation\u002Fog.jpg","ai-engineer",[76,77,78],"llms.txt vs Markdown content negotiation: what agents actually fetch","llms.txt is a proposal, Markdown content negotiation is a header. What AI agents fetch, what the logs show, and how to serve both from Nuxt and nginx.","Two doors with a square and a circle handle, an incoming square turning toward the right one.",1792222776118]