skills/decocms/deco-start/deco-cms-route-config

deco-cms-route-config

SKILL.md

CMS Route Configuration in @decocms/start

Reusable route configuration factories that live in @decocms/start/routes. Sites use thin wrappers that delegate to these factories, keeping route files small and consistent across all Deco sites.

When to Use This Skill

  • Setting up routes for a new Deco TanStack storefront
  • Migrating Fresh routes to TanStack Start
  • Debugging why variant changes trigger server re-fetches
  • Configuring cache headers per page type
  • Setting up admin protocol routes (meta, render, invoke)
  • Understanding the relationship between loaderDeps, staleTime, and server-side caching

Route Architecture

Site Routes (thin wrappers)          Framework (@decocms/start/routes)
─────────────────────────           ──────────────────────────────────
src/routes/$.tsx          ───────→  cmsRouteConfig()
src/routes/index.tsx      ───────→  cmsHomeRouteConfig()
src/routes/deco/meta.ts   ───────→  decoMetaRoute
src/routes/deco/render.ts ───────→  decoRenderRoute
src/routes/deco/invoke.$.ts ─────→  decoInvokeRoute
src/routes/__root.tsx     ×         Site-specific (fonts, theme, CSS)

Catch-All CMS Route ($.tsx)

The catch-all route handles all CMS-managed pages (PDP, PLP, institutional pages, etc.).

Site File (minimal)

// src/routes/$.tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { cmsRouteConfig, NotFoundPage } from "@decocms/start/routes";
import { DecoPageRenderer } from "@decocms/start/hooks";

const config = cmsRouteConfig({
  siteName: "My Store",
  defaultTitle: "My Store - Default Title",
  ignoreSearchParams: ["skuId"],
});

export const Route = createFileRoute("/$")({
  loaderDeps: config.loaderDeps,
  loader: async (ctx) => {
    const page = await config.loader(ctx);
    if (!page) throw notFound();
    return page;
  },
  component: CmsPage,
  notFoundComponent: NotFoundPage,
  staleTime: config.staleTime,
  gcTime: config.gcTime,
  headers: config.headers,
  head: config.head,
});

function CmsPage() {
  const page = Route.useLoaderData();
  return (
    <div>
      <DecoPageRenderer sections={page.resolvedSections} />
    </div>
  );
}

cmsRouteConfig Options

interface CmsRouteOptions {
  siteName: string;        // Used in page title: "Page Name | siteName"
  defaultTitle: string;    // Fallback title when CMS page has no name
  ignoreSearchParams?: string[];  // Search params excluded from loaderDeps
}

ignoreSearchParams — Critical for Variants

ignoreSearchParams: ["skuId"] tells TanStack Router that ?skuId changes should NOT trigger a loader re-fetch:

loaderDeps: ({ search }) => {
  const filtered = Object.fromEntries(
    Object.entries(search ?? {}).filter(([k]) => !ignoreSet.has(k)),
  );
  return { search: Object.keys(filtered).length ? filtered : undefined };
},

The loader only sees deps.search (which excludes skuId), so it builds the CMS path without ?skuId:

loader: async ({ params, deps }) => {
  const basePath = "/" + (params._splat || "");
  const searchStr = deps.search
    ? "?" + new URLSearchParams(deps.search).toString()
    : "";
  return loadCmsPage({ data: basePath + searchStr });
},

Cache Headers — Dynamic per Page Type

headers: ({ loaderData }) => {
  const profile = loaderData?.cacheProfile ?? "listing";
  return cacheHeaders(profile);
},

The cacheProfile is determined by detectCacheProfile(basePath) inside loadCmsPage:

URL Pattern Profile Edge TTL
*/p product 5 min
/s, ?q= search 60s
/cart, /checkout private none
Everything else listing 2 min

Head/SEO

head: ({ loaderData }) => ({
  meta: [
    { title: loaderData?.pageName
        ? `${loaderData.pageName} | ${siteName}`
        : defaultTitle },
  ],
}),

Homepage Route (index.tsx)

Hardcoded to / path — no params, no deps.

Site File

// src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { cmsHomeRouteConfig } from "@decocms/start/routes";
import { DecoPageRenderer } from "@decocms/start/hooks";

const config = cmsHomeRouteConfig({
  defaultTitle: "My Store - Homepage",
});

export const Route = createFileRoute("/")({
  ...config,
  component: HomePage,
});

function HomePage() {
  const page = Route.useLoaderData();
  if (!page) {
    return <div>Loading...</div>;
  }
  return <DecoPageRenderer sections={page.resolvedSections} />;
}

cmsHomeRouteConfig Options

interface CmsHomeRouteOptions {
  defaultTitle: string;
}

Admin Protocol Routes

These routes enable the Deco CMS admin (admin.deco.cx) to communicate with the storefront:

Meta Route — Schema & Manifest

// src/routes/deco/meta.ts
import { createFileRoute } from "@tanstack/react-router";
import { decoMetaRoute } from "@decocms/start/routes";

export const Route = createFileRoute("/deco/meta")({
  ...decoMetaRoute,
});

Render Route — Section Preview

// src/routes/deco/render.ts
import { createFileRoute } from "@tanstack/react-router";
import { decoRenderRoute } from "@decocms/start/routes";

export const Route = createFileRoute("/deco/render")({
  ...decoRenderRoute,
});

Invoke Route — Loader/Action Execution

// src/routes/deco/invoke.$.ts
import { createFileRoute } from "@tanstack/react-router";
import { decoInvokeRoute } from "@decocms/start/routes";

export const Route = createFileRoute("/deco/invoke/$")({
  ...decoInvokeRoute,
});

Important: Use Spread Operator

Always use { ...frameworkRoute } — NOT createFileRoute("/path")(frameworkRoute):

// BAD — "Route cannot have both an 'id' and a 'path' option"
export const Route = createFileRoute("/deco/meta")(decoMetaRoute);

// GOOD — spread into new object
export const Route = createFileRoute("/deco/meta")({ ...decoMetaRoute });

TanStack Router injects internal properties (id, path) that conflict if the config object already has them.


Framework Exports

// @decocms/start/routes
export {
  cmsRouteConfig,        // Catch-all CMS route config factory
  cmsHomeRouteConfig,    // Homepage route config factory
  loadCmsPage,           // Server function for CMS page resolution
  loadCmsHomePage,       // Server function for homepage resolution
  type CmsRouteOptions,
  CmsPage,              // Generic CMS page component
  NotFoundPage,         // Generic 404 component
  decoMetaRoute,        // Admin meta route config
  decoRenderRoute,      // Admin render route config
  decoInvokeRoute,      // Admin invoke route config
};

Add to package.json exports:

{
  "exports": {
    "./routes": "./src/routes/index.ts"
  }
}

Common Errors

Cannot find module '@decocms/start/routes'

TypeScript server needs restart after adding new exports to package.json. In VSCode/Cursor:

  • Cmd+Shift+P → "TypeScript: Restart TS Server"
  • Or restart the dev server

Route cannot have both an 'id' and a 'path' option

Use spread: { ...decoMetaRoute } instead of direct assignment.

Property 'resolvedSections' does not exist on type 'never'

TypeScript inference limitation with createServerFn + useLoaderData(). The page could be null. Add a null check:

function CmsPage() {
  const page = Route.useLoaderData();
  if (!page) return <NotFoundPage />;
  return <DecoPageRenderer sections={page.resolvedSections} />;
}

Root Route (__root.tsx) — Keep Site-Specific

The root route contains site-specific elements that should NOT be in the framework:

  • HTML lang attribute
  • Favicon
  • CSS stylesheet imports
  • Font loading
  • Theme configuration
  • QueryClient setup

staleTime / gcTime Configuration

Production

Set by routeCacheDefaults(profile) based on page type:

Profile staleTime gcTime
static 5 min 30 min
product 5 min 30 min
listing 2 min 10 min
search 60s 5 min
private 0 0

Development

staleTime: 5_000 (5 seconds) — not zero!

With staleTime: 0, TanStack Router re-fetches on every navigation even if loaderDeps returns identical deps. This causes:

  • Double-fetch on variant changes (despite ignoreSearchParams)
  • Prefetch + click = 2 server calls

Setting 5s staleTime allows rapid interactions (variant clicks, back/forward) to use cached data while still reflecting changes within a few seconds.


Related Skills

Skill Purpose
deco-variant-selection-perf Variant selection optimization using replaceState
deco-cms-layout-caching Layout section caching in CMS resolve
deco-edge-caching Cloudflare edge caching with workerEntry
deco-tanstack-storefront-patterns General storefront patterns
deco-start-architecture Full @decocms/start architecture reference
Weekly Installs
2
First Seen
7 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2