Skip to content

Every frontend framework supports template overrides for specific routes through pattern-based matching. When a new page is created and its path matches a configured pattern, your custom template is written instead of the default - useful for standardizing structure across landing pages, admin tools, or any section requiring a consistent starting point.

This is the frontend half of the feature; API routes have their own. Custom Route Templates ›

What It Overrides

Only page components - and not quite all of them:

FileTemplatable?
pages/**/index.* - page components
pages/index/index.* - the root route❌ always the built-in welcome page
pages/**/layout.* - layouts❌ always the built-in layout
pages/404.*❌ deployed once at folder creation

Templates only fill blank files

Boilerplate is written into a file only when that file is empty. It never overwrites work you have already done - which is also why changing a template does not retroactively rewrite existing pages. To re-seed one, empty the file and it will be filled again.

Configuration

Pass custom templates through the frontend block in your source folder's kosmo.config.ts:

kosmo.config.ts
ts
import { defineConfig } from "@kosmojs/dev";

const landingTemplate = `
export default function Page() {
  return (
    <div class="landing-page">
      <h1>Welcome</h1>
    </div>
  );
}`;

export default defineConfig({
  frontend: {
    stack: "react",
    base: "/front",
    templates: {
      "landing/*": landingTemplate,
      "marketing/**": landingTemplate,
    },
  },
});

Pattern Syntax

Templates use glob-style patterns to match routes:

Single-Depth Wildcard (*)

Matches routes at exactly one nesting level:

ts
{ "landing/*": template }

Matches: landing/home, landing/about, landing/[slug]

Excludes: landing/features/new (too deep), landing (too shallow)

Multi-Depth Wildcard (**)

Matches routes at any nesting depth:

ts
{ "marketing/**": template }

Matches: marketing/campaigns/summer, marketing/promo/2024/special, marketing/[id]/details

Exact Match

Targets a single specific route:

ts
{ "products/list": template }

Resolution Priority

When multiple patterns match, the first matching pattern wins - in the order the keys are written, so order them most specific first:

ts
generator({
  templates: {
    "landing/home": homeTemplate,   // highest specificity
    "landing/*": landingTemplate,   // medium specificity
    "**": fallbackTemplate,         // lowest specificity
  },
})

Numeric-looking patterns jump the queue

JavaScript objects order integer-like keys first, regardless of where you wrote them - so a pattern such as "2024/**" is hoisted to the front and matches before anything above it. Prefix it with ./ to keep your written order: "./2024/**". The ./ is stripped before matching.

The same applies to renderMode, which uses the same resolver.

Parameter Compatibility

Route parameters are escaped before matching, so [id], {id} and {...path} mean themselves rather than glob syntax:

ts
{
  "users/[id]": userTemplate,           // required parameter
  "products/{category}": productTemplate, // optional parameter
  "docs/{...path}": docsTemplate,        // splat parameter
  "shop/[category]/{sub}": shopTemplate, // combined
}

Template Format

Templates are written to disk as the page component file. A template can be either a plain string, or a function receiving the resolved route and returning the string - useful when the output depends on the route itself:

ts
templates: {
  // a plain string
  "landing/*": landingTemplate,

  // or a function of the route
  "admin/**": (route) => `
export default function Page() {
  return <h1>${route.name}</h1>;
}`,
}

Each framework has its own component structure:

ts
const customTemplate = `
import { useParams } from "react-router";

export default function Page() {
  const params = useParams();

  return (
    <div>
      <h1>Custom Template</h1>
      <p>Route params: {JSON.stringify(params)}</p>
    </div>
  );
}
`;

Templates use Handlebars syntax for any dynamic content injected during seeding. Avoid raw Vue interpolation {{}} inside template strings - wrap in quotes or escape as needed to prevent accidental Handlebars evaluation.

Common Use Cases

Landing & Marketing Pages

ts
generator({
  templates: {
    "landing/**": landingTemplate,
    "marketing/**": marketingTemplate,
    "promo/**": promoTemplate,
  },
})

Admin Interfaces

ts
generator({
  templates: {
    "admin/**": adminTemplate,
  },
})

Default Template Override

Routes without a matching pattern use the built-in default, which displays the route name as a placeholder. Replace it globally with:

ts
generator({
  templates: {
    "**": myDefaultTemplate,
  },
})

Note this still does not reach the root index route or layout files - see What It Overrides.

Released under the MIT License.