diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..736f24b
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,50 @@
+name: pages
+on:
+ push:
+ branches: [master]
+ paths:
+ - "apps/site/**"
+ - "packages/core/**"
+ - "pnpm-lock.yaml"
+ - ".github/workflows/pages.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with: { node-version: 22, cache: pnpm }
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm --filter @enspack/core build
+ # Demo fixtures until the Sepolia indexer is reachable from the public internet.
+ - run: pnpm --filter @enspack/site build
+ env:
+ VITE_BASE: /enspack/
+ VITE_DATA_SOURCE: demo
+ VITE_CHAIN: sepolia
+ - uses: actions/configure-pages@v5
+ with: { enablement: true }
+ - uses: actions/upload-pages-artifact@v3
+ with: { path: apps/site/dist }
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/apps/site/README.md b/apps/site/README.md
index c51d395..26f2706 100644
--- a/apps/site/README.md
+++ b/apps/site/README.md
@@ -16,6 +16,7 @@ into the browser bundle (it would pull viem and IPFS).
| `VITE_REGISTRAR_URL` | `https://registrar.enspack.dev` | Registrar base URL (attestations, claim-page link) |
| `VITE_CHAIN` | `sepolia` | `sepolia` \| `mainnet`; drives the testnet banner and explorer / ENS-app links |
| `VITE_DATA_SOURCE` | `http` | `http` \| `demo`; `demo` serves the recorded fixtures in-app |
+| `VITE_BASE` | `/` | Build-time public base path (`/enspack/` on GitHub Pages); the router basename follows it |
## Develop
@@ -36,6 +37,15 @@ pnpm --filter @enspack/site build
pnpm --filter @enspack/site preview
```
+## Deploy (GitHub Pages, preview)
+
+`.github/workflows/pages.yml` builds the site in demo mode with
+`VITE_BASE=/enspack/` on every push to `master` that touches `apps/site` and
+publishes it to . `pnpm build` also writes
+`dist/404.html` (a copy of `index.html`, since Pages has no rewrites) and
+`dist/.nojekyll`. Pages must be set to "GitHub Actions" as the source once
+(Settings → Pages); the workflow attempts to enable it itself.
+
## Deploy (Cloudflare Pages)
Hosting target is `enspack.dev`. DNS and the Pages project are manual.
diff --git a/apps/site/package.json b/apps/site/package.json
index d265ad8..0e4d73e 100644
--- a/apps/site/package.json
+++ b/apps/site/package.json
@@ -9,7 +9,7 @@
},
"scripts": {
"dev": "vite",
- "build": "vite build",
+ "build": "vite build && node scripts/spa-fallback.mjs",
"preview": "vite preview",
"typecheck": "tsc --noEmit -p tsconfig.json",
"test": "vitest run",
diff --git a/apps/site/scripts/spa-fallback.mjs b/apps/site/scripts/spa-fallback.mjs
new file mode 100644
index 0000000..82fd389
--- /dev/null
+++ b/apps/site/scripts/spa-fallback.mjs
@@ -0,0 +1,5 @@
+// GitHub Pages has no rewrite rules: serving index.html as 404.html lets deep links reach the router.
+import { copyFileSync, writeFileSync } from "node:fs";
+
+copyFileSync("dist/index.html", "dist/404.html");
+writeFileSync("dist/.nojekyll", "");
diff --git a/apps/site/src/lib/config.ts b/apps/site/src/lib/config.ts
index 78b0a42..5cd4ab6 100644
--- a/apps/site/src/lib/config.ts
+++ b/apps/site/src/lib/config.ts
@@ -27,3 +27,9 @@ export function readConfig(env: ImportMetaEnv = import.meta.env): SiteConfig {
}
export const config: SiteConfig = readConfig();
+
+/** react-router wants the base path without a trailing slash; Vite's BASE_URL always has one. */
+export function routerBasename(baseUrl: string): string {
+ const trimmed = baseUrl.replace(/\/+$/, "");
+ return trimmed === "" ? "/" : trimmed;
+}
diff --git a/apps/site/src/main.tsx b/apps/site/src/main.tsx
index 4d2469f..6027fd4 100644
--- a/apps/site/src/main.tsx
+++ b/apps/site/src/main.tsx
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import { App } from "./app.js";
import { ClientProvider } from "./lib/client-context.js";
+import { routerBasename } from "./lib/config.js";
import "./styles/tokens.css";
import "./styles/global.css";
@@ -11,7 +12,7 @@ if (root === null) throw new Error("missing #root");
createRoot(root).render(
-
+
diff --git a/apps/site/test/lib.test.ts b/apps/site/test/lib.test.ts
index 7124bb6..2074ab2 100644
--- a/apps/site/test/lib.test.ts
+++ b/apps/site/test/lib.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { ApiError } from "../src/lib/api.js";
import { FixtureIndexClient, HttpIndexClient } from "../src/lib/client.js";
-import { readConfig } from "../src/lib/config.js";
+import { readConfig, routerBasename } from "../src/lib/config.js";
import { ensAppUrl, firstLabel, formatBytes, shortHex } from "../src/lib/format.js";
import { demoFixtures } from "./helpers.js";
@@ -13,6 +13,12 @@ describe("config", () => {
expect(c.indexUrl).toBe("https://idx.example");
});
+ it("derives the router basename from Vite's BASE_URL", () => {
+ expect(routerBasename("/")).toBe("/");
+ expect(routerBasename("/enspack/")).toBe("/enspack");
+ expect(routerBasename("/enspack")).toBe("/enspack");
+ });
+
it("honours mainnet + demo", () => {
const c = readConfig({ VITE_CHAIN: "mainnet", VITE_DATA_SOURCE: "demo" } as ImportMetaEnv);
expect(c.chain).toBe("mainnet");
diff --git a/apps/site/vite.config.ts b/apps/site/vite.config.ts
index 2911915..26dfb04 100644
--- a/apps/site/vite.config.ts
+++ b/apps/site/vite.config.ts
@@ -1,7 +1,9 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
+// VITE_BASE lets the same bundle serve from a sub-path (GitHub Pages: "/enspack/") or the root.
export default defineConfig({
+ base: process.env.VITE_BASE ?? "/",
plugins: [react()],
build: { target: "es2022", sourcemap: true },
});