Template Routing
Every request to the theme worker goes through two stages: gating (should we render this, or proxy to Shopify?) and resolution (which template file do we use?). Both stages rely on the same URL → template mapping, but gating must also consult KV to resolve resource-specific template suffixes.
Request: /products/snowboard │ ▼1. GATING ─── resolveFullTemplateName() ──▶ "product.ge-optimized-color" │ │ │ KV lookup: fmdf:product:snowboard → { template_suffix: "ge-optimized-color" } │ ├── in templates_json? ──▶ YES → render └── not in list? ──▶ proxy to Shopify (301)
2. RESOLUTION ─── handle-render loads template ──▶ product.ge-optimized-color.jsonURL → base template mapping
Section titled “URL → base template mapping”resolveBaseTemplateName() maps URL pathnames to base template types:
| URL pattern | Base template |
|---|---|
/ or empty | index |
/products/{handle} | product |
/collections/{handle}/products/{handle} | product |
/collections | list-collections |
/collections/{handle} | collection |
/cart | cart |
/pages/{handle} | page |
/blogs/{blog}/{article} | article |
/blogs/{blog} | blog |
/search | search |
| anything else | index |
Template suffixes
Section titled “Template suffixes”Shopify resources (products, collections, pages) can have a template_suffix that maps them to alternate templates. For example, a product with template_suffix: "ge-optimized-color" uses templates/product.ge-optimized-color.json instead of templates/product.json.
The ?view=X query parameter overrides any resource-level suffix — ?view=modal always resolves to product.modal regardless of the product’s stored suffix.
Gating — the templates allowlist
Section titled “Gating — the templates allowlist”The templates_json column in the D1 clients table controls which templates the theme worker renders vs proxies to Shopify. Deploys can override the client-level list.
["index", "article", "collection", "product.ge-optimized-color"]1:1 matching — each entry matches exactly one template name. "product.ge-optimized-color" only matches products that have template_suffix: "ge-optimized-color". Products without that suffix (or with a different suffix) get proxied to Shopify.
This requires suffix-aware resolution at gating time: before checking the allowlist, the worker reads the resource’s template_suffix from KV via resolveFullTemplateName().
resolveFullTemplateName("/products/snowboard", undefined, kv, "fmdf") → KV get: "fmdf:product:snowboard" → { template_suffix: "ge-optimized-color" } → returns: "product.ge-optimized-color" → check: ["product.ge-optimized-color"] includes it? → YES → renderFor non-resource routes (/cart, /search, /) there’s no KV lookup — the base template name is used directly.
Hydration requirement
Section titled “Hydration requirement”Suffix-aware gating depends on KV having template_suffix populated for every resource. The hydrate-kv script fetches this from the Shopify Admin API:
pnpm hydrate:kv --slug=fmdf # all content typespnpm hydrate:kv --slug=fmdf --only=pages # just pages (~2s vs minutes)The --only flag accepts comma-separated types: products, collections, pages, blogs, menus.
This populates KV entries like fmdf:product:snowboard with { ..., template_suffix: "ge-optimized-color" } for all products, collections, pages, blogs, and articles.
You must hydrate before enabling a new template in templates_json. Without hydration, suffix resolution falls back to the bare base name (e.g., "product" instead of "product.ge-optimized-color"), and gating won’t match.
When to re-hydrate
Section titled “When to re-hydrate”- Before enabling a new suffixed template route
- After a merchant changes a product’s template assignment in Shopify admin
- After bulk product imports or template reassignments
Standard deploys (pnpm deploy:theme) do not require re-hydration — template suffixes are stable properties of resources, not of the theme code.
Enabling a new template (step-by-step)
Section titled “Enabling a new template (step-by-step)”When a template is ready for production (parity verified, tests passing):
1. UPDATE D1 templates_json ← must be FIRST, otherwise worker still proxies2. Deploy theme worker ← full rebuild if code changed3. Re-seed production KV ← for the specific content type4. Purge CDN cache ← zone-level host purge5. Verify rendering ← check x-rendered-by header1. Update the gating allowlist
Section titled “1. Update the gating allowlist”eval "$(mise env)"# Check current listnpx wrangler d1 execute layerkick-config --remote \ --command="SELECT templates_json FROM clients WHERE slug = '<slug>'" --json \ | jq '.[0].results[0].templates_json | fromjson'
# Add the new template (use FULL name with suffix)npx wrangler d1 execute layerkick-config --remote \ --command="UPDATE clients SET templates_json = '[\"index\",...,\"page\"]' WHERE slug = '<slug>'"Use the full suffixed name — "product.ge-optimized-color" not "product". Base templates like "page", "index", "article" have no suffix.
2. Deploy
Section titled “2. Deploy”pnpm deploy:theme <slug> --promote # full rebuild + promote dispatch pointerUse --skip-build only if the worker code hasn’t changed (KV-only update).
3. Re-seed KV
Section titled “3. Re-seed KV”pnpm hydrate:kv --slug=<slug> --only=pages # targeted (~2s)4. Purge CDN
Section titled “4. Purge CDN”curl -s -X POST "https://api.cloudflare.com/client/v4/zones/<zone-id>/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -d '{"hosts":["<slug>.layerkick.com"]}' | jq .successThe D1 in-memory cache has a 60s TTL — changes propagate globally within one minute without manual purge.
5. Verify
Section titled “5. Verify”curl -sI "https://<slug>.layerkick.com/pages/<handle>" | grep x-rendered-by# Expected: x-rendered-by: layerkick# If proxied: x-rendered-by: layerkick-proxyBrowser 301 cache: If the route previously returned a 301 redirect, Chrome caches it permanently. Users must clear site data or use incognito.
Resolution — selecting the template file
Section titled “Resolution — selecting the template file”After gating passes, handle-render.ts loads the resource from KV and selects the template:
- Read
template_suffixfrom the KV resource entry - Map to template JSON:
templates/product.ge-optimized-color.json - Load section order + settings from the template JSON
- Render each section via the transpiled registry
Key files
Section titled “Key files”| Concern | File |
|---|---|
| URL → template name | packages/cf-client-site/src/lib/template-resolution.ts |
| Gating check | packages/cf-client-site/src/worker.ts |
| Template rendering | packages/cf-client-site/src/render/handle-render.ts |
| KV hydration (production) | scripts/hydrate-kv.ts |
| KV seeding (local dev) | scripts/seed-kv.ts |
| Tests | packages/cf-client-site/src/lib/template-resolution.test.ts |