diff options
| author | Joe Mou <dev@mou.fo> | 2026-09-04 21:26:21 -0400 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-09-04 21:49:59 -0400 |
| commit | 53ac3bc511a45e4e2e3f234a8b8dcf853b0c38d9 (patch) | |
| tree | d887da14050b199aca617a4f46d52763a2da7073 /cgithub | |
| parent | 5bfe8b345d2ee92111a2363321a248adb4f5195a (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.md | 2 | ||||
| -rw-r--r-- | cgithub/scripts/build.ts | 20 | ||||
| -rw-r--r-- | cgithub/src/app.ts | 2 | ||||
| -rw-r--r-- | cgithub/src/worker.ts | 5 | ||||
| -rw-r--r-- | cgithub/views/blob.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/commits.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/issues.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/layout.eta | 66 | ||||
| -rw-r--r-- | cgithub/views/layouts/base.eta | 17 | ||||
| -rw-r--r-- | cgithub/views/layouts/repo.eta | 59 | ||||
| -rw-r--r-- | cgithub/views/owner.eta | 107 | ||||
| -rw-r--r-- | cgithub/views/pulls.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/release.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/releases.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/repo.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/tree.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/wiki.eta | 2 | ||||
| -rw-r--r-- | cgithub/views/wikiPages.eta | 2 |
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) %> |
