Project structure
A tour of every file and directory the default template lays down.
When you run zfb new my-site, the basic-blog template lays down a small but complete project. Knowing what each directory is for makes the rest of the docs much easier to skim.
my-site/
├── pages/
│ ├── index.tsx
│ ├── about.tsx
│ ├── 404.tsx
│ └── blog/
│ └── [slug].tsx
├── layouts/
│ └── default.tsx
├── components/
│ ├── callout.tsx
│ ├── theme-toggle.tsx
│ └── zfb-shim.d.ts
├── content/
│ └── blog/
│ ├── hello-zfb.mdx
│ ├── markdown-showcase.md
│ └── styling-with-zudo-wind.md
├── lib/
│ └── types.ts
├── styles/
│ └── global.css
├── mdx-components.tsx
├── zfb.config.ts
├── package.json
├── tsconfig.json
├── README.md
└── .gitignoreEvery file above is meant to be read and edited — the template has no hidden layer and no generated code you are expected to leave alone.
pages/
File-system routing lives here. Every page file under pages/ (.tsx, .ts, .jsx, .js, .mdx, .md, or .html) becomes a route — see Routing for the full contract:
pages/→index. tsx /pages/→about. tsx /about pages/blog/[slug].tsx→/(dynamic route — one HTML file per resolved slug)blog/ : slug pages/docs/[...slug].tsx→ catchall, matches any depth
The template ships three static routes and one dynamic route. index.tsx lists every post newest-first via getStaticProps(), about.tsx is a static page with no data loading at all, and blog/[slug].tsx exports a paths() function that zfb calls at build time to expand [slug] into one HTML file per collection entry. See Dynamic routes for the full API.
404.tsx is the one filename with a special meaning: a top-level pages/ emits a flat dist/ rather than dist/, which is the file zfb preview — and most static hosts — serve for an unmatched request.
The template deliberately stops at one dynamic route. Paginated listings and per-tag archives are a few lines each on top of the same paths() contract, so README.md links them as next steps instead: paginate() turns a collection into one route per page, and a pages/tags/[tag].tsx route groups posts by the tags already present in their frontmatter.
layouts/
Reusable page wrappers. layouts/ is the shell every page imports — <head>, header, footer, and an inline pre-paint script that applies the saved theme before the first frame. Layouts are plain TSX components; you compose them however you like.
components/
Plain components and islands. The template ships two components and one type declaration:
components/exports one basecallout. tsx Calloutplus five named wrappers —Note,Tip,Important,Warning, andCaution. Those five names are fixed by the GitHub alerts markdown feature, which rewrites a> [!NOTE]blockquote into a<Note>element before rendering.components/is the template's onlytheme- toggle. tsx "use client"island — it toggles dark mode and is the only component that ships JavaScript to the browser. The"use client"directive at the top of a file is what turns a component into a client-side island; components without it render only on the server. The full mental model is on the islands page.components/is not a component. It declares the supported barezfb- shim. d. ts zfbimport family for TypeScript by re-exporting each corresponding public entry from@takazudo/zfb. The build tools resolve those aliases across SSR, island scanning, client bundles, JSX pragma classification, and zudo-wind class extraction; the config loader separately mapszfb/configto an internal runtime stub. The build aliases do not configure TypeScript, andzfb checkdoes not injectcompilerOptions.paths; editors andzfb checkresolve bare specifiers through this shim, which the project'stsconfigincludes. The stock config includescomponents/; if you already have a shim, extend it with any missing module declarations. See Typing barezfbimports for the complete declaration file.
content/
Content collections. content/blog/ holds the three seed entries of a blog collection: Markdown and MDX files in a named directory, queryable from pages via getCollection("blog"):
hello-zfb.mdx— how a page becomes HTML, and what MDX adds over plain Markdown.markdown-showcase.md— worked examples of the markdown features the template turns on (tables, strikethrough, task lists, footnotes, alerts, enriched code blocks, and the heading-marker TOC), each linked to its reference page.styling-with-zudo-wind.md— the starter's utility tokens, authored CSS, and dark-mode setup.
Collection schemas are declared in zfb.config.ts under collections. See Content Collections for the query API.
mdx-components.tsx
A project-root component map applied to every rendered content entry. zfb discovers this file next to zfb.config.ts, copies it into the build, and installs its default export before any page renders — so <entry. picks it up with no per-call wiring.
The template's map exports the five alert components from components/. That is what makes > [!NOTE] work in a plain .md post: the githubAlerts feature produces a <Note> element, and an unresolved PascalCase name throws while rendering. Merge order is defaultComponents → this file → the components prop on the call site, so a single page can still override any of them locally. See MDX Components.
lib/
Shared TypeScript utilities. The template ships lib/ with the frontmatter and entry types used across pages and components. There is no framework magic here — it is a plain TypeScript module you import wherever you need it.
styles/
Global CSS. styles/ is the entry point, imported from layouts/. It contains authored CSS for the site's base styles and .prose block. The wind object in zfb.config.ts supplies the starter's utility tokens, breakpoint, reset, and data-theme dark variant; wind configuration is data rather than stylesheet directives.
The split is deliberate: page chrome in .tsx files uses zudo-wind utilities inline, while Markdown produces plain tags no utility class can reach, so those are styled once in CSS and opted into with class="prose". The starter explicitly configures wind; other projects can omit it for the empty enabled default, or set wind: false for authored CSS only. See Styling and zudo-wind configuration.
public/
Static assets served as-is from the site root. The template does not scaffold a public/ directory — create it when you need to serve static files. Put favicon.ico, robots.txt, SVGs, raster images, fonts, manifest files, or any binary you want to reference by absolute URL here. The directory does NOT appear in the URL — public/ is reachable at /, both in zfb dev and after zfb build.
Reference these files by URL from your TSX, MDX, or CSS:
<img src="/logo.svg" alt="" width={128} height={32} />
<link rel="icon" href="/favicon.ico" />Do NOT use bundler-style imports (import logo from ".) for static assets — zfb does not run an asset pipeline over public/. The directory is a verbatim mirror; files come out at the URL that matches their relative path.
When base is set in zfb.config.ts (e.g. base: "/pj/site/"), files in public/ are served under that prefix too — public/ → /. The same prepend happens at build time, so the prefix is consistent between dev and prod.
See Static Assets for the full reference, including when to use public/ vs a TSX import for islands.
zfb.config.ts
The template scaffolds a TypeScript config that wraps its object in defineConfig(), so you get full type checking and IDE completion:
import { defineConfig } from "zfb/config";
export default defineConfig({
wind: {
spec: 1,
reset: "owned-v1",
tokens: {
spacingUnit: "0.25rem",
colors: { accent: "var(--color-accent)" },
sizes: { "2xl": "42rem" },
// The starter supplies the rest of its explicit palette and scales here.
},
breakpoints: { sm: { minWidthPx: 640 } },
dark: { attribute: "data-theme", value: "dark" },
authoredClasses: { prose: true },
},
collections: [{ name: "blog", path: "content/blog", schema: { /* … */ } }],
markdown: {
gfm: { taskListItem: true, footnoteDefinition: true },
features: { githubAlerts: true, codeEnrichment: {}, headingMarkerToc: true },
},
});This shortened example shows the starter's explicit wind settings alongside its blog collection and markdown opt-ins. outDir and publicDir use their defaults; the full starter palette and scales are declared in its config. GFM task lists and footnotes are enabled on top of the always-on tables and strikethrough, along with GitHub alerts, code enrichment, and the heading-marker TOC. The markdown features index is the full map of what else you can turn on.
The config file uses a camelCase schema. Common keys include outDir (default "dist"), publicDir (default "public"), host (default "localhost"), port (default 3000, the zfb dev server default — zfb preview falls back to 4321 instead when unset), wind, collections, and plugins. This is not an exhaustive list — see defineConfig for the full schema reference.
outDir is honored by zfb dev, zfb build, and zfb preview. For build and preview, CLI --outdir overrides config outDir; the built-in fallback is dist/.
A plain zfb.config.json is also accepted as a legacy fallback — the schema is identical, but it gets no types and no defineConfig(). When both files are present, the .ts one wins.
package.json, tsconfig.json, README.md, .gitignore
Standard project plumbing. package.json declares project dependencies and four scripts: dev, build, preview, and typecheck (which runs zfb check — TypeScript plus collection-schema validation). The owned JSX runtime is a subpath of @takazudo/zfb, so it does not require a separate framework package. zfb new pins its zfb packages to the version of the CLI that scaffolded the project, so a fresh scaffold does not silently drift to a future release.
tsconfig.json sets jsx and jsxImportSource so TypeScript checks JSX against the same runtime that zfb uses. zfb build and zfb dev already compile page, component, and island JSX with the automatic transform and @takazudo/, unless a file's @jsxImportSource pragma overrides it; they do not read those two tsconfig settings. The config also defines one path alias: ~/* resolves to the project root. Use imports such as ~/lib/types for root-relative files; bare imports such as lib/types are not root-relative. This alias works without baseUrl, and the template uses it consistently in every import.
README.md is a short tour of the same tree, plus two tables listing which markdown features are on in the starter and which are available but left off, each linked to its reference page.
The shipped .gitignore excludes dist/ and node_modules/ so build output and dependencies stay out of version control. It also covers temporary files staged while zfb bundles islands, plugins, and module workers; those files are cleaned up after a run and ignored in case a process is interrupted.