New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@agentshop/seo

Package Overview
Dependencies
Maintainers
2
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@agentshop/seo

Server-rendered SEO for Shopify headless storefronts — JSON-LD, meta, Open Graph, and sitemaps for Next.js, Remix, and Hydrogen.

npmnpm
Version
0.3.2
Version published
Weekly downloads
182
208.47%
Maintainers
2
Weekly downloads
 
Created
Source

@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
# .env
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

// app/products/[handle]/page.tsx
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... */}
    </>
  );
}
// app/sitemap.xml/route.ts
import { productSitemapRoute } from "@agentshop/seo/next";
export const { GET } = productSitemapRoute();

Next.js — Pages Router

// pages/products/[handle].tsx
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... */}
    </>
  );
}
// pages/sitemap.xml.ts
import { productSitemapHandler } from "@agentshop/seo/next/pages";
export default productSitemapHandler();

Remix

// app/routes/products.$handle.tsx
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

// app/routes/products.$handle.tsx
import { json } from "@shopify/remix-oxygen";
import { loadProductSeo, agentshopProductMeta } from "@agentshop/seo/hydrogen";

// Oxygen exposes env on context.env — set AGENTSHOP_API_KEY + AGENTSHOP_SEO_URL there.
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 });
// other helpers: loadProductSeo(handle, { baseUrl, apiKey }), ProductJsonLd config prop, etc.
  • 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

Versions
React18, 19
Next.js14, 15, 16 (async params handled)
Remixv2
HydrogenOxygen (context.env)
RuntimeNode ≥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

  • Render each JSON-LD component once, and only if your storefront doesn't already emit its own. Two Product nodes describing the same page leave Google choosing between them — it may use the wrong one or ignore both, and mismatches show up as Search Console warnings. These are server components with no DOM to inspect, so the package can't detect an existing block for you.
  • Remix / Hydrogen: the framework serializes script:ld+json itself, with no < escaping. If a bundle's JSON-LD contains any <, this package omits the descriptor (and logs a warning once per page) rather than emit markup that could break out of the <script> tag — you'll see the rest of the meta tags but no structured data for that page. The check is broader than </ on purpose: <!--<script> escapes the element through the HTML tokenizer's escape states without containing </ at all. That means a bare < in a title or description (Widget < 5kg) also suppresses structured data — write it as &lt; or remove it to restore. The Next adapters escape instead, via serializeJsonLd, and are unaffected.
  • Calling the REST API directly? bundle.jsonLd is returned as a plain object, unescaped. If you render it yourself, use the exported serializeJsonLd — JSON.stringify alone lets a </script> in any product field inject markup into your storefront:
    import { serializeJsonLd } from "@agentshop/seo";
    <script type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: serializeJsonLd(bundle.jsonLd) }} />
    
  • App Router's generateProductMetadata omits og:type — Next's typed Metadata.openGraph rejects "product". The Pages Router / Remix / Hydrogen adapters emit og:type=product (raw meta).
  • React 18 (Next 14): <ProductJsonLd> is an async server component, which React 18's types flag as "not a valid JSX element" (it works at runtime). If your editor complains, render it inside your async page or add {/* @ts-expect-error async server component */} above it. React 19 (Next 15/16) has no such issue.
  • ESM-only. Zero runtime dependencies (uses the global fetch).

Keywords

seo

FAQs

Package last updated on 10 Aug 2026

Related posts