Skip to main content

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.

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"> from seo { title description }, through meta().
  • <script type="application/ld+json"> with the clione.jsonld graph, verbatim (Organization + WebSite + BreadcrumbList + Product, plus FAQPage when there are FAQs).
  • The .clione-faq accordion from clione.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:).