zigapagos

Rich interfaces.
Simple output.

Build with HTML, CSS, and TSX. Zigapagos turns pages, interactive components, and application routes into static files — fast. Deliver the interaction you need, with control over the JavaScript you ship.

Build a site or app See what ships

That button is a real island on this page. It was server-rendered into the HTML you received, then hydrated when it scrolled into view. View source — there is no framework runtime wrapping the rest of the page.

Zero JS by default

A page with no islands ships no framework JavaScript.

0 KB of framework runtime on a page with no islands — every docs page on this site
1 shared runtime, however many islands a page has
0 required CSS frameworks or design systems

Two honest exceptions, both of which devtools will show you. This page is a demo, so it ships the shared runtime plus the islands you can click. And every page — including the docs — carries about 1 KB of inline script for the theme toggle (closer to 550 bytes over the wire once gzipped), which is exactly why the toggle is inline script and not an island: making it a component would have put a framework runtime on every page of a generator whose pitch is that most pages need none. Open the network tab on any docs page and that is all you will find.

One binary and Bun

From source to production files, fast

content/*.smd    ──►  native core  ──┐
                    (SuperMD/SuperHTML)  │
                                         ├──►  static site
components/*.island.tsx ──►  Bun SSR  ───┤     + import map
                        └──►  bundle  ───┘     + one runtime

The native core generates your content and templates. Bun renders and bundles TSX at build time. You get a production output directory without maintaining a separate JavaScript bundler configuration. Building sites needs no Zig compiler; the shell installer supplies the toolchain.

Production build speed is a product priority. Measure your own project: content, images, type checking, and component code all affect the work. The backlog requires reproducible evidence for performance improvements.

How the build works →

Static files to deploy

Build once. Serve the output.

Your release is HTML, CSS, JavaScript where needed, and assets. Upload it to a compatible static host. The frontend needs no production Node or Bun process, and your source toolchain stays on the build machine.

For SPAs, configure the host's fallback routes. Zigapagos generates routing manifests for ZigBase, Nginx, and Apache, plus CSP and caching configuration. Apply the configuration for your host and verify deep links.

Static frontend output works with dynamic application data: connect to your existing backend API, or serve it alongside ZigBase on one origin. Client route guards guide navigation; the backend enforces authorization.

Plan your deployment →

zigapagos release --output=public

public/
├── index.html
├── styles.css
├── images/
└── …

# With interactive components:
# + JavaScript bundles and shared runtime
# + routing and security configuration

# Deploy the generated directory.

Familiar materials

Your markup. Your styles. Your components.

Work close to the web

HTML templates, CSS stylesheets, and Preact-compatible TSX components. SuperHTML adds template directives; SuperMD and Ziggy handle content and configuration. Those conventions are explicit and documented.

Explore the source model →

Bring your design

Write plain CSS, use a CSS framework's generated output, or build on your own design system. Zigapagos imposes no visual theme or component library. Keep any stylesheet compiler in your own build workflow.

Choose your styling workflow →

Choose the interaction

Use HTML for static pages, islands for individual interactions, and native SPA routes for application screens. Share one runtime across islands and choose when each component hydrates.

Build application routes →

Islands in 30 seconds

Write a component. Say when it should wake up.

import { useState } from "@z/runtime";

export interface Props { start?: number }

export default function Counter({ start = 0 }: Props) {
  const [n, setN] = useState(start);
  return <button onClick={() => setN(n + 1)}>Clicked {n} times</button>;
}

An island is a TSX component with a client: directive. Zigapagos renders it to HTML at build time so the page is complete without JavaScript, then ships just that component's code plus one shared runtime.

Props are typechecked at build time against the component's exported Props type. A misspelled prop fails the build rather than rendering undefined in production.

Islands reference →

Native SPAs

Give your application routes, layouts, and guarded navigation

Export a route table from a .spa.tsx and Zigapagos prerenders a real HTML shell for every static route, then hands over to a client-side router. Nested layouts, route guards, code-split routes and declarative redirects are part of the route table, not a library you add.

The shells are static files, so a guarded route still deploys to a CDN — the guard runs in the browser after the shell is served.

Navigation is tuned by default: a <Link> to a code-split route starts fetching that route's chunk on hover, focus or touch, so the click usually finds it already loaded — prefetch="viewport" and prefetch={false} adjust it per link, and a browser's data-saver signal turns it off. Add viewTransitions: true to the spa export and every soft navigation crossfades instead of flipping — feature-detected, off by default, no other code. Content pages get the declarative counterpart: .speculation_rules = true on the site injects a browser-native speculation-rules prefetch hint into every page, zero runtime JS.

Open the demo app

export const spa = { base: "/app" };

export const routes = [
  { path: "/", component: Home },
  { path: "/guides", component: Layout, children: [
      { path: "/:slug", component: Guide },
  ]},
  { path: "/account", component: Account,
    guard: requireSession },
];

One attribute picks the moment

Say when an island should wake up. Nothing wakes up by default.

client:load

Hydrates immediately, as soon as the runtime script runs.

client:idle

Waits for requestIdleCallback, so it never competes with the page's first paint.

client:visible

Hydrates when an IntersectionObserver reports the island entered the viewport.

client:media

Hydrates only while a matchMedia query matches — a sidebar that is never interactive on mobile ships no JS there.

client:only

Hydrates immediately, like load, but is never server-rendered — the placeholder starts empty and the component exists only in the browser.

no directive

Server-rendered HTML, no JavaScript, no runtime cost. The default for every island until you add one.

See all five directives hydrate on one page →

Images

Responsive images from a build step, not a service

Set .image_optimize = {} in the site config and every content image is resampled at build time into WebP variants at your configured widths, emitted as a <picture> with a full srcset/sizes and the untouched original as the <img> fallback. Variants are never upscaled past the source's own width, and derived files are cached, so a full rebuild after the first pays file-copy cost.

AVIF is an explicit hatch: point .image_optimize.avif_encoder at an avifenc-compatible binary you already have and every planned width gets an AVIF <source> ahead of the WebP one — Zigapagos never vendors or downloads an encoder. All of it is off by default: a site that never sets the field builds byte-identical output.

Images reference →

<picture>
  <source type="image/webp"
          srcset="cover.a1b2c3d4.480.webp 480w,
                  cover.a1b2c3d4.960.webp 960w"
          sizes="100vw">
  <img src="cover.jpg" width="1600"
       height="900" alt="…">
</picture>

Agent-ready

AI-friendly. AI not required.

Write the code yourself, work with a coding agent, or mix both. The same source, documented APIs, and local verification tools support every workflow. No AI account or service is needed to build or run your site.

Diagnostics as data

validate, doctor and explain-code take --format=json and emit NDJSON with stable ZP_* codes, so a fix loop matches on the code, never on message prose.

A dev server that detaches

zigapagos dev --background waits for the server to be ready, prints its URL, and exits; /_zigapagos/status then reports a build generation counter and pass/fail state, so an agent polls for its own edit to land instead of watching a terminal. In a recognized agent environment, dev backgrounds itself.

Context that ships

zigapagos init scaffolds AGENTS.md and CLAUDE.md into a new site, and both the Astro and Rails migrations are installable Agent Skills in the open agentskills.io format.

Machine-readable diagnostics → · Background dev server →

Zigapagos + ZigBase

Bring the whole application together

Pair Zigapagos with ZigBase for data, authentication, files, realtime, jobs, and custom backend logic. Serve the static frontend and backend API on one origin, with each project independently buildable.

Explore the pairing →

Zigapagos also works as a standalone frontend toolkit with your existing API. It is pre-1.0: test library compatibility and pin your releases. Backend capacity depends on the application and deployment.

Compare the tradeoffs → · See the feature backlog →

Migrating an existing app

A target, a handoff, and the gaps in between

zigapagos migrate <source> --target <new-site> recognizes Astro, Next.js, Gatsby, Nuxt/Vue, Hugo, Jekyll, Eleventy, Hexo, and Rails. It writes a buildable target from the deterministic parts and a MIGRATION.md that keeps everything uncertain visible instead of guessing.

Rails goes further: static route and ERB analysis produce versioned discovery and handoff manifests, operator decisions drive conversion, ZigBase operations bind forms and authentication, and generated parity runners state what the migrated presentation must preserve.

Try the migrate demo →

The Rails migration contract →

The Astro mapping →

Other framework mappings →