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 - render full documents via prerenderToNodeStream() or use CSS selector injectiongetStaticPaths 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 "react";
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}`)];
}
use())import { use } from "react";
const cache = new Map();
function fetchJson(url: string) {
if (!cache.has(url)) {
cache.set(
url,
fetch(url).then((r) => r.json()),
);
}
return cache.get(url);
}
export function useFetch<T>(url: string): T {
return use(fetchJson(url));
}
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.
React can render full HTML documents including <head>:
// main.server.tsx
import { prerenderToNodeStream } from "react-dom/static";
export async function render(url: string) {
return {
"#app": prerenderToNodeStream(<App path={url} />),
};
}
Return an object with CSS selectors as keys:
export async function render(url: string) {
return {
"#app": renderToString(<App path={url} />),
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/react";
import react from "@vitejs/plugin-react";
export default {
plugins: [
react(),
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