Start here Mado 0.15.3
Quickstart
Goal: have a Mado app running locally, edited from VS Code, and ready to ship to a static host.
1. Scaffold
Mado ships with a small universal starter by default. A larger modular starter remains available as an optional architecture experiment, not as a requirement for production applications.
Prerequisite: Node.js 22.12 or newer.
# Universal starter (default) — ~15 files, one shared component
npm exec --yes --package @madojs/mado@latest -- mado init my-app
# Optional modular experiment — auth shell, guarded zones, billing module
npm exec --yes --package @madojs/mado@latest -- \
mado init my-app --starter modular
cd my-app
npm install
What you get either way:
- exactly one runtime dependency:
@madojs/mado; vite.config.tswithmado()from@madojs/mado/vite;src/main.tsmounting the router into#app;src/app.routes.ts(a default export + a namedmanifestexport);- a
package.jsonwhose scripts wrap the Mado CLI.
2. The dev loop
npm run dev # Vite dev server, fast HMR for templates/styles
npm run typecheck # tsc --noEmit
npm run test # starter verification; add app-specific tests as it grows
npm run build # SPA build only (out/ without snapshots)
npm run release # typecheck + vite build + static snapshots + deploy files
npm run preview # serve out/ like a real static host
mado release is the single command you ship. It produces one folder,
out/, that you can rsync or upload to a static host after configuring its
routing policy for your static, SPA or hybrid route mix:
npm run release
rsync -avz out/ user@server:/var/www/my-app/
See 20-deployment.md for nginx / Cloudflare Pages / GitHub Pages / S3 recipes.
3. Your first page
Two primitives, that's it (see 10-pages-and-components.md for the full mental model):
page()— a route. Lives at a URL. Renders in light DOM. Hastitle/head/load/view/ optionalstatic.component()— a reusable<x-tag>. Shadow DOM by default.
// src/pages/home.page.ts
import { html, page } from "@madojs/mado";
export default page({
title: "Home",
head: () => ({ description: "Hello from Mado" }),
view: () => html`<h1>Hello, Mado</h1>`,
});
// src/app.routes.ts
import { routes } from "@madojs/mado";
export const manifest = {
"/": () => import("./pages/home.page"),
"*": () => import("./pages/not-found.page"),
};
export default routes(manifest);
Always export both default (for the router) and manifest (so
mado static can discover pages that opt into static capture).
Internal links must be base-aware:
import { routeUrl } from "@madojs/mado";
html`<a data-link href=${routeUrl("/about")}>About</a>`;
data-link lets the router intercept the click — including across
Shadow DOM. routeUrl() resolves against import.meta.env.BASE_URL,
so the same code works whether your app is hosted at / or at
/docs/.
4. IDE setup
Out of the box html and css are tagged-template strings. TypeScript
and most editors don't highlight them by default. The lit ecosystem's
tools work because Mado uses the same tag names and binding shapes.
VS Code (recommended)
Extensions → install lit-plugin (by
runem).Import
html/cssdirectly — no rename:import { html, css } from "@madojs/mado";(Optional) Recommended settings for Mado in
.vscode/settings.json:{ "lit-plugin.rules": { "no-unknown-attribute": "off", "no-incompatible-type-binding": "off" } }no-incompatible-type-binding: offis needed because Mado bindings accept signals (Signal<T>) where the plugin expects raw values.
You get: HTML/CSS highlighting inside templates, attribute/event auto-complete, go-to-definition for custom elements, typo checking.
WebStorm / IntelliJ
Native support for lit-style template literals since 2021. Nothing to install. If highlighting is missing, restart the TS server.
Neovim / Helix
npm install -g lit-html-server
Wire it into your LSP config (lit_html in lspconfig).
Optional extras
- Custom Elements Manifest for auto-complete on your own
<x-*>components:npx cem analyze --globs "src/**/*.ts". - Prettier 3+ formats
html/csstemplate literals viaembeddedLanguageFormatting: "auto"(default). - eslint-plugin-lit + eslint-plugin-wc provide template-aware lint rules. Not required.
What does NOT work today
- Type-checking of signal bindings:
<input .value=${count}>is flagged because the plugin expects a string, not aSignal<number>. Suppress with// @ts-expect-erroror the settings override above. - The
each()directive is recognised as a plain function — no special-case checking inside.
Mado works without any IDE plugin — the templates remain valid TS, they just won't be syntax-highlighted as HTML.
5. Where to go next
- 10 — Pages and components — the one rule.
- 11 — Templates and signals — binding shapes.
- 12 — Routing — manifest, layouts, guards, prefetch.
- 13 — Data —
resource()/mutation()/ auth. - 14 — Forms —
useForm()+ Shadow DOM input recipes. - 15 — Static snapshots — SEO without SSR.
- 16 — App architecture — start flat, then evaluate optional module boundaries.
- 17 — Mado UI — copy reviewed UI source into your app.