@agentshop/seo
Server-rendered SEO for Shopify headless storefronts — schema.org JSON-LD, meta title/description,
canonical, Open Graph/Twitter, and a product sitemap — fetched from your AgentShop SEO endpoint and
injected into the initial HTML so AI crawlers (GPTBot, ClaudeBot, PerplexityBot) and search
engines can read it.
One import per framework; the framework-specific gotchas are handled for you.
Install
npm i @agentshop/seo
AGENTSHOP_API_KEY=<your store's API key from Dashboard → Settings → API Keys>
AGENTSHOP_SEO_URL=https://api.useagentshop.com/api/v1/headless-seo
Your API key (ask_…) is generated per store in the AgentShop dashboard under
Settings → API Keys. It identifies exactly one store, so the endpoints know what to
serve without you passing a store id anywhere.
The key is a secret. Keep it server-side only — never in a client bundle or a
NEXT_PUBLIC_* var. Every helper below runs on the server, so the key never reaches the
browser. Rotating a key in Settings keeps the old one working for 24 hours, so you can
rotate and redeploy without a gap in coverage.
Next.js — App Router
import { generateProductMetadata, ProductJsonLd } from "@agentshop/seo/next";
export const generateMetadata = generateProductMetadata;
export default async function Page({ params }: { params: Promise<{ handle: string }> }) {
const { handle } = await params;
return (
<>
<ProductJsonLd handle={handle} />
{/* ...your product page... */}
</>
);
}
import { productSitemapRoute } from "@agentshop/seo/next";
export const { GET } = productSitemapRoute();
Next.js — Pages Router
import { getProductSeoProps, ProductHead } from "@agentshop/seo/next/pages";
export const getServerSideProps = getProductSeoProps;
export default function Product({ seo }: { seo: any }) {
return (
<>
<ProductHead seo={seo} />
{/* ...your product page... */}
</>
);
}
import { productSitemapHandler } from "@agentshop/seo/next/pages";
export default productSitemapHandler();
Remix
import { json } from "@remix-run/node";
import { loadProductSeo, agentshopProductMeta } from "@agentshop/seo/remix";
export async function loader({ params }) {
return json(await loadProductSeo(params.handle));
}
export const meta = agentshopProductMeta;
Hydrogen
import { json } from "@shopify/remix-oxygen";
import { loadProductSeo, agentshopProductMeta } from "@agentshop/seo/hydrogen";
export async function loader({ context, params }) {
return json(await loadProductSeo(context, params.handle));
}
export const meta = agentshopProductMeta;
Configuration
Every helper takes an optional config that overrides the env defaults. For the
App Router generateMetadata, use the factory so config isn't confused with Next's
(props, parent) call signature:
export const generateMetadata = createProductMetadata({ timeoutMs: 5000 });
apiKey — defaults to process.env.AGENTSHOP_API_KEY (Hydrogen: context.env.AGENTSHOP_API_KEY).
baseUrl — defaults to process.env.AGENTSHOP_SEO_URL (Hydrogen: context.env.AGENTSHOP_SEO_URL).
timeoutMs — request timeout, default 3000. Fetches never throw — on any failure
(missing config, bad key, timeout, network) they return null and render no tags.
Because failures degrade to "no tags" rather than an error, a bad credential looks the same
as no credential. If your tags don't appear, check in this order:
AGENTSHOP_SEO_URL unset — the only case that logs a warning (Missing base URL, once).
AGENTSHOP_API_KEY unset, wrong, or rotated out — the endpoint 401s and the helper
returns null silently. Confirm the key in Settings → API Keys matches your env.
- Timeout —
timeoutMs defaults to 3000; a cold backend can exceed it. Raise it and retry.
Compatibility
| React | 18, 19 |
| Next.js | 14, 15, 16 (async params handled) |
| Remix | v2 |
| Hydrogen | Oxygen (context.env) |
| Runtime | Node ≥18, edge/workerd |
Verify it works
Load a product page, then View Source (not the devtools DOM) and confirm the
<script type="application/ld+json">, <title>, <link rel="canonical">, and og:* tags are in
the raw HTML. Paste the JSON-LD into https://validator.schema.org/.
Notes