Deployment
Furin supports two deployment strategies: a Bun build (live server with SSR/ISR support) and a static export (pre-rendered HTML files, no server required).
Static Export — GitHub Pages
The static export pre-renders all SSG routes to dist/path/index.html files and generates a 404.html SPA shell so that client-side navigation keeps working on GitHub Pages.
Configuration
Add a static block to furin.config.ts:
import { defineConfig } from "@teyik0/furin/config";
export default defineConfig({
plugins: [...],
static: {
basePath: "/my-repo", // GitHub repository name, no trailing slash
outDir: "dist", // output directory (default: "dist")
onSSR: "error", // "error" (default) | "skip"
},
});
| Option | Type | Default | Description |
|---|---|---|---|
basePath | string | "" | Path prefix for GitHub Pages (e.g. "/furin" when the site lives at user.github.io/furin/) |
outDir | string | "dist" | Output directory |
onSSR | "error" | "skip" | "error" | Behavior when an SSR route is encountered: hard error or warn + silent skip |
Build Command
{
"scripts": {
"build:static": "furin build --target static"
}
}
Output Layout
dist/
├── index.html ← route "/"
├── blog/
│ └── hello-world/
│ └── index.html ← route "/blog/hello-world"
├── 404.html ← SPA shell (GitHub Pages fallback)
└── _client/
├── _hydrate.js
└── _hydrate.css
SSR / ISR Routes
The static export does not support SSR or ISR routes — they require a live server.
onSSR: "error"(default) — the build fails and lists all non-exportable routes. Recommended to prevent silent production gaps.onSSR: "skip"— SSR/ISR routes are skipped with a warning. Useful when some pages are served through a separate mechanism.
# Example error output with onSSR: "error"
✗ Static export failed: the following routes cannot be statically exported:
- /dashboard (mode=ssr)
- /feed (mode=isr)
Set onSSR: "skip" in furin.config.ts to skip them instead.
Deploying to GitHub Pages
Add a workflow file at .github/workflows/deploy.yml:
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build:static
- uses: actions/upload-pages-artifact@v3
with:
path: dist
- id: deploy
uses: actions/deploy-pages@v4
Tip: the generated
404.htmlis an empty SPA shell. GitHub Pages serves it automatically for any unknown URL, which lets the client-side router take over.
Bun Build (Live Server)
Use the Furin CLI through your package script or directly:
{
"scripts": {
"build": "bunx furin build --target bun"
}
}
Output Layout
The Bun build writes to .furin/build/bun:
.furin/build/bun/
├── client/
│ └── index.html
├── public/
├── server.js
└── manifest.json
Compiled Binaries
If you pass --compile server, Furin writes a server binary and keeps the built client assets on disk.
bunx furin build --target bun --compile server
If you pass --compile embed, Furin writes a self-contained server binary and removes the built client directory from the target output because the assets are embedded.
bunx furin build --target bun --compile embed
Production Runtime
For disk-based builds:
server.jsreads client assets fromclient/next to the server bundlepublic/is copied into.furin/build/bun/publicFURIN_CLIENT_DIRcan override the client asset directory at runtime
FURIN_CLIENT_DIR=/absolute/path/to/client bun ./.furin/build/bun/server.js
Planned Targets
The config surface already names node, vercel, and cloudflare, but the current core build path throws for those targets. Treat them as planned, not available, until their builders land in packages/core/src/build/index.ts.
Public Environment Variables
Prefix client-visible variables with FURIN_PUBLIC_ and expose them through Bun:
[serve.static]
env = "FURIN_PUBLIC_*"
FURIN_PUBLIC_API_URL=https://api.example.com
Giscus In The Docs App
The docs app can render Giscus comments from GitHub Discussions by setting these public values:
FURIN_PUBLIC_GISCUS_REPO=teyik0/furin
FURIN_PUBLIC_GISCUS_REPO_ID=...
FURIN_PUBLIC_GISCUS_CATEGORY=General
FURIN_PUBLIC_GISCUS_CATEGORY_ID=...
That keeps comments out of your application database.