with one click
capri
// Capri static site generator with island architecture. Use when creating components, pages, configuring hydration, fetching data, or working with this Capri project.
// Capri static site generator with island architecture. Use when creating components, pages, configuring hydration, fetching data, or working with this Capri project.
| name | capri |
| description | Capri static site generator with island architecture. Use when creating components, pages, configuring hydration, fetching data, or working with this Capri project. |
Capri is a static site generator with island architecture. Only components with .island.tsx suffix ship JavaScript to the browser. Everything else renders to static HTML with zero JS.
getStaticProps - fetch data in main.server.tsx or use useFetch in components<Head> component - use CSS selector injection in the render functiongetStaticPaths exists but lives in main.server.tsx.island.tsx file suffix instead of client:* directivesexport const options = { loading: "visible" }āāā main.tsx # Client entry - hydrates islands
āāā main.server.tsx # SSR entry - renders static HTML, exports getStaticPaths
āāā router.tsx # File-based routing logic
āāā pages/ # Page components (auto-discovered)
ā āāā root.tsx # Home page (/)
āāā components/
ā āāā islands/ # Interactive components (.island.tsx)
ā āāā ui/ # Static components (no JS shipped)
āāā index.html # HTML template
āāā vite.config.ts # Capri plugin config
Any component with .island.tsx suffix becomes interactive:
// components/islands/counter.island.tsx
import { useState } from "preact/hooks";
export default function Counter({ start = 0 }) {
const [count, setCount] = useState(start);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
Control when islands hydrate by exporting an options object:
export const options = {
loading: "visible", // "eager" (default) | "idle" | "visible"
media: "(max-width: 768px)", // Only hydrate when media query matches
};
Fetch data at the top level of main.server.tsx:
// main.server.tsx
const posts = await fetchPosts(); // Runs once at build time
export async function getStaticPaths() {
return ["/", ...posts.map((p) => `/blog/${p.slug}`)];
}
Preact uses the throw-promise pattern for Suspense:
const cache = new Map();
export function useFetch<T>(url: string): T {
if (!cache.has(url)) {
cache.set(
url,
fetch(url).then((r) => r.json()),
);
}
const result = cache.get(url);
if (result instanceof Promise) throw result; // Suspense catches this
return result;
}
Files in pages/ map to routes:
pages/root.tsx ā /pages/about.tsx ā /aboutpages/blog/post.tsx ā /blog/postExport getStaticPaths from main.server.tsx:
export async function getStaticPaths() {
const posts = await fetchAllPosts();
return ["/", "/about", ...posts.map((p) => `/blog/${p.slug}`)];
}
The router in router.tsx handles URL matching.
// main.server.tsx
import { prerender } from "preact-iso";
export async function render(url: string) {
const { html } = await prerender(<App path={url} />);
return { "#app": html };
}
Return an object with CSS selectors as keys:
export async function render(url: string) {
const { html } = await prerender(<App path={url} />);
return {
"#app": html,
title: getPageTitle(url),
"meta[name=description]": { content: getPageDescription(url) },
};
}
| Task | How |
|---|---|
| Add a page | Create pages/name.tsx |
| Add an island | Create components/islands/name.island.tsx |
| Add static component | Create components/ui/name.tsx (no .island suffix) |
| Lazy-load island | Add export const options = { loading: "visible" } |
| Mobile-only island | Add export const options = { media: "(max-width: 768px)" } |
| Add dynamic routes | Export getStaticPaths() from main.server.tsx |
npm run dev # Start dev server
npm run build # Build static site (vite build && vite build --ssr)
npm run preview # Preview production build
import capri from "@capri-js/preact";
import preact from "@preact/preset-vite";
export default {
plugins: [
preact(),
capri({
prerender: ["/", "/about"], // Explicit paths to render
followLinks: true, // Auto-discover by crawling links
spa: "/preview", // Client-only SPA routes
islandGlobPattern: "**/*.island.*",
sitemap: { origin: "https://example.com" },
}),
],
};
main.server.tsx - SSR render function, getStaticPathsmain.tsx - Client hydration entryrouter.tsx - URL to component mappingvite.config.ts - Build configurationcomponents/async/use-fetch.ts - Data fetching utility