TerevintoSoftware.StaticSiteGenerator
An ASP.NET Core tool for generating static websites from MVC views.
Build static sites with interactive islands
Capri is a static site generator for React and Preact that uses island architecture: only interactive components hydrate, everything else ships as static HTML.
.island.tsx to opt into hydration.# React npm create capri@latest my-site --template react # Preact npm create capri@latest my-site --template preact
Components without .island.tsx render to static HTML and ship zero JavaScript. Components with .island.tsx hydrate on the client.
// Counter.island.tsx
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
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.
// main.tsx
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { Router } from "./router.jsx";
ReactDOM.createRoot(document.body).render(
<StrictMode>
<Router path={window.location.pathname} />
</StrictMode>,
);
// 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(
<StrictMode>
<Router path={url} />
</StrictMode>,
),
};
}
Your Router decides what to render based on the path prop:
export function Router({ path }: { path: string }) {
if (path === "/") return <Home />;
if (path === "/about") return <About />;
throw new Error("Not found");
}
A simple file-based router using import.meta.glob():
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 <Page />;
}
// Visible.island.tsx
export const options = {
loading: "visible",
};
eager (default): hydrate immediatelyidle: hydrate when the browser is idlevisible: hydrate when the island enters the viewport// MediaQuery.island.tsx
export const options = {
media: "(max-width: 768px)",
};
// 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),
};
}
use() during static renderingimport { use } from "react";
const cache = new Map();
export function useFetch<T>(url: string): T {
let promise = cache.get(url);
if (!promise) {
promise = fetch(url).then((res) => res.json());
cache.set(url, promise);
}
return use(promise);
}
// main.server.tsx
export function getStaticPaths() {
return ["/", "/about", "/blog/post-1", "/blog/post-2"];
}
capri({
prerender: "/",
followLinks: true,
islandGlobPattern: "/src/**/*.island.*",
spa: "/preview",
inlineCss: false,
sitemap: {
origin: "https://example.com",
},
});
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.
Contributions are welcome. See the GitHub repository for details.
MIT
more like this
An ASP.NET Core tool for generating static websites from MVC views.
A simplified build system for static sites and single-page webapps, based on Webpack 3.
search projects, people, and tags