@clione/seo SDK
@clione/seo is a framework-agnostic npm package for rendering Clione's enriched SEO/AEO/GEO signals in headless storefronts. Two modes (API or metafield), with adapters for React / Next.js, Remix / Shopify Hydrogen and Nuxt / Vue Storefront.
If you're shipping a custom-built frontend (Catalyst, Hydrogen, Vue Storefront, Next.js Commerce, your own thing), this SDK is the recommended way to consume Clione signals.
For non-headless stores (BC Stencil, Shopify Liquid themes), you don't need the SDK: publishing from Clione delivers the JSON-LD server-side — through a widget on BigCommerce and the theme app embeds on Shopify. See Signal propagation.
Install
npm install @clione/seo
# or
pnpm add @clione/seo
# or
yarn add @clione/seo
The core client uses the runtime's native fetch (Node 18+, Deno, Bun, edge runtimes). The adapters need their framework as a peer dependency: react ≥ 18 for @clione/seo/react and @clione/seo/remix, vue ≥ 3 for @clione/seo/nuxt.
| Import path | What it gives you |
|---|---|
@clione/seo | The core client: createClioneSeo() |
@clione/seo/react | getClioneSeoMetadata, fetchClioneSeo, getJsonLdScriptProps |
@clione/seo/remix | loadClioneSignals, clioneMeta, <ClioneJsonLd>, <ClioneHead>, <ClioneFaq>, CLIONE_STOREFRONT_FIELDS |
@clione/seo/nuxt | useClioneSchemaGraph, useClioneSeo |
Two modes
| Mode | When to use |
|---|---|
API mode (getSignals) | You want signals at request time. Calls the SEO Signals API with a Clione API key. |
Metafield mode (extractSignals) | You already fetch the entity from the Shopify Storefront API with the clione.jsonld metafield in the response. No extra request. |
Create a client
import { createClioneSeo } from '@clione/seo'
const clione = createClioneSeo({
source: 'api',
apiUrl: process.env.CLIONE_API_URL!,
apiKey: process.env.CLIONE_API_KEY!, // a `server` key — keep it on the server
platform: 'shopify',
storefrontHost: 'shop.example.com',
})
Configuration
| Option | Default | Notes |
|---|---|---|
source | 'api' | 'api' or 'metafield'. |
apiUrl | — | Clione API base URL, e.g. https://api.clione.ai. Required in API mode. A base path is kept (https://example.com/proxy/clione works). |
apiKey | — | Clione API key with products:read. Sent as Authorization: Bearer (getSignals) or X-API-Key (getSchemaGraph). |
platform | 'bigcommerce' | 'shopify' or 'bigcommerce'. Pass it explicitly: a Shopify store queried without it is looked up as BigCommerce. |
storefrontHost | — | The storefront host this client renders for. Sent as X-Clione-Storefront-Host; needed when a server key has allowed domains. See Authentication. |
Methods
| Method | Returns | Notes |
|---|---|---|
getSignals(entityType, entityId) | Promise<ClioneSeoSignals> | API mode. Calls GET /api/v1/public/seo-signals/:platform/:entityType/:entityId. |
getSchemaGraph(entityType?, entityId?, pageUrl?) | Promise<ClioneSeoSchemaGraph | null> | Calls GET /api/v1/public/schema.jsonld. Without an entity, returns Organization + WebSite for non-entity pages. |
renderSchemaGraph(entityType?, entityId?, pageUrl?) | Promise<string> | The graph as a <script type="application/ld+json"> string, or ''. |
extractSignals(entity) | ClioneSeoSignals | Metafield mode. No network call. |
toHeadTags(signals) | SeoHeadTags | Framework-agnostic { title, meta, script } descriptors. |
renderHead(signals) | string | <title>, <meta name="description"> and the JSON-LD <script> as an HTML string. |
Errors. getSignals does not throw on a network error or a non-2xx response: it logs a warning (with the HTTP status for a non-2xx response) and returns empty signals (jsonLd: null, metaTitle: null, metaDescription: null, keywords: [], faq: null), so a Clione outage never breaks your page render. getSchemaGraph returns null in the same cases. Both throw only on a configuration error — a missing apiUrl, or getSignals called on a client created with source: 'metafield'.
All JSON-LD the SDK renders is serialized with < escaped, so a </script> inside an answer cannot close the tag. The heads built by renderHead, toHeadTags, clioneMeta and <ClioneHead> also carry <meta name="clione:enriched" content="true">, which the Clione Pulse extension uses to recognise a headless storefront.
API mode
const signals = await clione.getSignals('product', productId)
const headHtml = clione.renderHead(signals)
Entity types
type ClioneEntityType = 'product' | 'category' | 'collection' | 'page'
Use category on BigCommerce, collection on Shopify. entityId is the entity's Clione id or, for products, the platform's numeric id; on Shopify a handle also works.
Signals
interface ClioneSeoSignals {
jsonLd: Record<string, unknown> | null
metaDescription: string | null
metaTitle: string | null
keywords: string[]
faq: Array<{ question: string; answer: string }> | null
faqHtml?: string | null // metafield mode only
}
Metafield mode (Shopify Storefront API)
If you already query the entity from the Storefront API, read Clione's signals from it:
const clione = createClioneSeo({ source: 'metafield' })
const signals = clione.extractSignals(product)
extractSignals reads:
clione.jsonld— the complete@graphClione propagated.faqis derived from itsFAQPagenode.clione.faq_html— the visible FAQ accordion markup.seo { title description }— the meta title and description Clione wrote toglobal.title_tag/global.description_tag. The Storefront API does not return theglobalnamespace throughmetafields(identifiers:), so queryseoinstead. On collections and pages the meta title is only written when Push meta_title to categories, collections, and pages is on (Store Settings → General); on products it always is.
Products, collections and pages all carry these metafields. With @clione/seo/remix, CLIONE_STOREFRONT_FIELDS is the GraphQL selection to paste into your query — see Remix / Hydrogen.
This avoids a second network call. Useful when:
- You already pay the cost of fetching the entity from Shopify.
- You want signals consistent with the entity version you just rendered.
- You're on the edge and want fewer fan-out calls.
Framework adapters
Next.js (App Router)
// app/products/[id]/page.tsx
import { getClioneSeoMetadata, fetchClioneSeo, getJsonLdScriptProps } from '@clione/seo/react'
const config = {
apiUrl: process.env.CLIONE_API_URL!,
apiKey: process.env.CLIONE_API_KEY!,
platform: 'shopify' as const,
storefrontHost: 'shop.example.com',
}
export async function generateMetadata({ params }: { params: { id: string } }) {
return getClioneSeoMetadata(params.id, config, 'product') // { title, description }
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const signals = await fetchClioneSeo('product', params.id, config)
const jsonLdProps = getJsonLdScriptProps(signals)
return (
<>
{jsonLdProps && <script {...jsonLdProps} />}
{/* rest of page */}
</>
)
}
Full walkthrough: Next.js guide.
Remix / Shopify Hydrogen
@clione/seo/remix renders Clione's signals server-side from metafields, on the first response:
// app/routes/products.$handle.tsx
import { json, type LoaderFunctionArgs, type MetaFunction } from '@shopify/remix-oxygen'
import { useLoaderData } from '@remix-run/react'
import { CLIONE_STOREFRONT_FIELDS, ClioneFaq, ClioneJsonLd, clioneMeta, loadClioneSignals } from '@clione/seo/remix'
const PRODUCT_QUERY = `#graphql
query ClioneProduct($handle: String!) {
product(handle: $handle) {
id
title
handle
descriptionHtml
${CLIONE_STOREFRONT_FIELDS}
}
}
`
export async function loader({ params, context }: LoaderFunctionArgs) {
const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: { handle: params.handle },
})
if (!product) throw new Response('Not found', { status: 404 })
return json({ product, seo: loadClioneSignals(product) })
}
export const meta: MetaFunction<typeof loader> = ({ data }) =>
clioneMeta(data?.seo, { title: data?.product.title })
export default function ProductPage() {
const { product, seo } = useLoaderData<typeof loader>()
return (
<main>
<ClioneJsonLd signals={seo} />
<h1>{product.title}</h1>
<ClioneFaq signals={seo} />
</main>
)
}
| Export | What it does |
|---|---|
CLIONE_STOREFRONT_FIELDS | GraphQL selection for product, collection or page: seo { title description } plus the clione.jsonld and clione.faq_html metafields. |
loadClioneSignals(entity) | Loader helper. Metafield mode, no network call; returns plain JSON for useLoaderData(). |
clioneMeta(signals, fallback?) | Descriptors for a route's meta() export: title (falling back to fallback.title), description when there is one, and the clione:enriched marker. |
<ClioneJsonLd signals nativeStructuredData? /> | The clione.jsonld graph verbatim in one <script type="application/ld+json">. Pass nativeStructuredData (true, your breadcrumb's HTML as a string, or { jsonLdBlocks: [yourJsonLdString] }) when the page already declares a BreadcrumbList, and Clione's is left out. |
<ClioneHead signals fallbackTitle? /> | Title + description + marker + <ClioneJsonLd>, for a layout that renders <head> from a component. Don't combine it with clioneMeta() or the title is emitted twice. |
<ClioneFaq signals heading? /> | The visible FAQ accordion: the clione.faq_html markup verbatim, or built from faq pairs. |
Full walkthrough: Hydrogen guide.
Nuxt / Vue Storefront / Alokai
useClioneSchemaGraph fetches the full @graph (Organization + WebSite + BreadcrumbList + entity + FAQPage) and injects it through Nuxt's useHead() during SSR. Pass Nuxt's own useHead in the config:
<!-- pages/products/[id].vue -->
<script setup>
import { useClioneSchemaGraph } from '@clione/seo/nuxt'
const route = useRoute()
const runtime = useRuntimeConfig()
await useClioneSchemaGraph('product', route.params.id, route.fullPath, {
apiUrl: runtime.public.clione.apiUrl,
apiKey: runtime.clione.apiKey,
platform: 'bigcommerce',
useHead,
})
</script>
Categories, collections and pages use the same call with another entityType. On a page with no entity (home, search), pass undefined for both and you get Organization + WebSite.
useClioneSeo(entityOrId, config) injects the single-entity signals instead: meta title, description and the entity's JSON-LD. Pass an id string for API mode (with entityType in the config, default 'product'), or an entity object with metafields for metafield mode.
Caching
The SEO Signals API answers with Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400. The SDK calls the platform fetch with no cache options of its own, so cache with your framework's primitive (Next.js revalidate, Hydrogen's cache strategies, your CDN).
TypeScript
The package ships its types:
import type {
ClioneSeoConfig,
ClioneSeoSignals,
ClioneSeoApiResponse,
ClioneSeoSchemaGraph,
ProductWithMetafields,
SeoHeadTags,
} from '@clione/seo'
Troubleshooting
Empty signals and a @clione/seo: API returned 401 warning — Bad or missing API key. Check CLIONE_API_KEY.
API returned 403 — The key lacks products:read; or it has allowed domains and the request named none of them (set storefrontHost to a host on the list, or update the list in Store Settings → API Keys); or it is a browser key used from a server — create a server key.
API returned 404 — Entity not synced, wrong id, or wrong platform (remember it defaults to bigcommerce).
Metafield mode returns empty signals — The clione.jsonld metafield is empty (the entity hasn't been propagated), or the query doesn't select it. Use CLIONE_STOREFRONT_FIELDS.