summaryrefslogtreecommitdiff
path: root/cgithub
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-09-04 21:26:21 -0400
committerJoe Mou <dev@mou.fo>2026-09-04 21:49:59 -0400
commit53ac3bc511a45e4e2e3f234a8b8dcf853b0c38d9 (patch)
treed887da14050b199aca617a4f46d52763a2da7073 /cgithub
parent5bfe8b345d2ee92111a2363321a248adb4f5195a (diff)
Split the page shell out of the repository layout
layout.eta was two things at once: the HTML shell every page needs and the repository chrome around it. owner.eta, being the one page that is not about a repository, could use neither and carried its own copy of the shell, which had drifted -- its footer named no build commit and did not link to the original page on GitHub. The shell moves to views/layouts/base.eta, taking its title and any deferred scripts as layout data, and the chrome to views/layouts/repo.eta, a layout over it that adds the clone commands, nav, search box, and the "/" shortcut that focuses it. Repository pages point at repo.eta and render as before; owner.eta uses base.eta directly and so gains the footer it was missing. Templates now live in more than one directory, so scripts/build.ts walks views/ recursively and keys each template by its path underneath it. Names crossing a directory are written from the views root ("/layouts/base.eta"), the one form both Eta's filesystem resolver and the precompiled resolver in worker.ts agree on; siblings stay relative. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ba5p88cWtCq42vf3uweweL
Diffstat (limited to 'cgithub')
-rw-r--r--cgithub/CLAUDE.md2
-rw-r--r--cgithub/scripts/build.ts20
-rw-r--r--cgithub/src/app.ts2
-rw-r--r--cgithub/src/worker.ts5
-rw-r--r--cgithub/views/blob.eta2
-rw-r--r--cgithub/views/commits.eta2
-rw-r--r--cgithub/views/issues.eta2
-rw-r--r--cgithub/views/layout.eta66
-rw-r--r--cgithub/views/layouts/base.eta17
-rw-r--r--cgithub/views/layouts/repo.eta59
-rw-r--r--cgithub/views/owner.eta107
-rw-r--r--cgithub/views/pulls.eta2
-rw-r--r--cgithub/views/release.eta2
-rw-r--r--cgithub/views/releases.eta2
-rw-r--r--cgithub/views/repo.eta2
-rw-r--r--cgithub/views/tree.eta2
-rw-r--r--cgithub/views/wiki.eta2
-rw-r--r--cgithub/views/wikiPages.eta2
18 files changed, 156 insertions, 142 deletions
diff --git a/cgithub/CLAUDE.md b/cgithub/CLAUDE.md
index edb33f5..6c6dc60 100644
--- a/cgithub/CLAUDE.md
+++ b/cgithub/CLAUDE.md
@@ -24,7 +24,7 @@ pnpm test
- `src/worker.ts` - Cloudflare Workers entry (config in `wrangler.jsonc`)
- `src/scraper.ts` - GitHub HTML fetching and JSON extraction logic
-**Templates:** `views/*.eta` - Eta templates with layout inheritance (`layout.eta` as base). On Workers, runtime code generation is disallowed, so `scripts/build.ts` precompiles templates to `src/views.generated.ts`. The same script records the build's `git describe` output in `src/build.generated.ts` for the page footer. Both are gitignored and regenerated by the `dev`/`deploy` scripts.
+**Templates:** `views/*.eta` - Eta templates with layout inheritance. `views/layouts/base.eta` is the HTML shell every page uses; `views/layouts/repo.eta` wraps it with the repository header, nav, and search box. Templates address each other from the views root (`/layouts/base.eta`) or as siblings (`./_tree`). On Workers, runtime code generation is disallowed, so `scripts/build.ts` precompiles templates to `src/views.generated.ts`. The same script records the build's `git describe` output in `src/build.generated.ts` for the page footer. Both are gitignored and regenerated by the `dev`/`deploy` scripts.
**Static files:** `public/static/*`, served by Workers Static Assets.
diff --git a/cgithub/scripts/build.ts b/cgithub/scripts/build.ts
index 76d88a0..4908532 100644
--- a/cgithub/scripts/build.ts
+++ b/cgithub/scripts/build.ts
@@ -14,10 +14,22 @@ const root = path.join(import.meta.dirname, "..");
const viewsDir = path.join(root, "views");
const eta = new Eta({ views: viewsDir });
-const names = fs
- .readdirSync(viewsDir)
- .filter((name) => name.endsWith(".eta"))
- .sort();
+// Names are the templates' paths under views/, e.g. "layouts/base.eta", which
+// is how the templates address each other (see src/worker.ts).
+function templateNames(dir: string, prefix = ""): string[] {
+ const names: string[] = [];
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const name = prefix + entry.name;
+ if (entry.isDirectory()) {
+ names.push(...templateNames(path.join(dir, entry.name), `${name}/`));
+ } else if (entry.name.endsWith(".eta")) {
+ names.push(name);
+ }
+ }
+ return names.sort();
+}
+
+const names = templateNames(viewsDir);
let out = `// @ts-nocheck
// Generated by scripts/build.ts -- do not edit.
diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts
index 2fa5832..a040aaa 100644
--- a/cgithub/src/app.ts
+++ b/cgithub/src/app.ts
@@ -39,7 +39,7 @@ export function createApp(eta: Eta) {
const app = new Hono();
app.use(async (c, next) => {
- // Include render data for layout.eta.
+ // Include render data for views/layouts/base.eta.
c.setRenderer((template, data) =>
c.html(eta.render(template, { ...data, githubUrl: githubUrlFor(c), buildCommit })),
);
diff --git a/cgithub/src/worker.ts b/cgithub/src/worker.ts
index 7bf9100..e59c690 100644
--- a/cgithub/src/worker.ts
+++ b/cgithub/src/worker.ts
@@ -9,8 +9,11 @@ import { views } from "./views.generated.ts";
// cache: true, Eta serves them from the cache and never compiles.
const eta = new Eta({ cache: true });
+// Templates name each other as siblings ("./_tree") or from the views root
+// ("/layouts/base.eta"); either way the name is the path under views/ that
+// scripts/build.ts keyed the template by.
eta.resolvePath = (templatePath: string) => {
- let name = templatePath.replace(/^\.\//, "");
+ let name = templatePath.replace(/^\.?\//, "");
if (!name.endsWith(".eta")) name += ".eta";
return name;
};
diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta
index 2014b14..9f6706e 100644
--- a/cgithub/views/blob.eta
+++ b/cgithub/views/blob.eta
@@ -1,4 +1,4 @@
-<% layout("./layout.eta"); it.searchType = "code" %>
+<% layout("/layouts/repo.eta"); it.searchType = "code" %>
<%~ include("./_nav.eta", {...it, view: "blob"}) %>
diff --git a/cgithub/views/commits.eta b/cgithub/views/commits.eta
index c29693d..a80ef07 100644
--- a/cgithub/views/commits.eta
+++ b/cgithub/views/commits.eta
@@ -1,4 +1,4 @@
-<% layout("./layout.eta") %>
+<% layout("/layouts/repo.eta") %>
<%~ include("./_nav.eta", {...it, view: "commits"}) %>
diff --git a/cgithub/views/issues.eta b/cgithub/views/issues.eta
index 646da46..754afb8 100644
--- a/cgithub/views/issues.eta
+++ b/cgithub/views/issues.eta
@@ -1,4 +1,4 @@
-<% layout("./layout"); it.searchType = "issues" %>
+<% layout("/layouts/repo.eta"); it.searchType = "issues" %>
<table>
<% for (const issue of it.issues) { %>
diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta
deleted file mode 100644
index 37289dd..0000000
--- a/cgithub/views/layout.eta
+++ /dev/null
@@ -1,66 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title><%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path || '/' %></title>
- <link rel="stylesheet" href="/static/style.css">
- <script src="/static/refs.js" defer></script>
-</head>
-<body>
- <div class="box">
- <div class="repo"><a href="/<%= it.repo.owner %>"><%= it.repo.owner %></a>/<a href="/<%= it.repo.owner %>/<%= it.repo.name %>"><%= it.repo.name %></a></div>
- <pre><code>git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
-git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git</code></pre>
- <nav>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>">Code</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/issues">Issues</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/pulls">Pull requests</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases">Releases</a>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/wiki">Wiki</a>
- <% if (it.branch) { %>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %><%= it.path && it.path !== '/' ? '/' + it.path : '' %>">Commits</a>
- <% } %>
- </nav>
- <form action="/<%= it.repo.owner %>/<%= it.repo.name %>/search" method="get">
- <input id="search-input" type="text" name="q" placeholder="Search…" value="<%= it.q ?? "" %>">
- <select name="type">
- <option value="issues" <%= it.searchType === "issues" ? "selected" : "" %>>Issues</option>
- <option value="pullrequests" <%= it.searchType === "pullrequests" ? "selected" : "" %>>Pull requests</option>
- <option value="code" <%= it.searchType === "code" || it.searchType == null ? "selected" : "" %>>Code</option>
- </select>
- <button type="submit">Search</button>
- </form>
- </div>
-
- <%~ it.body %>
-
- <footer>Generated by cgithub commit <code><%= it.buildCommit %></code> · <a href="<%= it.githubUrl %>" rel="noreferrer">View original</a></footer>
-
- <script>
- document.addEventListener("keydown", function(e) {
- let shortcut = "";
- if (e.ctrlKey || e.metaKey) shortcut += "C-";
- if (e.altKey) shortcut += "A-";
- if (e.shiftKey) shortcut += "S-";
- shortcut += e.key;
-
- if (shortcut === "Escape") {
- document.activeElement.blur();
- return;
- }
-
- if (["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
- return;
- }
-
- if (shortcut === "/") {
- e.preventDefault();
- const input = document.getElementById('search-input');
- input.select();
- input.scrollIntoView({ block: "nearest" });
- }
- });
- </script>
-</body>
-</html>
diff --git a/cgithub/views/layouts/base.eta b/cgithub/views/layouts/base.eta
new file mode 100644
index 0000000..fb644f5
--- /dev/null
+++ b/cgithub/views/layouts/base.eta
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title><%= it.title %></title>
+ <link rel="stylesheet" href="/static/style.css">
+ <% for (const src of it.scripts ?? []) { %>
+ <script src="<%= src %>" defer></script>
+ <% } %>
+</head>
+<body>
+ <%~ it.body %>
+
+ <footer>Generated by cgithub commit <code><%= it.buildCommit %></code> · <a href="<%= it.githubUrl %>" rel="noreferrer">View original</a></footer>
+</body>
+</html>
diff --git a/cgithub/views/layouts/repo.eta b/cgithub/views/layouts/repo.eta
new file mode 100644
index 0000000..e4f7880
--- /dev/null
+++ b/cgithub/views/layouts/repo.eta
@@ -0,0 +1,59 @@
+<%
+layout("/layouts/base.eta", {
+ title: `${it.repo.owner}/${it.repo.name} - ${it.path || "/"}`,
+ scripts: ["/static/refs.js"],
+})
+%>
+
+<div class="box">
+ <div class="repo"><a href="/<%= it.repo.owner %>"><%= it.repo.owner %></a>/<a href="/<%= it.repo.owner %>/<%= it.repo.name %>"><%= it.repo.name %></a></div>
+ <pre><code>git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
+git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git</code></pre>
+ <nav>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>">Code</a>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/issues">Issues</a>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/pulls">Pull requests</a>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases">Releases</a>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/wiki">Wiki</a>
+ <% if (it.branch) { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %><%= it.path && it.path !== '/' ? '/' + it.path : '' %>">Commits</a>
+ <% } %>
+ </nav>
+ <form action="/<%= it.repo.owner %>/<%= it.repo.name %>/search" method="get">
+ <input id="search-input" type="text" name="q" placeholder="Search…" value="<%= it.q ?? "" %>">
+ <select name="type">
+ <option value="issues" <%= it.searchType === "issues" ? "selected" : "" %>>Issues</option>
+ <option value="pullrequests" <%= it.searchType === "pullrequests" ? "selected" : "" %>>Pull requests</option>
+ <option value="code" <%= it.searchType === "code" || it.searchType == null ? "selected" : "" %>>Code</option>
+ </select>
+ <button type="submit">Search</button>
+ </form>
+</div>
+
+<%~ it.body %>
+
+<script>
+ document.addEventListener("keydown", function(e) {
+ let shortcut = "";
+ if (e.ctrlKey || e.metaKey) shortcut += "C-";
+ if (e.altKey) shortcut += "A-";
+ if (e.shiftKey) shortcut += "S-";
+ shortcut += e.key;
+
+ if (shortcut === "Escape") {
+ document.activeElement.blur();
+ return;
+ }
+
+ if (["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
+ return;
+ }
+
+ if (shortcut === "/") {
+ e.preventDefault();
+ const input = document.getElementById('search-input');
+ input.select();
+ input.scrollIntoView({ block: "nearest" });
+ }
+ });
+</script>
diff --git a/cgithub/views/owner.eta b/cgithub/views/owner.eta
index e5667fb..b3304fb 100644
--- a/cgithub/views/owner.eta
+++ b/cgithub/views/owner.eta
@@ -1,65 +1,54 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title><%= it.name ? `${it.name} (${it.login})` : it.login %></title>
- <link rel="stylesheet" href="/static/style.css">
-</head>
-<body>
- <div class="box">
- <% if (it.avatarUrl) { %>
- <img class="avatar" src="<%= it.avatarUrl %>" alt="" width="64" height="64">
- <% } %>
- <div class="repo"><%= it.name || it.login %></div>
- <div><%= it.login %><%= it.type === "organization" ? " (organization)" : "" %></div>
- <% if (it.location) { %>
- <div><%= it.location %></div>
- <% } %>
- <% if (it.nav.length > 0) { %>
- <nav>
- <% for (const item of it.nav) { %>
- <a href="<%= item.href %>"><%= item.text %></a>
- <% } %>
- </nav>
- <% } %>
- </div>
+<% layout("/layouts/base.eta", { title: it.name ? `${it.name} (${it.login})` : it.login }) %>
- <% if (it.bio || it.links.length > 0 || it.followers || it.company) { %>
- <div class="box repo-info">
- <% if (it.bio) { %>
- <div class="description"><%= it.bio %></div>
- <% } %>
- <% for (const link of it.links) { %>
- <div class="detail"><img src="/static/icons/link.svg" alt="" class="icon"> <a href="<%= link.href %>" rel="noopener noreferrer"><%= link.text %></a></div>
- <% } %>
- <% if (it.followers) { %>
- <div class="detail"><img src="/static/icons/eye.svg" alt="" class="icon"> <%= it.followers %> followers<%= it.following ? ` · ${it.following} following` : "" %></div>
- <% } %>
- <% if (it.company) { %>
- <div class="detail"><%= it.company %></div>
- <% } %>
- </div>
+<div class="box">
+ <% if (it.avatarUrl) { %>
+ <img class="avatar" src="<%= it.avatarUrl %>" alt="" width="64" height="64">
<% } %>
-
- <% if (it.pinned.length > 0) { %>
- <section>
- <h2>Pinned</h2>
- <% for (const repo of it.pinned) { %>
- <%~ include("./_ownerRepo", { repo, login: it.login }) %>
- <% } %>
- </section>
+ <div class="repo"><%= it.name || it.login %></div>
+ <div><%= it.login %><%= it.type === "organization" ? " (organization)" : "" %></div>
+ <% if (it.location) { %>
+ <div><%= it.location %></div>
<% } %>
-
- <% if (it.repos.length > 0) { %>
- <section>
- <h2>Repositories</h2>
- <% for (const repo of it.repos) { %>
- <%~ include("./_ownerRepo", { repo, login: it.login }) %>
+ <% if (it.nav.length > 0) { %>
+ <nav>
+ <% for (const item of it.nav) { %>
+ <a href="<%= item.href %>"><%= item.text %></a>
<% } %>
- </section>
+ </nav>
<% } %>
+</div>
+
+<% if (it.bio || it.links.length > 0 || it.followers || it.company) { %>
+ <div class="box repo-info">
+ <% if (it.bio) { %>
+ <div class="description"><%= it.bio %></div>
+ <% } %>
+ <% for (const link of it.links) { %>
+ <div class="detail"><img src="/static/icons/link.svg" alt="" class="icon"> <a href="<%= link.href %>" rel="noopener noreferrer"><%= link.text %></a></div>
+ <% } %>
+ <% if (it.followers) { %>
+ <div class="detail"><img src="/static/icons/eye.svg" alt="" class="icon"> <%= it.followers %> followers<%= it.following ? ` · ${it.following} following` : "" %></div>
+ <% } %>
+ <% if (it.company) { %>
+ <div class="detail"><%= it.company %></div>
+ <% } %>
+ </div>
+<% } %>
- <footer>Generated by cgithub</footer>
-</body>
-</html>
+<% if (it.pinned.length > 0) { %>
+ <section>
+ <h2>Pinned</h2>
+ <% for (const repo of it.pinned) { %>
+ <%~ include("./_ownerRepo", { repo, login: it.login }) %>
+ <% } %>
+ </section>
+<% } %>
+
+<% if (it.repos.length > 0) { %>
+ <section>
+ <h2>Repositories</h2>
+ <% for (const repo of it.repos) { %>
+ <%~ include("./_ownerRepo", { repo, login: it.login }) %>
+ <% } %>
+ </section>
+<% } %>
diff --git a/cgithub/views/pulls.eta b/cgithub/views/pulls.eta
index 9ec5ca1..697f6b1 100644
--- a/cgithub/views/pulls.eta
+++ b/cgithub/views/pulls.eta
@@ -1,4 +1,4 @@
-<% layout("./layout"); it.searchType = "pullrequests" %>
+<% layout("/layouts/repo.eta"); it.searchType = "pullrequests" %>
<table>
<% for (const pull of it.pulls) { %>
diff --git a/cgithub/views/release.eta b/cgithub/views/release.eta
index 46dd5db..64cca35 100644
--- a/cgithub/views/release.eta
+++ b/cgithub/views/release.eta
@@ -1,3 +1,3 @@
-<% layout("./layout.eta") %>
+<% layout("/layouts/repo.eta") %>
<%~ include("./_release.eta", { repo: it.repo, release: it.release, linkToDetail: false }) %>
diff --git a/cgithub/views/releases.eta b/cgithub/views/releases.eta
index 840908c..4ede28b 100644
--- a/cgithub/views/releases.eta
+++ b/cgithub/views/releases.eta
@@ -1,4 +1,4 @@
-<% layout("./layout.eta") %>
+<% layout("/layouts/repo.eta") %>
<% for (const release of it.releases) { %>
<%~ include("./_release.eta", { repo: it.repo, release, linkToDetail: true }) %>
diff --git a/cgithub/views/repo.eta b/cgithub/views/repo.eta
index 5dc6beb..60746df 100644
--- a/cgithub/views/repo.eta
+++ b/cgithub/views/repo.eta
@@ -1,4 +1,4 @@
-<% layout("./layout.eta") %>
+<% layout("/layouts/repo.eta") %>
<% if (it.info) { %>
<div class="box repo-info">
diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta
index 41ded06..fabf5b9 100644
--- a/cgithub/views/tree.eta
+++ b/cgithub/views/tree.eta
@@ -1,3 +1,3 @@
-<% layout("./layout.eta"); it.searchType = "code" %>
+<% layout("/layouts/repo.eta"); it.searchType = "code" %>
<%~ include("./_tree.eta", it) %>
diff --git a/cgithub/views/wiki.eta b/cgithub/views/wiki.eta
index d6047e1..781caff 100644
--- a/cgithub/views/wiki.eta
+++ b/cgithub/views/wiki.eta
@@ -1,4 +1,4 @@
-<% layout("./layout.eta"); it.path = "wiki/" + it.title %>
+<% layout("/layouts/repo.eta"); it.path = "wiki/" + it.title %>
<%~ include("./_wikiNav.eta", it) %>
diff --git a/cgithub/views/wikiPages.eta b/cgithub/views/wikiPages.eta
index 58d6d21..eb90781 100644
--- a/cgithub/views/wikiPages.eta
+++ b/cgithub/views/wikiPages.eta
@@ -1,4 +1,4 @@
-<% layout("./layout.eta"); it.path = "wiki/_pages" %>
+<% layout("/layouts/repo.eta"); it.path = "wiki/_pages" %>
<%~ include("./_wikiNav.eta", it) %>