Skip to content

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.json

resolveBaseTemplateName() maps URL pathnames to base template types:

URL patternBase template
/ or emptyindex
/products/{handle}product
/collections/{handle}/products/{handle}product
/collectionslist-collections
/collections/{handle}collection
/cartcart
/pages/{handle}page
/blogs/{blog}/{article}article
/blogs/{blog}blog
/searchsearch
anything elseindex

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.

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 → render

For non-resource routes (/cart, /search, /) there’s no KV lookup — the base template name is used directly.

Suffix-aware gating depends on KV having template_suffix populated for every resource. The hydrate-kv script fetches this from the Shopify Admin API:

Terminal window
pnpm hydrate:kv --slug=fmdf # all content types
pnpm 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.

  • 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.

When a template is ready for production (parity verified, tests passing):

1. UPDATE D1 templates_json ← must be FIRST, otherwise worker still proxies
2. Deploy theme worker ← full rebuild if code changed
3. Re-seed production KV ← for the specific content type
4. Purge CDN cache ← zone-level host purge
5. Verify rendering ← check x-rendered-by header
Terminal window
eval "$(mise env)"
# Check current list
npx 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.

Terminal window
pnpm deploy:theme <slug> --promote # full rebuild + promote dispatch pointer

Use --skip-build only if the worker code hasn’t changed (KV-only update).

Terminal window
pnpm hydrate:kv --slug=<slug> --only=pages # targeted (~2s)
Terminal window
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 .success

The D1 in-memory cache has a 60s TTL — changes propagate globally within one minute without manual purge.

Terminal window
curl -sI "https://<slug>.layerkick.com/pages/<handle>" | grep x-rendered-by
# Expected: x-rendered-by: layerkick
# If proxied: x-rendered-by: layerkick-proxy

Browser 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:

  1. Read template_suffix from the KV resource entry
  2. Map to template JSON: templates/product.ge-optimized-color.json
  3. Load section order + settings from the template JSON
  4. Render each section via the transpiled registry
ConcernFile
URL → template namepackages/cf-client-site/src/lib/template-resolution.ts
Gating checkpackages/cf-client-site/src/worker.ts
Template renderingpackages/cf-client-site/src/render/handle-render.ts
KV hydration (production)scripts/hydrate-kv.ts
KV seeding (local dev)scripts/seed-kv.ts
Testspackages/cf-client-site/src/lib/template-resolution.test.ts