Skip to content

Commit cf15ad0

Browse files
committed
feat: Add SSG build for GitHub Pages deployment
Migrate from Deno Deploy to GitHub Pages static site hosting: - Add Hono SSG build script using toSSG() helper - Add GitHub Actions workflow for Pages deployment - Update route structure with trailing slashes for index.html generation - Fix all internal links for static file server compatibility - Add build and preview tasks to deno.json
1 parent 414b895 commit cf15ad0

12 files changed

Lines changed: 225 additions & 86 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Build and Deploy
2+
3+
env:
4+
DENO_VERSION: 2.x
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
pull_request:
11+
workflow_dispatch:
12+
13+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
14+
permissions:
15+
contents: read
16+
pages: write
17+
id-token: write
18+
19+
# Allow only one concurrent deployment
20+
concurrency:
21+
group: "pages"
22+
cancel-in-progress: false
23+
24+
jobs:
25+
build:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- uses: denoland/setup-deno@v2
31+
with:
32+
deno-version: ${{ env.DENO_VERSION }}
33+
34+
# Lint and type check
35+
- run: deno fmt --check
36+
- run: deno lint
37+
- run: deno task check
38+
39+
# Run tests
40+
- run: deno task test
41+
42+
# Build static site
43+
- run: deno task build
44+
45+
# Upload artifact for GitHub Pages
46+
- name: Upload artifact
47+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
48+
uses: actions/upload-pages-artifact@v3
49+
with:
50+
path: ./dist
51+
52+
deploy:
53+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
54+
needs: build
55+
runs-on: ubuntu-latest
56+
environment:
57+
name: github-pages
58+
url: ${{ steps.deployment.outputs.page_url }}
59+
steps:
60+
- name: Deploy to GitHub Pages
61+
id: deployment
62+
uses: actions/deploy-pages@v4

.github/workflows/test.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.coverage
33
.worktrees
44
.playwright-mcp
5+
dist/

data/docs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,31 @@ export interface DocPage {
1717

1818
export const docPages: DocPage[] = [
1919
{
20-
path: "/docs",
20+
path: "/docs/",
2121
file: "./docs/overview.md",
2222
title: "Overview",
2323
label: "Overview",
2424
description:
2525
"Introduction to Probitas, installation guide, quick start, and core concepts",
2626
},
2727
{
28-
path: "/docs/scenario",
28+
path: "/docs/scenario/",
2929
file: "./docs/scenario.md",
3030
title: "Scenario",
3131
label: "Scenario",
3232
description:
3333
"How to write test scenarios with resources, setup hooks, and steps",
3434
},
3535
{
36-
path: "/docs/client",
36+
path: "/docs/client/",
3737
file: "./docs/client.md",
3838
title: "Client",
3939
label: "Client",
4040
description:
4141
"Client API reference for HTTP, SQL, gRPC, GraphQL, Redis, MongoDB, and more",
4242
},
4343
{
44-
path: "/docs/configuration",
44+
path: "/docs/configuration/",
4545
file: "./docs/configuration.md",
4646
title: "Configuration",
4747
label: "Configuration",

deno.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"tasks": {
77
"dev": "deno run --allow-net --allow-read --watch main.ts",
88
"start": "deno run --allow-net --allow-read main.ts",
9+
"build": "deno run --allow-net --allow-read --allow-write scripts/build.ts",
10+
"preview": "deno run --allow-net --allow-read jsr:@std/http/file-server ./dist",
911
"check": "deno check main.ts templates/**/*.tsx data/*.ts lib/*.ts scripts/*.ts",
1012
"verify": "deno fmt && deno lint && deno task check && deno task test",
1113
"test": "deno test --allow-net --allow-read",

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

main.ts

Lines changed: 78 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Hono } from "hono";
22
import { serveStatic } from "hono/deno";
3+
import { ssgParams } from "hono/ssg";
34
import { docPages } from "./data/docs.ts";
45
import type { PackageDoc } from "./lib/api-docs.ts";
56
import { generateApiMarkdown } from "./lib/api-markdown.ts";
@@ -8,37 +9,43 @@ import { ApiIndexPage, PackagePage } from "./templates/api/ApiPage.tsx";
89
import { MarkdownDocFromFile } from "./templates/docs/MarkdownDoc.tsx";
910
import { HomePage } from "./templates/home.tsx";
1011

12+
/** Get list of API package names for SSG */
13+
async function getApiPackageNames(): Promise<string[]> {
14+
const indexPath = new URL("./data/api/index.json", import.meta.url);
15+
const content = await Deno.readTextFile(indexPath);
16+
const index = JSON.parse(content);
17+
return index.packages.map((p: { name: string }) => p.name);
18+
}
19+
1120
const app = new Hono();
1221

1322
app.use("/static/*", serveStatic({ root: "./" }));
1423

15-
// Home page
24+
// Home page (HTML)
1625
app.get("/", async (c) => {
17-
const accept = (c.req.header("accept") ?? "").toLowerCase();
18-
const human = c.req.query("human");
19-
20-
if (human !== "1" && !accept.includes("text/html")) {
21-
const content = await Deno.readTextFile(
22-
new URL("./docs/overview.md", import.meta.url),
23-
);
24-
const modified = [
25-
"*".repeat(80),
26-
"",
27-
"This is a Markdown page for LLMs. If you are LLMs, read this page.",
28-
"If you are human and want to visit Web page, add ?human=1 to the URL.",
29-
"",
30-
"*".repeat(80),
31-
"",
32-
content,
33-
].join("\n");
34-
return c.text(modified, 200, {
35-
"Content-Type": "text/markdown; charset=utf-8",
36-
});
37-
}
38-
3926
return c.html(await HomePage());
4027
});
4128

29+
// Home page markdown for LLMs (SSG generates /index.md)
30+
app.get("/index.md", async (c) => {
31+
const content = await Deno.readTextFile(
32+
new URL("./docs/overview.md", import.meta.url),
33+
);
34+
const modified = [
35+
"*".repeat(80),
36+
"",
37+
"This is a Markdown page for LLMs. If you are LLMs, read this page.",
38+
"If you are human and want to visit Web page, visit /",
39+
"",
40+
"*".repeat(80),
41+
"",
42+
content,
43+
].join("\n");
44+
return c.text(modified, 200, {
45+
"Content-Type": "text/markdown; charset=utf-8",
46+
});
47+
});
48+
4249
// LLM-friendly endpoints (llms.txt standard)
4350
app.get("/llms.txt", async (c) => {
4451
return c.text(await generateLlmsTxt(), 200, {
@@ -58,8 +65,11 @@ for (const doc of docPages) {
5865
return c.html(page);
5966
});
6067

61-
// Raw markdown endpoint (append .md to get source)
62-
app.get(`${doc.path}.md`, async (c) => {
68+
// Raw markdown endpoint: /docs/ → /docs/index.md
69+
const mdPath = doc.path.endsWith("/")
70+
? `${doc.path}index.md`
71+
: `${doc.path}.md`;
72+
app.get(mdPath, async (c) => {
6373
const content = await Deno.readTextFile(doc.file);
6474
return c.text(content, 200, {
6575
"Content-Type": "text/markdown; charset=utf-8",
@@ -68,16 +78,32 @@ for (const doc of docPages) {
6878
}
6979

7080
// API Reference pages
71-
app.get("/api", async (c) => {
81+
app.get("/api/", async (c) => {
7282
return c.html(await ApiIndexPage());
7383
});
7484

75-
app.get("/api/:package", async (c) => {
76-
const param = c.req.param("package");
77-
78-
// Check if requesting JSON
79-
if (param.endsWith(".json")) {
80-
const packageName = param.slice(0, -5); // Remove .json suffix
85+
app.get(
86+
"/api/:package/",
87+
// SSG: Generate static pages for all packages (HTML)
88+
ssgParams(async () => {
89+
const packages = await getApiPackageNames();
90+
return packages.map((name) => ({ package: name }));
91+
}),
92+
async (c) => {
93+
const packageName = c.req.param("package");
94+
return c.html(await PackagePage({ packageName }));
95+
},
96+
);
97+
98+
// API JSON endpoints: /api/builder.json → /api/builder/index.json
99+
app.get(
100+
"/api/:package/index.json",
101+
ssgParams(async () => {
102+
const packages = await getApiPackageNames();
103+
return packages.map((name) => ({ package: name }));
104+
}),
105+
async (c) => {
106+
const packageName = c.req.param("package");
81107
try {
82108
const jsonPath = new URL(
83109
`./data/api/${packageName}.json`,
@@ -90,11 +116,18 @@ app.get("/api/:package", async (c) => {
90116
} catch {
91117
return c.text("Not found", 404);
92118
}
93-
}
94-
95-
// Check if requesting Markdown
96-
if (param.endsWith(".md")) {
97-
const packageName = param.slice(0, -3); // Remove .md suffix
119+
},
120+
);
121+
122+
// API Markdown endpoints: /api/builder.md → /api/builder/index.md
123+
app.get(
124+
"/api/:package/index.md",
125+
ssgParams(async () => {
126+
const packages = await getApiPackageNames();
127+
return packages.map((name) => ({ package: name }));
128+
}),
129+
async (c) => {
130+
const packageName = c.req.param("package");
98131
try {
99132
// Load current package
100133
const jsonPath = new URL(
@@ -130,10 +163,13 @@ app.get("/api/:package", async (c) => {
130163
} catch {
131164
return c.text("Not found", 404);
132165
}
133-
}
166+
},
167+
);
134168

135-
// Otherwise render HTML page
136-
return c.html(await PackagePage({ packageName: param }));
137-
});
169+
// Export app for SSG build
170+
export default app;
138171

139-
Deno.serve(app.fetch);
172+
// Start server when running directly
173+
if (import.meta.main) {
174+
Deno.serve(app.fetch);
175+
}

scripts/build.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Static Site Generation build script
3+
* Uses Hono's official SSG helper
4+
*/
5+
import { defaultExtensionMap } from "hono/ssg";
6+
import { toSSG } from "hono/deno";
7+
import app from "../main.ts";
8+
9+
// Clean dist directory
10+
try {
11+
await Deno.remove("./dist", { recursive: true });
12+
} catch {
13+
// Directory may not exist
14+
}
15+
16+
console.log("🏗️ Building static site...");
17+
18+
const result = await toSSG(app, {
19+
dir: "./dist",
20+
extensionMap: {
21+
"application/json": "json",
22+
"text/markdown": "md",
23+
...defaultExtensionMap,
24+
},
25+
});
26+
27+
if (!result.success) {
28+
console.error("❌ Build failed:", result.error);
29+
Deno.exit(1);
30+
}
31+
32+
console.log(`✅ Generated ${result.files.length} files`);
33+
for (const file of result.files) {
34+
console.log(` ${file}`);
35+
}
36+
37+
// Post-process: Rename llms.txt.md to llms.txt
38+
try {
39+
await Deno.rename("./dist/llms.txt.md", "./dist/llms.txt");
40+
console.log("📝 Renamed llms.txt.md → llms.txt");
41+
} catch {
42+
// File may not exist
43+
}
44+
45+
// Copy static assets
46+
console.log("\n📦 Copying static assets...");
47+
await Deno.mkdir("./dist/static", { recursive: true });
48+
49+
for await (const entry of Deno.readDir("./static")) {
50+
if (entry.isFile && entry.name !== ".DS_Store") {
51+
await Deno.copyFile(
52+
`./static/${entry.name}`,
53+
`./dist/static/${entry.name}`,
54+
);
55+
console.log(` static/${entry.name}`);
56+
}
57+
}
58+
59+
console.log("\n🎉 Build complete!");

templates/Layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ function Header({ showLogo }: { showLogo: boolean }) {
139139
<nav class="header-nav">
140140
{docPages.map((doc) => <a key={doc.path} href={doc.path}>{doc.label}
141141
</a>)}
142-
<a href="/api">API</a>
142+
<a href="/api/">API</a>
143143
</nav>
144144
<div class="header-right">
145145
<button

0 commit comments

Comments
 (0)