Kamado is an extremely simple static site build tool. No hydration, no client-side runtime, no magic. No runtime needed, just the file system and raw HTML. Baked on demand. Thoroughly baked in a Kamado, that is what Kamado is.
Kamado is a static site build tool similar to 11ty, but aims for a simpler design. It is a tool for those who stick to the legacy, old-school ways of building.
The biggest feature of Kamado is that it requires absolutely no runtime. No client-side runtime (hydration) is needed. Because it generates pure static HTML, it achieves persistence and robustness. It generates HTML that will work just the same 10 years, or even 20 years from now.
Modern frameworks like Astro or Next.js require a runtime. Kamado does not depend on a runtime and generates pure static HTML. It is a tool for developers who prefer legacy approaches and do not want to depend on a runtime.
The biggest feature of Kamado is that it requires absolutely no runtime. No client-side runtime (hydration) is needed. Only pure static HTML is generated. This ensures persistence and robustness. You won't be troubled by runtime version upgrades or security patches.
Leave CSS and JavaScript to esbuild or vite, and Kamado will focus on managing HTML. This allows development that leverages the strengths of each tool.
The development server builds only the necessary files when they are accessed. With the transpile-on-demand method, it works comfortably even on sites with 10,000 pages. A lean design that bakes only what is needed.
Mapping management via a page tree allows for efficient builds even on large-scale sites.
Kamado adopts parallel build processing. What is happening during the build is clearly output to the console. You can check the build status of each file in real-time, and progress is obvious at a glance. Parallel processing also improves build speed.
Fire up the Kamado with Hono 🔥
If a server request matches a destination path, it builds starting from the requested file in a chain reaction. There is no need to watch dependency files; only the necessary files are automatically built.
It doesn't use Chokidar and doesn't do live reload. During development, only server requests from browser reloads trigger builds.
The page tree holds the source file paths and destination paths. Since mapping is managed at this point, if a server request matches a destination path, only the source file needs to be built.
npm install kamado
# or
yarn add kamadoCreate kamado.config.ts in the project root:
import path from 'node:path';
import { defineConfig } from 'kamado/config';
import { createPageCompiler } from '@kamado-io/page-compiler';
import { createScriptCompiler } from '@kamado-io/script-compiler';
import { createStyleCompiler } from '@kamado-io/style-compiler';
export default defineConfig({
dir: {
root: import.meta.dirname,
input: path.resolve(import.meta.dirname, '__assets', 'htdocs'),
output: path.resolve(import.meta.dirname, 'htdocs'),
},
devServer: {
open: true,
port: 8000,
},
compilers: (def) => [
def(createPageCompiler(), {
files: '**/*.{html,pug}',
outputExtension: '.html',
globalData: {
dir: path.resolve(import.meta.dirname, '__assets', '_libs', 'data'),
},
layouts: {
dir: path.resolve(import.meta.dirname, '__assets', '_libs', 'layouts'),
},
// Transform pipeline (optional, defaults to createDefaultPageTransforms())
// See @kamado-io/page-compiler documentation for customization
}),
def(createStyleCompiler(), {
files: '**/*.{css,scss,sass}',
ignore: '**/*.{scss,sass}',
outputExtension: '.css',
alias: {
'@': path.resolve(import.meta.dirname, '__assets', '_libs'),
},
}),
def(createScriptCompiler(), {
files: '**/*.{js,ts,jsx,tsx,mjs,cjs}',
outputExtension: '.js',
minifier: true,
alias: {
'@': path.resolve(import.meta.dirname, '__assets', '_libs'),
},
}),
],
async onBeforeBuild(context) {
// Process before build
// context.mode is available: 'build' or 'serve'
},
async onAfterBuild(context) {
// Process after build
// context.mode is available: 'build' or 'serve'
},
});dir.root: Project root directorydir.input: Source file directorydir.output: Output directory
devServer.port: Server port number (default:3000)devServer.host: Server host name (default:localhost)devServer.open: Whether to automatically open the browser on startup (default:false)devServer.startPath: Custom path to open in the browser when starting the server (optional, e.g.,'__tmpl/')devServer.transforms: Array of response transformation functions that modify responses during development (optional, see Response Transform API)devServer.proxy: Proxy rules for forwarding requests to external servers during development (optional, see Proxy API)
The compilers option uses a callback form for type-safe compiler configuration. The callback receives a def helper function that binds compiler factories to their options. Each def(factory(), options) call returns a compiler with metadata. The compiler options include:
files(optional): Glob pattern for files to compile. Patterns are resolved relative todir.input. Default values are provided by each compiler (see below).ignore(optional): Glob pattern for files to exclude from compilation. Patterns are resolved relative todir.input. For example,'**/*.scss'will ignore all.scssfiles in the input directory and subdirectories.outputExtension(optional): Output file extension (e.g.,.html,.css,.js,.php). Default values are provided by each compiler (see below).- Other compiler-specific options (see each compiler's documentation below)
The order of entries in the returned array determines the processing order.
files(optional): Glob pattern for files to compile. Patterns are resolved relative todir.input(default:'**/*.html')ignore(optional): Glob pattern for files to exclude from compilation. Patterns are resolved relative todir.input. For example,'**/*.tmp'will ignore all.tmpfiles.outputExtension(optional): Output file extension (default:'.html')globalData.dir: Global data file directoryglobalData.data: Additional global datalayouts.dir: Layout file directorycompileHooks: Compilation hooks for customizing compile process (required for Pug templates)transforms: Array of transform functions to apply to compiled HTML. If omitted, usescreateDefaultPageTransforms(). See @kamado-io/page-compiler for details on the Transform Pipeline API.
Note: page-compiler is a generic container compiler and does not compile Pug templates by default. To use Pug templates, install @kamado-io/pug-compiler and configure compileHooks. See @kamado-io/pug-compiler README for details.
Example: To compile .pug files to .html:
def(createPageCompiler(), {
files: '**/*.pug',
outputExtension: '.html',
compileHooks: {
main: {
compiler: compilePug(),
},
},
});files(optional): Glob pattern for files to compile. Patterns are resolved relative todir.input(default:'**/*.css')ignore(optional): Glob pattern for files to exclude from compilation. Patterns are resolved relative todir.input. For example,'**/*.{scss,sass}'will ignore all.scssand.sassfiles.outputExtension(optional): Output file extension (default:'.css')alias: Path alias map (used in PostCSS@import)banner: Banner configuration (can specify CreateBanner function or string)
Example: To compile .scss files to .css while ignoring source files:
def(createStyleCompiler(), {
files: '**/*.{css,scss,sass}',
ignore: '**/*.{scss,sass}',
outputExtension: '.css',
alias: {
'@': path.resolve(import.meta.dirname, '__assets', '_libs'),
},
});files(optional): Glob pattern for files to compile. Patterns are resolved relative todir.input(default:'**/*.{js,ts,jsx,tsx,mjs,cjs}')ignore(optional): Glob pattern for files to exclude from compilation. Patterns are resolved relative todir.input. For example,'**/*.test.ts'will ignore all test files.outputExtension(optional): Output file extension (default:'.js')alias: Path alias map (esbuild alias)minifier: Whether to enable minificationbanner: Banner configuration (can specify CreateBanner function or string)
Example: To compile TypeScript files to JavaScript:
def(createScriptCompiler(), {
files: '**/*.{js,ts,jsx,tsx}',
outputExtension: '.js',
minifier: true,
alias: {
'@': path.resolve(import.meta.dirname, '__assets', '_libs'),
},
});The pageList option allows you to customize the page list used for navigation, breadcrumbs, and other features that require a list of pages.
import { defineConfig } from 'kamado/config';
import { urlToFile, getFile } from 'kamado/files';
export default defineConfig({
// ... other config
pageList: async (pageAssetFiles, config) => {
// Filter pages (e.g., exclude drafts)
const filtered = pageAssetFiles.filter((page) => !page.url.includes('/drafts/'));
// Add external pages with custom metadata
const externalPage = {
...urlToFile('/external-page/', {
inputDir: config.dir.input,
outputDir: config.dir.output,
outputExtension: '.html',
}),
metaData: { title: 'External Page Title' },
};
return [...filtered, externalPage];
},
});The function receives:
pageAssetFiles: Array of all page files found in the file systemconfig: The full configuration object
Returns an array of PageData objects (extends CompilableFile with optional metaData).
Note about metaData and titles:
- During individual page compilation,
metaDatais automatically populated from frontmatter - However, at
pageListhook time (globalData collection),metaDatais NOT yet populated - If you need titles for breadcrumbs/navigation, you must explicitly set
metaData.titlein thepageListhook - Without explicit
metaData.title, breadcrumbs and navigation will show__NO_TITLE__
onBeforeBuild: Function executed before build. ReceivesContext(which extendsConfigwithmode: 'build' | 'serve')onAfterBuild: Function executed after build. ReceivesContext(which extendsConfigwithmode: 'build' | 'serve')
The Response Transform API allows you to modify response content during development server mode. This is useful for injecting scripts, implementing pseudo-SSI, adding meta tags, or any other response transformation needs.
Important Distinction:
Both use the same Transform interface (kamado/config), but differ in scope and application:
devServer.transforms: Applied to ALL responses during development server mode only (kamado server). Middleware-style transforms that can process any file type (HTML, CSS, JS, images, etc.). Thefilteroption (include/exclude) is respected here. Does not run during builds.createPageCompiler()({ transforms }): Applied to compiled HTML pages in both build and serve modes. Transform pipeline for HTML processing only. Thefilteroption is ignored (all HTML pages are processed). See @kamado-io/page-compiler for details.
You can reuse the same transform functions (like manipulateDOM(), prettier(), or custom transforms) in both places.
Key Features:
- Development-only: Transforms only apply in
servemode, not during builds - Flexible filtering: Filter by glob patterns (include/exclude)
- Error resilient: Errors in transform functions don't break the server
- Async support: Supports both synchronous and asynchronous transform functions
- Chainable: Multiple transforms are applied in array order
Configuration:
import path from 'node:path';
import fs from 'node:fs/promises';
import { defineConfig } from 'kamado/config';
export default defineConfig({
devServer: {
port: 3000,
transforms: [
// Example 1: Inject development script into HTML
{
name: 'inject-dev-script',
filter: {
include: '**/*.html',
},
transform: (content) => {
if (typeof content !== 'string') {
const decoder = new TextDecoder('utf-8');
content = decoder.decode(content);
}
return content.replace(
'</body>',
'<script src="/__dev-tools.js"></script></body>',
);
},
},
// Example 2: Implement pseudo-SSI (Server Side Includes)
{
name: 'pseudo-ssi',
filter: {
include: '**/*.html',
},
transform: async (content, ctx) => {
if (typeof content !== 'string') {
const decoder = new TextDecoder('utf-8');
content = decoder.decode(content);
}
// Process <!--#include virtual="/path/to/file.html" -->
const includeRegex = /<!--#include virtual="([^"]+)" -->/g;
let result = content;
for (const match of content.matchAll(includeRegex)) {
const includePath = match[1];
const filePath = path.resolve(
ctx.context.dir.output,
includePath.replace(/^\//, ''),
);
try {
const includeContent = await fs.readFile(filePath, 'utf-8');
result = result.replace(match[0], includeContent);
} catch (error) {
console.warn(`Failed to include ${includePath}:`, error);
}
}
return result;
},
},
// Example 3: Add source comment to CSS files
{
name: 'css-source-comment',
filter: {
include: '**/*.css',
},
transform: (content, ctx) => {
if (typeof content !== 'string') {
const decoder = new TextDecoder('utf-8');
content = decoder.decode(content);
}
const source = ctx.inputPath || ctx.outputPath;
return `/* Generated from: ${source} */\n${content}`;
},
},
],
},
});Transform Interface:
interface Transform<M extends MetaData> {
readonly name: string; // Transform name for debugging
readonly filter?: {
readonly include?: string | readonly string[]; // Glob patterns to include
readonly exclude?: string | readonly string[]; // Glob patterns to exclude
};
readonly transform: (
content: string | ArrayBuffer,
context: TransformContext<M>,
) => Promise<string | ArrayBuffer> | string | ArrayBuffer;
}
interface TransformContext<M extends MetaData> {
readonly path: string; // Request path
readonly filePath: string; // File path (alias for path)
readonly inputPath?: string; // Original input file path (if available)
readonly outputPath: string; // Output file path
readonly outputDir: string; // Output directory path
readonly isServe: boolean; // Always true in dev server
readonly context: Context<M>; // Full execution context
readonly compile: CompileFunction; // Function to compile other files
}Filter Options:
include: Glob pattern(s) to match request paths (e.g.,'**/*.html',['**/*.css', '**/*.js'])exclude: Glob pattern(s) to exclude (e.g.,'**/_*.html'to skip files starting with_)
Important Notes:
- Transform functions receive either
stringorArrayBuffer. For text-based transformations, decodeArrayBufferusingTextDecoder:if (typeof content !== 'string') { const decoder = new TextDecoder('utf-8'); content = decoder.decode(content); }
- Static files (non-compiled files) are typically passed as
ArrayBuffer, so always decode them if you need to process as text - Errors in transform functions are logged but don't break the server (original content is returned)
- Transforms are executed in array order
- Only applied in development server mode (
kamado server), not during builds
The Proxy API allows you to forward requests to external servers during development. This is useful when your static site makes AJAX requests to APIs on different domains, avoiding CORS issues during local development.
Key Features:
- Development-only: Proxy only applies in
servemode, not during builds - All HTTP methods: Supports GET, POST, PUT, DELETE, PATCH, and other methods
- Streaming: Responses are streamed without buffering
- Path rewriting: Optionally rewrite request paths before forwarding
- Simple and advanced forms: Use a string shorthand for simple cases or an object for full control
Configuration:
import { defineConfig } from 'kamado/config';
export default defineConfig({
devServer: {
port: 3000,
proxy: {
// Simple: string shorthand — forward /api/* to the target
'/api': 'https://backend.example.com',
// Advanced: object form with path rewriting
'/api/v2': {
target: 'https://api-v2.example.com',
// Rewrite /api/v2/users → /users
pathRewrite: (path) => path.replace(/^\/api\/v2/, ''),
changeOrigin: true,
},
},
},
});With the configuration above:
GET /api/data→GET https://backend.example.com/api/dataPOST /api/v2/users→POST https://api-v2.example.com/users(path rewritten)
ProxyRule Interface:
interface ProxyRule {
target: string; // Target URL to proxy to
pathRewrite?: (path: string) => string | Promise<string>; // Rewrite path before proxying
changeOrigin?: boolean; // Change Origin/Host headers to match target (default: false)
}Proxy Configuration:
The proxy option is a record where:
- Key: Path prefix to match (e.g.,
'/api') - Value: A target URL string (shorthand) or a
ProxyRuleobject
Important Notes:
- Proxy routes are matched before file-serving routes, so proxy paths take priority over local files
- Longer path prefixes are matched first (e.g.,
/api/v2takes priority over/api) - Query strings are preserved and forwarded to the target
- Request headers are forwarded. Set
changeOrigin: trueto rewriteHostandOriginheaders to match the target (useful when the target server validates theHostheader) - On proxy failure, a
502 Bad Gatewayresponse is returned - Only applied in development server mode (
kamado server), not during builds
kamado buildkamado build "path/to/file.pug" # Build a specific file
kamado build "path/to/*.css" # Build only CSS files
kamado build "path/to/*.ts" # Build only TypeScript fileskamado serverWhen the development server starts, pages accessed via the browser are built on demand. If there is a request, it bakes it on the spot and returns it.
The following options are available for all commands:
| Option | Short | Description |
|---|---|---|
--config <path> |
-c |
Path to a specific config file. If not specified, Kamado searches for kamado.config.js, kamado.config.ts, etc. |
--verbose |
Enable verbose logging |
# Use a specific config file
kamado build --config ./custom.config.ts
kamado server -c ./dev.config.js
# Enable verbose logging during build
kamado build --verboseKamado's core types accept a generic type parameter M extends MetaData for type-safe custom metadata. This section explains how to use it.
Most user-facing types (Config, Context, UserConfig, Transform, TransformContext, PageData, GlobalData) have a default of = MetaData. If you don't need custom metadata, you can use the types without a type argument:
import type { Config, Transform, PageData } from 'kamado/config';
// No type argument needed — defaults to MetaData
const config: Config = {
/* ... */
};
const transform: Transform = {
/* ... */
};The base MetaData interface is an empty interface ({}). Any interface or type satisfies the extends MetaData constraint. You can define custom metadata properties via generics.
To propagate custom metadata types throughout your project, pass a type argument to defineConfig:
interface MyMeta {
title: string;
description?: string;
draft?: boolean;
}
export default defineConfig<MyMeta>({
pageList: async (pageAssetFiles, config) => {
// pageAssetFiles are CompilableFile[], return PageData<MyMeta>[]
return pageAssetFiles.map((file) => ({
...file,
metaData: { title: 'Default Title' },
}));
},
async onBeforeBuild(context) {
// context is Context<MyMeta> — fully typed
},
});Note:
Config<M>is invariant inM.Due to TypeScript's type system,
Config<M>is invariant in its type parameterM. This meansConfig<PageMetaData>cannot be assigned toConfig<MetaData>(or vice versa), becauseMappears in both covariant positions (return types likePageData<M>[]) and contravariant positions (callback parameters likeconfig: Config<M>).If you write a helper function that receives a
Config, make it generic:// ✅ Good — works with any metadata type function helper<M extends MetaData>(config: Config<M>) { ... } // ❌ Bad — Config<PageMetaData> is NOT assignable to Config<MetaData> function helper(config: Config<MetaData>) { ... }
The compilers option uses a callback form: compilers: (def) => [...]. The def parameter is a CompilerDefine<M> function that binds a compiler factory to its options. This exists so TypeScript can automatically infer each compiler's option types — you never need to write type arguments manually:
compilers: (def) => [
// TypeScript infers the options type from createPageCompiler's return type
def(createPageCompiler(), {
files: '**/*.html',
outputExtension: '.html',
}),
];The type parameter M flows through the system:
defineConfig<M>() → Config<M> → Context<M> → TransformContext<M>
→ PageData<M>
→ CompileData<M> → NavNode<M>
This ensures that custom metadata types are consistent across configuration, compilation, and template data.
