SEO and tracking
Blanxer builds the product feed and the catalog sitemap. The site passes the sitemap through, renders metadata and JSON-LD from Blanxer's SEO fields, and loads the tracking tags the merchant set in Blanxer. SEO text is changed in Blanxer through the MCP, never hardcoded in the site.
Files Blanxer builds
| File | URL | Cache |
|---|---|---|
| Catalog sitemap | https://api.blanxer.com/public/sitemap/<store_id>.xml |
Data rebuilt at most daily, and after product, category, page or store-settings changes. Brand and blog changes don't trigger a rebuild, so they can take up to 24 hours |
Product feed (RSS 2.0 + g: namespace) |
https://api.blanxer.com/public/product-feed/<store_id>.xml |
Data rebuilt at most hourly |
Both send Cache-Control: public, max-age=3600, stale-while-revalidate=86400. An unknown store id is HTTP 404.
Every link in them is absolute, on the store's site origin:
- the saved site link (
storefront_url), if set; - else the verified Blanxer custom domain;
- else
https://<sub>.blanxer.io.
So save the site link in Blanxer before you submit either file (blanxer_set_storefront_url, see Deploy). Before that, they point at the Blanxer-hosted site. The API uses a changed link on the next request, but HTTP caches (max-age=3600 plus stale-while-revalidate=86400) and the site's own /sitemap-catalog.xml (cached 3600 s) can serve the old links for an hour or more.
What the catalog sitemap lists
/,/products,/collections/new-arrivals/collections/<slug>for every category/product/<slug>for every Active, non-POS product/brand/<slug>for every brand/blogs,/blog/<slug>, and/blogs?category=<slug>for every category incheck_domain.blog_categories(even one with no posts), when the store has published, indexable posts- page-builder pages
/p/<slug>only when no site link is saved; a custom site has none
This is why the paths are fixed (rule 10). It does not list /brands, /sale, /search, /forms/<slug> or any page you add. Those go in the site's own sitemap.
What the product feed contains
Only Active, non-POS products. One <item> per product, or per variant for products with variants:
g:id: the product_id, or<product_id>_<variant_id>for a variant, withg:item_group_id= the product_id;g:link:<origin>/product/<slug>;g:title,g:description,g:image_link(+ up to 10g:additional_image_link),g:condition,g:availability(in stock/out of stock);g:brand: the product's brand, else the store's brand name or name;g:product_type: only when the product has categories;g:priceandg:sale_price: whencompare_at_priceis above the selling price,g:priceiscompare_at_price(the regular price) andg:sale_priceis the selling price; otherwiseg:priceis the selling price and there is nog:sale_price;g:quantity_to_sell_on_facebook.
Items without an image or a price above 0 are left out (Meta rejects them). The g:id values are the same ids the site must send as Meta content_ids (see Meta Pixel).
Don't build a feed or a catalog sitemap in the site (rule 10).
The site's sitemap and robots
| Route | Content |
|---|---|
/sitemap.xml |
A sitemap index listing /sitemap-catalog.xml and any sitemaps of the site's own pages |
/sitemap-catalog.xml |
Blanxer's catalog sitemap, passed through unchanged |
/sitemap-pages.xml (optional, you add it) |
The site's own extra pages: /brands, /sale, /forms/<slug>, about, contact |
/robots.txt |
Allow all, keep private pages out, point to /sitemap.xml |
Google wants a sitemap on the same host as the pages it lists. That's why the site serves the catalog sitemap on its own host instead of linking to api.blanxer.com.
// app/sitemap-catalog.xml/route.ts
import { catalogSitemapRoute } from '@blanxer-dev/storefront/next';
export const GET = catalogSitemapRoute(); // fetches /public/sitemap/<store_id>.xml// app/sitemap.xml/route.ts (don't hand-write the index)
import { sitemapIndexRoute } from '@blanxer-dev/storefront/next';
export const GET = sitemapIndexRoute({ extra: ['/sitemap-pages.xml'] }); // leave out `extra` until you add that file// app/robots.txt/route.ts
import { robotsRoute } from '@blanxer-dev/storefront/next';
export const GET = robotsRoute();robotsRoute() allows everything, disallows /checkout, /cart, /payment/, /track/ and /cdn-cgi/ (add more with { disallow: [...] }), and ends with Sitemap: <origin>/sitemap.xml. (The Blanxer-hosted storefront disallows /checkout and /cdn-cgi/.)
The origin comes from siteOrigin(): SITE_URL, else the saved storefront_url, else the request host.
Page metadata
Use Next.js generateMetadata. The package's storeMetadata, productMetadata, categoryMetadata, brandMetadata and blogPostMetadata build these from the API data, so the merchant's SEO work in Blanxer shows up on the site. Titles get the template <title> | <store name> from the home page's metadata.
| Page | <title> |
Description | Share image (og:image) |
|---|---|---|---|
| Home | The landing page's seo_title (getHomeSeo()), else brand_name (or name) |
Landing seo_description, else description, else top_bar_text |
Landing seo_image, else hero_images[0], else the logo |
| Product | seo_title, else the name |
seo_description, else description, else the name |
seo_image, else image_urls[0] |
| Category | seo_title, else the name |
seo_description, else the store description, else "Shop |
seo_image, else the category image, else hero_images[0] |
| Brand | The brand page's seo_title, else the brand name |
The brand page's seo_description, else "Shop |
The brand page's seo_image, else the brand logo |
| Blog post | seo_title |
seo_description |
seo_image (all three already defaulted by the API) |
Where the Blanxer-hosted storefront differs: its product <title> is the name (seo_title only for og:title), its category description falls back to top_bar_text, and its brand title has no brand-name fallback (the package's fallback is an improvement).
- Home SEO comes from the store's landing page (
GET /public/landing/<store_id>, read bygetHomeSeo()). Only stores on a page-builder plan have one; otherwise the store name and description are used. - The brand page's SEO fields come from
GET /brand/public/products/<store_id>/<slug>asbrand.page.seo_title,seo_description,seo_image. - Every page:
og:site_name= the store name,og:type(website,product,article),twitter:card=summary_large_image, and a canonical URL<origin><path>without query strings (except/blogs?category=). noindexon/cart,/checkout,/payment/success,/payment/failed,/track/[id],/search, and blog posts withno_index.- On stores without a landing page, confirm the home title and description (the store name and description) with the merchant.
To improve the text, use the MCP: blanxer_audit_seo, then blanxer_update_product_status_seo, blanxer_bulk_update_products, blanxer_bulk_update_categories_seo, blanxer_update_brand. Confirm bulk changes first (rule 13). See MCP for your website.
JSON-LD
Add structured data with the package's productJsonLd, organizationJsonLd and breadcrumbJsonLd, in a <script type="application/ld+json">.
Product (product pages), from real data only:
@type: Product,@id: <url>#product,name,url,description(seo_descriptionor the name),image(allimage_urls);offers:@type: Offer,price(first variant's price, else the product's),priceCurrency: NPR,availability(InStockwhencontinue_selling, or any stock),itemCondition: NewCondition,seller= the store;skuandgtin(barcode) when present,brandwhen the product has one,categoryfrom the first matching category name;aggregateRatingonly whenreview_count > 0:ratingValue=total_rating / review_count(the average, 1–5),reviewCount. (The Blanxer storefront puts the raw sum inratingValue; don't copy that.)reviewentries only for real reviews.
Organization (home): name, url, logo, and sameAs with the social links that are set.
BreadcrumbList (product, category, brand): Home → Category or Brand → Product.
Never add placeholder ratings, prices or reviews (rule 8).
Search Console verification
- The merchant adds the site in Google Search Console as a URL-prefix property (
https://mystore.com/) and picks HTML tag. They copy the tag or just itscontentvalue. - You call
blanxer_set_site_verification { google_verification_code: "<the tag or the content value>" }through the MCP. Blanxer stores it in themeta_tagsplugin as{n: "google-site-verification", c: "<code>"}. - The site renders every
meta_tagsentry as<meta name={n} content={c}>on the home page.verificationMetadata(store)from the package returns it as Next.jsmetadata.verification/metadata.other. - Wait about a minute for the caches, check the tag is in the page source, then click Verify.
- Submit
https://<site>/sitemap.xmlunder Sitemaps.
A Domain property (verified with a DNS TXT record) works too: the merchant adds the TXT record in their own Cloudflare DNS. Then no meta tag is needed.
Google Analytics, Tag Manager and Clarity
The ids come from check_domain.plugins, an array of {name, details}:
ga: a GA4 id (G-...);gtm: a Tag Manager id (GTM-...);clarity: a Microsoft Clarity id.
Set or change GA/GTM with blanxer_set_site_verification { ga_id } or { gtm_id }. The package's <Pixel> loads GA4, GTM, Clarity and the Meta Pixel from these plugins, each only when its id is present and after the page is interactive. Never hardcode an id in the site.
Meta Pixel and Conversions API
- Pixel: when
pluginshasfb_pixel, load Meta'sfbevents.jswith that id and trackPageViewon every route change. - Conversions API relay: when
pluginshasmeta_capi(itsdetailsis justtrue; the access token stays on Blanxer's server), every event is also sent to Blanxer, which forwards it to Meta. The relay goes through Blanxer's domain, so ad blockers don't drop it.
The package's Pixel component and trackEvent do both.
Events
| Event | When | custom_data |
|---|---|---|
ViewContent |
Product page opens | content_ids: [product _id], content_name, content_type: 'product', value, currency: 'NPR' |
AddToCart |
Add to cart | content_ids: ['<product>_<variant>' or '<product>'], content_name, content_type, value, currency |
InitiateCheckout |
Checkout opens with lines | content_ids, content_type, value, currency, num_items |
Purchase |
Order placed (COD) / paid (online) | content_ids, content_type, value, currency, num_items |
content_ids must match the feed's g:id: <product_id>_<variant_id> for a variant line, the product _id otherwise. Otherwise Meta's catalog ads can't match events to products.
The relay call
POST /public/meta-event/<store_id>
Content-Type: application/json
{ "event_name": "AddToCart", "event_id": "<uuid, the same one passed to fbq as eventID>",
"event_source_url": "https://mystore.com/product/pashmina-shawl",
"fbp": "<_fbp cookie>", "fbc": "<_fbc cookie>",
"custom_data": { "content_ids": ["..._..."], "content_type": "product", "value": 2500, "currency": "NPR" } }event_nameis one ofViewContent,AddToCart,InitiateCheckout,Purchase. Addemailandphoneonly forPurchase.custom_datakeeps onlyvalue,currency,content_type,content_ids(≤ 200),contentsandnum_items. Other keys, such ascontent_name, are dropped by the relay (the browser Pixel still gets them).- A valid event always gets
{"success": true}: also when the store has nometa_capi, and when forwarding to Meta fails. A malformed body is HTTP 400 (validation error). - Limit: 100 per minute per IP.
- Send it from the browser with
fetch(..., { keepalive: true })and don't wait for it. Blanxer forwards the caller's IP and user agent to Meta, so a server-side call would report the Worker instead of the shopper.
Purchase without double counting
- The checkout creates one
event_idper order and sends it in the order'smetablock withfbp,fbcandevent_source_url. - COD: Blanxer's server sends the CAPI
Purchasewhen the order is created. The browser fires only the PixelPurchase, with the sameevent_id. - Online payments:
/payment/successfires the Pixel and the relay with thatevent_id. - Meta dedupes on
event_id.
Register the product feed
After the site link is saved:
- Meta Commerce Manager: Catalog → Data sources → Data feed → scheduled feed from URL
https://api.blanxer.com/public/product-feed/<store_id>.xml, hourly or daily. - Google Merchant Center: Products → Feeds → scheduled fetch of the same URL.
Meta's and Google's crawlers fetch the feed from api.blanxer.com directly. That hasn't been checked against Cloudflare's bot rules yet, so after adding it, check the feed fetched without errors (Commerce Manager / Merchant Center shows the fetch status).
blanxer_get_store_context returns the feed and sitemap URLs (product_feed_url, sitemap_url) only once a site link is saved. They arrive with the BlanxerAPI release of the MCP.
Agents read this page at /api-docs/seo-and-tracking.md · Edit on GitHub