# ![Capri](logo.svg) Capri is a static site generator for React and Preact that uses island architecture: only interactive components hydrate, everything else ships as static HTML. ## Why Capri - **React/Preact everywhere**: Pages and islands are standard components. No custom syntax. - **Islands by convention**: Add `.island.tsx` to opt into hydration. - **Static-first**: Render at build time and deploy to any static host. - **Vite-native**: Fast dev server, predictable builds, easy integration. ## Quick Start ```bash # React npm create capri@latest my-site --template react # Preact npm create capri@latest my-site --template preact ``` ## Core Concepts ### Islands Components **without** `.island.tsx` render to static HTML and ship **zero JavaScript**. Components **with** `.island.tsx` hydrate on the client. ```tsx // Counter.island.tsx import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ; } ``` ### Entry Points Capri uses two entry files for a clean split between static rendering and client-side previews. The server entry builds static HTML at build time. The client entry is used in dev mode and, when connected to a CMS, enables live previews (and even in-place editing) without a server-side rendering round-trip. Previews render in the browser from the same static output. ```tsx // main.tsx import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { Router } from "./router.jsx"; ReactDOM.createRoot(document.body).render( , ); ``` ```tsx // main.server.tsx import { StrictMode } from "react"; import { prerenderToNodeStream } from "react-dom/static"; import { Router } from "./router.jsx"; export async function render(url: string) { return { body: prerenderToNodeStream( , ), }; } ``` ### Routing Your `Router` decides what to render based on the `path` prop: ```tsx export function Router({ path }: { path: string }) { if (path === "/") return ; if (path === "/about") return ; throw new Error("Not found"); } ``` A simple file-based router using `import.meta.glob()`: ```tsx const modules = import.meta.glob("./pages/**/*.tsx", { eager: true }); export function Router({ path }: { path: string }) { const normalizedPath = path === "/" ? "/root" : path; const module = modules[`./pages${normalizedPath}.tsx`]; if (!module) throw new Error(`Page not found: ${path}`); const Page = module.default; return ; } ``` ### Island Loading Strategies ```tsx // Visible.island.tsx export const options = { loading: "visible", }; ``` - `eager` (default): hydrate immediately - `idle`: hydrate when the browser is idle - `visible`: hydrate when the island enters the viewport ### Media Queries ```tsx // MediaQuery.island.tsx export const options = { media: "(max-width: 768px)", }; ``` ## Data Fetching ### Entry-level (recommended for CMS/API data) ```tsx // main.server.tsx const posts = await fetchPosts(); export async function getStaticPaths() { return ["/", ...posts.map((p) => `/blog/${p.slug}`)]; } export async function render(url: string) { const post = posts.find((p) => `/blog/${p.slug}` === url); return { body: renderPost(post), }; } ``` ### React `use()` during static rendering ```tsx import { use } from "react"; const cache = new Map(); export function useFetch(url: string): T { let promise = cache.get(url); if (!promise) { promise = fetch(url).then((res) => res.json()); cache.set(url, promise); } return use(promise); } ``` ## Static Paths ```tsx // main.server.tsx export function getStaticPaths() { return ["/", "/about", "/blog/post-1", "/blog/post-2"]; } ``` ## Configuration ```ts capri({ prerender: "/", followLinks: true, islandGlobPattern: "/src/**/*.island.*", spa: "/preview", inlineCss: false, sitemap: { origin: "https://example.com", }, }); ``` ## Deployment ```bash npm run build ``` This runs two Vite builds: a client bundle for island hydration and an SSR build for static HTML generation. Deploy the `dist` directory to any static host. ## Contributing Contributions are welcome. See the GitHub repository for details. ## License MIT