Add commerce-llms-txt template: agent-readable product catalogs powered by Workers AI#968
Add commerce-llms-txt template: agent-readable product catalogs powered by Workers AI#968mconroy-cf wants to merge 7 commits intocloudflare:mainfrom
Conversation
…comments, stabilise tests
Preview AssetsHere's the preview screenshot for the template gallery card (16:9, styled rendering of the Preview image source: I've included an HTML file below that renders the template output as it would appear in a browser. Happy to provide a PNG screenshot at whatever resolution you need, or you can render it directly from this HTML. The template serves JSON/markdown endpoints (no visual UI), so this styled rendering of the Preview screenshot HTML (expand to view source)<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commerce llms.txt — Preview</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
background: #1e1e2e;
color: #cdd6f4;
padding: 2rem 3rem;
font-size: 14px;
line-height: 1.7;
}
.url-bar {
background: #313244;
border-radius: 8px;
padding: 0.5rem 1rem;
margin-bottom: 1.5rem;
color: #a6adc8;
font-size: 13px;
display: flex;
align-items: center;
gap: 0.75rem;
}
.url-bar .dots { display: flex; gap: 6px; }
.url-bar .dot { width: 12px; height: 12px; border-radius: 50%; }
.dot.red { background: #f38ba8; }
.dot.yellow { background: #f9e2af; }
.dot.green { background: #a6e3a1; }
.url-bar .url { color: #89b4fa; }
h1 { color: #cba6f7; font-size: 1.5rem; margin-bottom: 0.75rem; }
blockquote {
border-left: 3px solid #45475a;
padding-left: 1rem;
color: #a6adc8;
margin-bottom: 1rem;
}
.meta { color: #6c7086; margin-bottom: 1.5rem; }
.meta strong { color: #89b4fa; }
h2 { color: #89dceb; font-size: 1.1rem; margin: 1.5rem 0 0.75rem 0; }
h3 { color: #f5c2e7; font-size: 1rem; margin: 1rem 0 0.3rem 0; }
ul { list-style: none; padding: 0; }
li { padding: 0.1rem 0; }
li strong { color: #89b4fa; }
.price { color: #a6e3a1; }
.summary { color: #cdd6f4; }
.bestfor { color: #f9e2af; }
.usecases { color: #a6adc8; }
a { color: #89b4fa; text-decoration: none; }
.badge {
display: inline-block;
background: #313244;
border: 1px solid #45475a;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-size: 12px;
margin-right: 0.5rem;
}
.badge.ai { border-color: #cba6f7; color: #cba6f7; }
.badge.kv { border-color: #89dceb; color: #89dceb; }
.badge.shopify { border-color: #a6e3a1; color: #a6e3a1; }
.badges { margin-bottom: 1.5rem; }
</style>
</head>
<body>
<div class="url-bar">
<div class="dots">
<div class="dot red"></div>
<div class="dot yellow"></div>
<div class="dot green"></div>
</div>
<span class="url">https://yourstore.com/llms.txt</span>
</div>
<div class="badges">
<span class="badge ai">Workers AI</span>
<span class="badge kv">KV Cache</span>
<span class="badge shopify">Shopify</span>
</div>
<h1># Summit Sprouts</h1>
<blockquote>A family-focused ski and snow shop specializing in children's gear. This catalog is dynamically generated with real-time inventory and AI-enriched product descriptions optimized for agent consumption.</blockquote>
<div class="meta">
<li><strong>Generated</strong>: 2026-04-15T13:00:51.445Z</li>
<li><strong>Total products</strong>: 6 (5 in stock, 1 out of stock)</li>
<li><strong>Categories</strong>: toddler-skis, helmets, boots, clothing, accessories</li>
<li><strong>Pricing</strong>: All prices in USD, tax not included</li>
</div>
<h2>## Products In Stock</h2>
<h3>### Little Ripper 70 Ski Package</h3>
<ul>
<li><strong>Price</strong>: <span class="price">$89.99</span></li>
<li><strong>Availability</strong>: In stock (18 available)</li>
<li><strong>Summary</strong>: <span class="summary">Lightweight plastic skis designed for toddlers skiing for the very first time. Short enough for a 3-year-old to control on a bunny hill, with step-in bindings simple enough for parents to operate while wearing gloves.</span></li>
<li><strong>Best for</strong>: <span class="bestfor">First-time skiers ages 2-4 who need lightweight, forgiving gear to build confidence on gentle slopes.</span></li>
<li><strong>Use cases</strong>: <span class="usecases">first ski lesson, resort bunny hill, backyard snow play</span></li>
</ul>
<h3>### Snow Sprout Toddler Helmet</h3>
<ul>
<li><strong>Price</strong>: <span class="price">$49.99</span></li>
<li><strong>Availability</strong>: In stock (31 available)</li>
<li><strong>Summary</strong>: <span class="summary">A properly fitted toddler ski helmet that actually fits heads as small as 48cm — most kid helmets start at 52cm. Dial-adjust sizing means it grows with the child for 2-3 seasons.</span></li>
<li><strong>Best for</strong>: <span class="bestfor">Toddlers and preschoolers (ages 2-5) who need a helmet that actually fits.</span></li>
</ul>
<h3>### Tiny Tracks Ski Boots</h3>
<ul>
<li><strong>Price</strong>: <span class="price">$59.99</span></li>
<li><strong>Availability</strong>: In stock (14 available)</li>
<li><strong>Summary</strong>: <span class="summary">Toddler ski boots with a single buckle and wide rear opening — designed so parents aren't wrestling a squirming 3-year-old's foot into a stiff boot in a parking lot.</span></li>
</ul>
</body>
</html>For the icon SVG: Happy for the Growth team to generate one, or I can provide a custom SVG if preferred. A shopping cart + document or text-file-with-sparkle motif would fit the template's purpose. Let me know the preferred format/resolution for the preview image and I'll provide a final PNG, or feel free to render directly from the HTML above at 1920x1080. |
- Remove PLACEHOLDER KV namespace so one-click deploy works out of the box - Restructure README: deploy-first flow, Shopify/KV as optional next steps - Add configurable AI_MODEL env var (no more hardcoded model ID) - Fix stock count: report actual Shopify variant availability, not fabricated numbers - Add Shopify pagination (up to 5,000 products across pages) - Use waitUntil for non-blocking KV writes instead of awaiting - Document Content-Signal header, caching behaviour, and known limitations - Add missing bindings to package.json cloudflare metadata - Remove broken image URL placeholders from sample catalog
Address review feedback: simplify caching to a single KV-backed path and swap the default enrichment model to Google Gemma 4. - Remove in-memory fallback cache and its module-scoped globals from llms-txt-worker. KV is now the only cache path, which matches production behaviour and removes branching a template reader has to reason about. - Make ENRICHMENT_CACHE a required binding on the Env interface; collapse the `if (env.ENRICHMENT_CACHE) else` branches in getEnrichedCatalog. - Declare `kv_namespaces` for ENRICHMENT_CACHE in wrangler.json with a clearly-marked placeholder id. One-click "Deploy to Cloudflare" provisions the namespace automatically; manual deploys paste the id from `wrangler kv namespace create` into the placeholder. - Swap DEFAULT_AI_MODEL from @cf/meta/llama-3.1-8b-instruct to @cf/google/gemma-4-26b-a4b-it in the enrichment module and wrangler defaults. - Update README Setup section to reflect that KV is part of the deploy flow (not a later-optional upgrade), update the AI_MODEL config-table default, and note Miniflare provisions a local KV namespace for `npm run dev` and `npm run test`. - Update package.json cloudflare.bindings description for AI_MODEL.
Love this. Would it be possible to throw something like this on top of the API endpoint so there's a visual component to the app itself? If you look at the vite react template you can see how we can do both the api and little static react spa in the same worker: https://github.com/cloudflare/templates/tree/main/vite-react-template |
Per @deloreyj's review feedback, graft the vite-react-template pattern onto the Worker so visiting the root URL renders a visual landing page instead of returning JSON. The SPA fetches the Worker's own endpoints (/api/raw-catalog and /api/products) and shows the raw merchant data next to the Workers AI-enriched output per product, making the template self-demonstrating in the gallery. Worker behavior is unchanged for existing API routes (/llms.txt, /llms-full.txt, /api/products, /api/products/:slug, /api). The old GET / JSON handler is removed (assets now own that route); its fields are merged into /api. A new /api/raw-catalog endpoint exposes the pre-enrichment catalog so the SPA can render the 'before' side. - wrangler.json: add assets.directory + SPA fallback - vite.config.ts, index.html, src/react-app/*: React 19 SPA - tsconfig split into project references (app/worker/node) - Tests updated: former GET / test retargeted to /api; new /api/raw-catalog test added. Playwright spec now checks for the SPA heading on / and API JSON on /api.
|
I think this should cover it, right? Thanks a ton for helping me get this over the line. I really appreciate it |
|
Let me know if this addresses your ask. I'll follow this advice on all future template additions too! thanks! |
deloreyj
left a comment
There was a problem hiding this comment.
Review summary
Reviewed at HEAD 9a54f14. Strong template — approving.
Checks (all green locally)
pnpm run check:templates— passpnpm run check:lockfiles— passpnpm run check:prettier— passtsc && vite build && wrangler deploy --dry-run— passpnpm test— 10/10 pass (ran 3x to confirm reliability)
What's good
- README is best-in-class: deploy button at top, dash-content markers, "Who is this for", "After deploy", endpoint table, configuration table, caching explanation, Shopify integration notes, known limitations
- 10 vitest tests + 3 Playwright E2E tests — well above the 5 minimum
- Clean architecture: worker / enrichment / shopify / catalog / types are properly separated
- Defensive fallbacks throughout: no AI binding → rule-based fallback, Shopify failure → sample catalog, KV write uses
ctx.waitUntil - Hand-written
FALLBACK_ENRICHMENTSmake local dev demo-quality without needing AI Content-Signalresponse header is a thoughtful agent-friendly touch- The bundled React + Vite SPA gives the deploy-button experience real visual punch — raw vs enriched side-by-side is a great way to communicate what Workers AI actually adds
@cf/google/gemma-4-26b-a4b-itis a current Workers AI model (released April 2026) — appropriate default
Minor non-blocking observations
- Tests take ~30s because each test triggers a real Workers AI call (KV is per-test in the miniflare pool, so the cache doesn't persist). Not blocking, but consider seeding the KV namespace in a
beforeAllhook in a future revision to make the suite run in a few seconds. <REPLACE_WITH_YOUR_KV_NAMESPACE_ID>literal inwrangler.json— the Deploy to Cloudflare flow auto-provisions KV (per docs) and the README documents the manual override clearly, so this is fine. Other KV-using templates in this repo (to-do-list-kv-template,openauth-template) ship with real-looking hex IDs as their default, which is also a valid pattern. Either works.
Pending Cloudflare team
Per CONTRIBUTING.md these are not contributor-blockable:
cloudflare.preview_image_url— Growth team uploadscloudflare.preview_icon_url— Growth team uploadscloudflare.publish: true— set when ready to publish
Approving — looks ready to merge once preview assets and publish: true are in place.
|
Preview link not generated: you must be on a branch, not on a fork. |


Summary
Adds a new Workers template that makes merchant product catalogs visible to AI shopping agents by serving a dynamic
/llms.txtendpoint./products.jsonAPI, with a sample catalog fallback for zero-config demoWhat's included
src/llms-txt-worker/index.tssrc/enrichment/index.tssrc/lib/shopify.tssrc/lib/catalog.tssrc/lib/types.tstest/index.test.tsplaywright-tests/commerce-llms-txt-template.spec.tsEndpoints
GET /llms.txtGET /llms-full.txtGET /api/productsGET /api/products/:slugGET /apiBindings
ENRICHMENT_CACHEfor persistent enrichment cacheMERCHANT_NAME,MERCHANT_DESCRIPTION,STORE_CURRENCY,MERCHANT_VERTICAL,SHOPIFY_STORE_DOMAIN, etc.Checklist
package.jsonwithcloudflaremetadata (label, products, categories, bindings)wrangler.json(not .toml)README.mdwith deploy button +dash-content-start/endmarkers.gitignore(standard template)preview_image_url/preview_icon_url— need from Growth teampublish: true— waiting for team approval