capri
Capri static site generator with island architecture. Use when creating components, pages, configuring hydration, fetching data, or working with this Capri project.
Installation
Suivez les instructions du dépôt pour votre agent. Consultez les fichiers et dépendances avant l’installation.
Voir la source ↗GuidesInstructions
Capri (React)
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.
Coming from Next.js?
- No
getStaticProps- fetch data inmain.server.tsxor useuseFetchin components - No
<Head>component - render full documents viaprerenderToNodeStream()or use CSS selector injection - No API routes - this is pure static generation
-
getStaticPathsexists but lives inmain.server.tsx
Coming from Astro?
- Islands use
.island.tsxfile suffix instead ofclient:*directives - Hydration timing via
export const options = { loading: "visible" } - No frontmatter - use React components throughout
Project Structure
├── 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
Islands (Interactive Components)
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>;
}
Hydration Options
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
};
- eager: Hydrate immediately (default)
- idle: Hydrate when browser is idle (requestIdleCallback)
- visible: Hydrate when scrolled into view (IntersectionObserver)
- media: Only hydrate when media query matches (e.g., mobile-only islands)
Data Fetching
In entry files (for CMS/API data)
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}`)];
}
In components (using React 19's 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));
}
Routing
File-based routing
Files in pages/ map to routes:
-
pages/root.tsx→/ -
pages/about.tsx→/about -
pages/blog/post.tsx→/blog/post
Dynamic routes
Export 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.
Head/Meta Management
Using prerenderToNodeStream (recommended)
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} />),
};
}
CSS selector injection
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) },
};
}
Common Tasks
| 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 |
Commands
npm run dev # Start dev server
npm run build # Build static site (vite build && vite build --ssr)
npm run preview # Preview production build
Vite Config
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" },
}),
],
};
Key Files
-
main.server.tsx- SSR render function,getStaticPaths -
main.tsx- Client hydration entry -
router.tsx- URL to component mapping -
vite.config.ts- Build configuration -
components/async/use-fetch.ts- Data fetching utility
Exemples
Consultez les exemples du dépôt source.
Voir la source ↗Historique source
- Date d’indexation
- 2026-03-11
- Mise à jour de l’index
- 2026-03-11
- Branch
- main
Les dates concernent ce répertoire. Consultez le dépôt pour l’historique du code.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!