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:
| File | Templatable? |
|---|---|
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:
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:
{ "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:
{ "marketing/**": template }Matches: marketing/campaigns/summer, marketing/promo/2024/special, marketing/[id]/details
Exact Match
Targets a single specific route:
{ "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:
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:
{
"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:
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:
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
generator({
templates: {
"landing/**": landingTemplate,
"marketing/**": marketingTemplate,
"promo/**": promoTemplate,
},
})Admin Interfaces
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:
generator({
templates: {
"**": myDefaultTemplate,
},
})Note this still does not reach the root index route or layout files - see What It Overrides.