Skip to main content

@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 pathWhat it gives you
@clione/seoThe core client: createClioneSeo()
@clione/seo/reactgetClioneSeoMetadata, fetchClioneSeo, getJsonLdScriptProps
@clione/seo/remixloadClioneSignals, clioneMeta, <ClioneJsonLd>, <ClioneHead>, <ClioneFaq>, CLIONE_STOREFRONT_FIELDS
@clione/seo/nuxtuseClioneSchemaGraph, useClioneSeo

Two modes​

ModeWhen 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​

OptionDefaultNotes
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​

MethodReturnsNotes
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)ClioneSeoSignalsMetafield mode. No network call.
toHeadTags(signals)SeoHeadTagsFramework-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 @graph Clione propagated. faq is derived from its FAQPage node.
  • clione.faq_html — the visible FAQ accordion markup.
  • seo { title description } — the meta title and description Clione wrote to global.title_tag / global.description_tag. The Storefront API does not return the global namespace through metafields(identifiers:), so query seo instead. 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>
)
}
ExportWhat it does
CLIONE_STOREFRONT_FIELDSGraphQL 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.