Clione + Hydrogen (Remix) Integration Guide
Render Clione SEO signals in a Shopify Hydrogen storefront, server-side, on the first response.
@clione/seo/remix gives you the same two blocks the Shopify theme app extension renders — the JSON-LD @graph and the visible FAQ accordion — as React components, fed by the same metafields.
Install
npm install @clione/seo
react (already in every Hydrogen app) is the only peer dependency this adapter needs.
Option A: Metafield mode (recommended)
Hydrogen already queries Shopify's Storefront API. Add Clione's fields to that query — no extra API call, no Clione API key.
1. Add Clione's fields to your query
CLIONE_STOREFRONT_FIELDS selects seo { title description } and the clione.jsonld and clione.faq_html metafields:
import { CLIONE_STOREFRONT_FIELDS } from '@clione/seo/remix'
const PRODUCT_QUERY = `#graphql
query ClioneProduct($handle: String!, $country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
product(handle: $handle) {
id
title
handle
descriptionHtml
${CLIONE_STOREFRONT_FIELDS}
}
}
`
The meta title and description Clione writes live in Shopify's global.title_tag / global.description_tag. The Storefront API does not return the global namespace through metafields(identifiers:); seo { title description } is how a storefront reads them, and it's already in the selection. 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.
CLIONE_STOREFRONT_FIELDS reads only the clione.jsonld and clione.faq_html metafields, so in a translated market the JSON-LD and the FAQ this adapter renders stay in the store's primary language.
2. Load the signals and render them
// app/routes/products.$handle.tsx
import { json, type LoaderFunctionArgs, type MetaFunction } from '@shopify/remix-oxygen'
import { useLoaderData } from '@remix-run/react'
import { ClioneFaq, ClioneJsonLd, clioneMeta, loadClioneSignals } from '@clione/seo/remix'
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 })
// Metafield mode: no network call. The signals are plain JSON.
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>
{/* A JSON-LD script is valid anywhere in the document. */}
<ClioneJsonLd signals={seo} />
<h1>{product.title}</h1>
<div dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} />
{/* Where you want the visible FAQ accordion. */}
<ClioneFaq signals={seo} />
</main>
)
}
What this puts in the first HTML response, with no Clione script:
<title>and<meta name="description">fromseo { title description }, throughmeta().<script type="application/ld+json">with theclione.jsonldgraph, verbatim (Organization + WebSite + BreadcrumbList + Product, plus FAQPage when there are FAQs).- The
.clione-faqaccordion fromclione.faq_html, verbatim. <meta name="clione:enriched" content="true">, which the Clione Pulse extension uses to recognise a headless storefront.
Your own breadcrumb
If the route already renders a BreadcrumbList (JSON-LD, microdata or RDFa), tell <ClioneJsonLd> so Clione's is left out — two breadcrumbs on one URL conflict:
<ClioneJsonLd signals={seo} nativeStructuredData={true} />
nativeStructuredData also accepts your breadcrumb's HTML string. Hydrogen's default templates render no breadcrumb, so a stock storefront needs nothing.
Rendering <head> from a component
If your layout renders <head> from a component instead of a route meta() export, use <ClioneHead signals={seo} fallbackTitle={product.title} />: title, description, the marker and the JSON-LD in one. Don't combine it with clioneMeta(), or the title is emitted twice.
Option B: API mode
If you'd rather not touch your Storefront API queries, fetch the signals from Clione during SSR. This needs a Clione API key of kind server — see Authentication.
// app/routes/products.$handle.tsx
import { json, type LoaderFunctionArgs, type MetaFunction } from '@shopify/remix-oxygen'
import { useLoaderData } from '@remix-run/react'
import { createClioneSeo } from '@clione/seo'
import { ClioneFaq, ClioneJsonLd, clioneMeta } from '@clione/seo/remix'
export async function loader({ params, context }: LoaderFunctionArgs) {
const clione = createClioneSeo({
source: 'api',
apiUrl: context.env.CLIONE_API_URL,
apiKey: context.env.CLIONE_API_KEY,
platform: 'shopify',
storefrontHost: 'shop.example.com', // a host on the key's allowed domains
})
const [{ product }, seo] = await Promise.all([
context.storefront.query(PRODUCT_QUERY, { variables: { handle: params.handle } }),
clione.getSignals('product', params.handle!), // a Shopify handle works as the id
])
return json({ product, seo })
}
export const meta: MetaFunction<typeof loader> = ({ data }) =>
clioneMeta(data?.seo, { title: data?.product.title })
The component is the same as in Option A. getSignals never throws on a network or HTTP error — it returns empty signals and the page renders without them.
Environment variables
# API mode only
CLIONE_API_URL=https://api.clione.ai
CLIONE_API_KEY=sk_live_...
Why metafield mode is better for Hydrogen
Hydrogen already makes a Storefront API call for every page. Adding Clione's fields to that query costs no extra request. API mode adds a second network call during SSR — fast and cacheable, but unnecessary when the data is already in Shopify.
Supported entity types
Products, collections and pages carry the same clione.jsonld / clione.faq_html metafields, so the loader and components are identical — select CLIONE_STOREFRONT_FIELDS inside collection(handle:) or page(handle:) instead of product(handle:).