The Clione render contract for commercetools
Who this is for: the developers who build the storefront. Not the merchant. If you were sent here, somebody connected a commercetools project to Clione and the work below is the half that makes it visible.
This page is public because the people who have to act on it do not have accounts in our internal docs, and a contract only one side can read is not a contract.
Why there is anything for you to do
On Shopify, BigCommerce and WooCommerce, Clione publishes into the storefront
itself — a theme block, a widget, a wp_head hook. commercetools has no
storefront. Clione writes four attributes onto your product projections, and
your front end renders them.
If it does not, nothing changes in your HTML. A perfect write and a front end that never reads the attributes produce exactly the same page as no Clione at all — because the consumers here are crawlers and AI assistants, and they do not execute JavaScript and do not call our API.
The four attributes
Declared on your product type, read off the variant (commercetools has no
product-level attribute). Every one is ltext — a LocalizedString — because a
product projection is already per locale:
| Attribute | Type | Carries |
|---|---|---|
clione_meta_title | ltext | the meta title, per locale |
clione_meta_description | ltext | the meta description, per locale |
clione_faq | ltext | a JSON array of { question, answer }, per locale |
clione_jsonld | ltext | the schema.org @graph, as JSON, per locale |
Treat these names as frozen once you have shipped against them. Renaming one is a deploy on both sides in the same window: in between, every product either carries the old name your code no longer reads or the new name your deployed code does not know. If you need different names, say so before you go live.
What you have to render
Three things, in the first HTML response, with no JavaScript:
- The title and description from
clione_meta_titleandclione_meta_description. - One
<script type="application/ld+json">holdingclione_jsonldverbatim. - The FAQ as real markup —
<details>/<summary>works and needs no JS.
And two documents on your domain, not ours:
/llms.txt— an index of your pages/.well-known/agents.json— the same, for agents
They have to be on your origin. A document a crawler finds on another domain describing your site is discounted, so the helpers below throw if you hand them anything but your own origin.
What you do NOT have to do
Compose any schema.org. clione_jsonld is already the graph Clione built
for that entity — Organization, WebSite, BreadcrumbList, the Product and the
FAQPage, cross-referenced by @id. Emit it. Building your own means two
sources of truth for one page, and ours is the one the merchant reviews and
approves.
Copy this
The reference implementation is a Next.js App Router app in the Clione repository:
packages/seo-client/examples/commercetools-next/
lib/clione-contract.ts <- copy this file; it has no dependencies
app/products/[key]/page.tsx <- a SERVER component
app/llms.txt/route.ts
app/.well-known/agents.json/route.ts
mock/products.ts <- one enriched product, one never enriched
lib/clione-contract.ts imports nothing — not from Clione, not from a
framework — so it is yours the moment you copy it. Change the markup, the class
names, the fallback rules. Clione keeps an identical copy on its side, and a
test runs both over the same fixtures on every build, so the file cannot drift
away from what the write side produces.
Ask your Clione contact for the directory if you do not have the repository.
Four things that are easy to get wrong
A client component. The App Router default is a server component; keep it.
ssr: false, a useEffect fetch or a client-side mount all produce an empty
shell for the only readers that matter.
React escaping the JSON-LD. Putting the graph in a <script> from JSX needs
dangerouslySetInnerHTML; without it React escapes the body into something no
parser reads as JSON-LD. Escape < to < first — a </script> inside any
string ends the element as far as the HTML parser is concerned, whatever the
JSON says. The reference does both.
A silent locale fallback. Serving English meta on a German page looks correct to everyone except the reader. The reference ships with no fallback: a locale Clione has not written yet renders your own native meta instead. Your call to change, but make it deliberately.
Rendering nothing when a signal is missing, and saying nothing. Every read
in the contract answers a reason — not_enriched, locale_missing,
invalid_json, wrong_shape — so you can log it. A renderer that cannot tell
"nobody has enriched this product yet" from "the attribute holds something we
could not parse" is how a bad write survives a month.
Check it the way we do
No browser, no JS execution — the same commands we run against every other platform:
UA="Mozilla/5.0 (compatible; Googlebot/2.1)"
curl -sSL -A "$UA" https://your-shop.example.com/products/some-key -o /tmp/p.html
grep -c '"@type":"Product"' /tmp/p.html # >= 1
grep -c '"@type":"FAQPage"' /tmp/p.html # >= 1
grep -c '"@type":"Organization"' /tmp/p.html # >= 1
grep -c '"@type":"WebSite"' /tmp/p.html # >= 1
grep -c '"@type":"BreadcrumbList"' /tmp/p.html # >= 1
grep -c '<summary>' /tmp/p.html # >= 1 the visible FAQ
curl -sS https://your-shop.example.com/llms.txt | grep -c 'clione.ai' # == 0
# The control that makes the rest mean something: a product with no Clione
# attributes must render NONE of it.
curl -sSL -A "$UA" https://your-shop.example.com/products/not-enriched-yet \
| grep -c 'application/ld+json' # == 0
That last command is the one people skip. Without it, every check above is also satisfied by a page that prints a hardcoded graph, and a product that lost its signals would look fine.
Status
The write side — Clione setting these attributes on your products — is in development. Reading your catalog and enriching it work today. Nothing appears on your site until both this contract and the write side are live.