Skip to main content

JSON-LD escaping

Embedding JSON inside a <script> tag has a classic injection hazard: if any data field contains </script>, the browser terminates the script block there and executes whatever follows as HTML. Product titles, vendor names, and FAQ answers are all merchant- or user-influenced data. The Next.js adapters (App Router and Pages Router) serialize through serializeJsonLd, which escapes every < to its JSON unicode form (\u003c). The output is still valid JSON — parsers read the escape back as < — but it can never close the script tag. This was verified with a hostile payload through a real app: a product name containing
renders inertly inside the JSON-LD block; no script breakout, and the structured data remains valid. Remix and Hydrogen are different. Their "script:ld+json" meta descriptor is rendered by the framework with a plain JSON.stringify into dangerouslySetInnerHTML — no < escaping, and escaping can’t be injected through that descriptor (any < the SDK produced would be re-escaped back). Versions of @agentshop/seo before 0.3.2 passed the JSON-LD straight through, so a </script> in merchant data broke out of the script tag on those frameworks — upgrade to 0.3.2 or later. Since 0.3.2 the descriptor is omitted whenever the JSON-LD would contain any < character (the check is deliberately broader than </script>: the HTML tokenizer’s escape states mean payloads like <!--<script> also break out without containing </), and a warning is logged once per page naming the affected URL. The practical cost: a bare < in a product title or description (e.g. Widget < 5kg) drops structured data on Remix/Hydrogen until it’s removed or written as &lt;; the Next adapters render such values normally. If you write your own script tags (custom integrations using the core fetch helpers), never JSON.stringify straight into the tag:
This applies to anyone consuming the REST API directly, too — always pass bundle.jsonLd through the exported serializeJsonLd, never JSON.stringify.

The API key model

The SDK authenticates with your store API key (ask_…), sent as Authorization: Bearer. Unlike a public tag, this key is a secret — treat it like a password:
  • Server-side only. Use it in environment variables read on the server: generateMetadata, async server components, Remix/Hydrogen loaders, and route handlers all run on the server, so the shipped SDK never exposes it. Never put it in a NEXT_PUBLIC_* (or any client-exposed) variable, a client component, or a committed file.
  • Never in the browser or a client bundle. Anyone with the key can read your store’s SEO bundles. If it leaks (a public repo, a client bundle), rotate it immediately.
  • One key per store. A key resolves to exactly one connected store; a merchant with several stores has one key each.
This is also why the endpoints send Cache-Control: private — and the pixel-config endpoint goes further with no-store, since its response body is a credential — a per-store secret can’t be stored in a shared or CDN cache.

Rotating a key

Rotate a key any time in the dashboard under Settings → API Keys (this is also where you generate it the first time). After a rotation the previous key keeps working for 24 hours, so you can roll the new key out to your deployments without downtime. Update your AGENTSHOP_API_KEY env var and redeploy within that window.

Requests we make to your storefront

Once your store is connected, AgentShop makes one automated request to your storefront per day.
string
AgentShopIntegrationProbe/1.0 (+https://useagentshop.com)
It performs a single GET on your storefront’s home page and checks whether the analytics script is still installed and current. Nothing else is requested, nothing is submitted, and the page content is not stored — only whether the check passed. This exists because a headless storefront is your codebase, not ours. If the installed pixel stops matching your store, we have no way to repair it from our side; the daily check is what turns that into an email to you instead of attribution that quietly stops. Identifying it. The user agent above is stable and self-identifying, so you can allowlist it in bot protection or exclude it from your own analytics. If you use AI crawler visibility, note that the edge bot check treats our probe like any other bot, so it will appear in your crawler data — filter on the user agent to exclude it. Opting out. Blocking the user agent in your WAF or robots.txt stops the check. You’ll keep collecting analytics normally; you simply won’t be told if the installation breaks.