Every frontend framework supports template overrides for specific routes through pattern-based matching. Useful for standardizing structure across landing pages, admin tools, or any section requiring a consistent starting point.
Templates seed page index files - pages/**/index.* - and nothing else. Every other seeded file gets the built-in minimal boilerplate to start from.
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,
},
},
});Now every new route under landing/ and marketing/ will start with your template.
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.
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:
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:
// React: kosmo.config.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
frontend: {
stack: "react",
base: "/front",
templates: {
"landing/**": landingTemplate,
"marketing/**": marketingTemplate,
"promo/**": promoTemplate,
},
}Admin Interfaces
frontend: {
stack: "react",
base: "/front",
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:
templates: {
"**": myDefaultTemplate,
}