zfb
GitHub repository

Type to search...

to open search from anywhere

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-tailwind.md
├── lib/
│   └── types.ts
├── styles/
│   └── global.css
├── mdx-components.tsx
├── zfb.config.ts
├── package.json
├── tsconfig.json
├── README.md
└── .gitignore

Every 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/blog/:slug (dynamic route — one HTML file per resolved 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/404.tsx emits a flat dist/404.html rather than dist/404/index.html, 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/default.tsx 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/callout.tsx exports one base Callout plus five named wrappers — Note, Tip, Important, Warning, and Caution. Those five names are fixed by the GitHub alerts markdown feature, which rewrites a > [!NOTE] blockquote into a <Note> element before rendering.

  • components/theme-toggle.tsx is the template's only "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/zfb-shim.d.ts is not a component. It declares the bare zfb/config specifier that zfb.config.ts imports defineConfig from, mapping it onto the real types shipped by @takazudo/zfb. The config loader aliases that specifier to an internal stub at parse time, so nothing resolves it on disk — without the shim, TypeScript and zfb check would report the import as unresolved.

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-tailwind.md — where the Tailwind setup lives and how dark mode is wired.

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.Content /> picks it up with no per-call wiring.

The template's map exports the five alert components from components/callout.tsx. 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/types.ts 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/global.css is the entry point, imported from layouts/default.tsx. It opens with @import "tailwindcss";, defines the project's theme tokens in an @theme block, binds Tailwind's dark: variant to the data-theme attribute the theme toggle writes, and closes with a scoped .prose block for rendered Markdown bodies.

The split is deliberate: page chrome in .tsx files uses Tailwind 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 template sets no tailwind key in its config — Tailwind is on unless you set tailwind: { enabled: false }. See Styling.

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/logo.svg is reachable at /logo.svg, 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 "./logo.svg") 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/logo.svg/pj/site/logo.svg. 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({
  framework: "preact",
  collections: [{ name: "blog", path: "content/blog", schema: { /* … */ } }],
  markdown: {
    gfm: { taskListItem: true, footnoteDefinition: true },
    features: { githubAlerts: true, codeEnrichment: {}, headingMarkerToc: true },
  },
});

Every option zfb already defaults to is omitted on purpose — outDir, publicDir, and tailwind are all left implicit, so the file shows only the decisions the project actually made. What remains is three top-level keys: framework ("preact"), the blog collection (with a JSON Schema that zfb check validates every post's frontmatter against), and the markdown opt-ins — GFM task lists and footnotes on top of the always-on tables and strikethrough, plus 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), framework ("preact" or "react", default "preact"), collections, tailwind, 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 the framework runtime dependency (preact by default) and four scripts: dev, build, preview, and typecheck (which runs zfb check — TypeScript plus collection-schema validation). zfb new exact-pins @takazudo/zfb and @takazudo/zfb-runtime to the version of the CLI that scaffolded the project — an exact pin like =0.1.0-next.7, not a ^/~ range — so a fresh scaffold never silently drifts to a future release.

tsconfig.json is configured so TSX across the project type-checks cleanly, with a single path alias: ~/* resolves to the project root, 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, plus the temp files zfb's pipelines synthesise while a build is in flight — including **/zfb-tailwind-entry-*.css, the entry file the Tailwind pipeline writes next to your CSS entry (see Styling).

Revision History

CreatedUpdated