Repository navigation
Add extension directory page to hunk.dev - #811
Conversation
Extensions are already published as ordinary GitHub repositories tagged `hunk-extension` and installed with `hunk extension install owner/repo`, but nothing on hunk.dev showed people what exists or what publishing one gets them. The docs pointed at the raw GitHub topic page, which ranks by stars and cannot say which extensions this Hunk can actually load. `/extensions/` renders the marketing shell rather than a docs page: it is a directory, not documentation, and the install command is the product. The catalog in `website/src/data/extensions.ts` is curated by hand while the list is short, shaped so a topic-driven indexer can replace the literal without touching the page, and static facts live there so the page renders when GitHub is unreachable. Stars and last-push come from a build-time fetch that degrades to omitting them. `scripts/check-extension-catalog.test.ts` keeps the directory honest: it parses every listing's install command with the CLI's own source parser and fails when the advertised API version drifts from the one Hunk ships, so the page can never publish a command that does not run or an extension this Hunk refuses to load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
The first pass was one column of tall cards, each carrying a full-width install command. That reads fine at five listings and falls apart well before a hundred: no way to narrow, nothing to sort by, and a page whose height grows without bound. The card is now fixed-anatomy and tight — owner avatar, `owner/name`, two-line summary, category chips, then stars, version, API version, and recency — in a two-column grid, so a reader scans a column instead of re-reading every card. The install command moves to a copy button in the card corner and the title link covers the card, which keeps the action one click away without spending a row on it. Above the grid: search over name, owner and summary; sort by stars, recency, newest, or name; and a filter chip per category with its count, built only from categories something is actually tagged with so a chip never leads to an empty grid. The grid pages 24 at a time. Every card is server-rendered, so the directory works with JavaScript off and the script only reorders and hides what is already there; it sorts by moving nodes rather than painting CSS order, so reading order, tab order, and visual order cannot disagree. Verified by hand against a temporary 60-listing catalog: two columns hold, facet counts stay honest, and paging shows 24 with "show 24 more". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
The page carried three things that explained it instead of showing it: a kicker, a count card whose number the results line already gives, a paragraph defining extensions, and a publishing section repeating four steps the authoring guide owns. On a page whose job is "here is what exists", each one pushed the grid further down. What is left above the grid is two lines: what these are and how to add one, then the small print that says where listings come from and that nobody reviewed them. The publish path survives as a link on "write your own"; the authoring guide is where those steps belong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
…ails The directory renders stars, versions, and recency from a build-time GitHub read, but it made one request per listing and swallowed every failure. Both halves were wrong. Unauthenticated builds get 60 requests an hour against a shared IP, so a hundred listings would exhaust the budget and lose every star count at once — and because a failed read just omits the field, the page would look like it had never been built to show stars at all. One topic search returns up to a hundred tagged repositories with the same metadata in a single request, and it is the query the future indexer runs, so the fetch now sits on the shape it will keep. A listing the search does not return is read directly and named in the build log: that only happens when a listed repository is missing from its own topic, which is a catalog bug worth seeing — it will disappear the day this list is generated from the topic rather than maintained by hand. Failures now warn on the build instead of quietly rendering less. Parsing moved into `indexActivityByRepo` and `readActivity`, which are pure and tested: case-insensitive repository matching, and a field GitHub omits stays absent rather than becoming a card claiming zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
"Made by people who review a lot of code" described the authors, not the page. A reader landing from the nav wants to know what extensions do for them before they scan a grid of five names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
The warning explained where listings come from before it got to what a reader needs to act on. Three facts, in the order that matters: what an extension can do to your machine, that nobody checked these, and what to do about it. The topic is no longer named here — the authoring guide, one click away on "write your own", is where publishing belongs — so its browse URL is gone with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryAdds a new static extension directory with build-time GitHub metadata and client-side search, filtering, sorting, and pagination.
Confidence Score: 4/5The PR appears safe to merge after addressing two non-blocking hardening and repository-conformance concerns. The directory behavior is otherwise coherent, but its JSON-LD serializer permits script-element termination by catalog text and its metadata loader bypasses the required typed environment configuration path. Files Needing Attention: website/src/pages/extensions.astro, website/src/data/extensions.ts
|
| Filename | Overview |
|---|---|
| website/src/pages/extensions.astro | Implements the directory UI and client behavior; its raw JSON-LD serialization needs script-safe escaping. |
| website/src/data/extensions.ts | Defines the catalog and graceful GitHub metadata loading, but directly accesses process.env contrary to repository configuration rules. |
| website/tests/extensions-smoke.spec.ts | Covers core search, filtering, sorting, navigation, and accessibility behavior for the current catalog. |
| scripts/check-extension-catalog.test.ts | Validates catalog compatibility, install commands, facets, metadata parsing, and recency formatting. |
| scripts/check-website-links.ts | Extends marketing-route metadata validation to the new extension directory. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Catalog[Curated extension catalog] --> Loader[Build-time metadata loader]
GitHub[GitHub API] --> Loader
Loader --> Astro[Astro /extensions page]
Astro --> HTML[Pre-rendered extension cards]
HTML --> Client[Client search, filters, sorting, pagination]
Prompt To Fix All With AI
### Issue 1
website/src/pages/extensions.astro:74
**Escape JSON-LD script terminators**
A catalog contribution containing `</script>` in an extension name or summary passes through `JSON.stringify` unchanged and terminates this raw script element, allowing the remaining content to become executable markup on `/extensions/`. Escape `<` before inserting the JSON-LD. **How this was verified:** Catalog names and summaries flow directly into this raw `set:html` sink, and `JSON.stringify` does not escape the HTML script-closing sequence.
```suggestion
<script
type="application/ld+json"
is:inline
set:html={JSON.stringify(schema).replaceAll("<", "\\u003c")}
></script>
```
### Issue 2
website/src/data/extensions.ts:164
**Direct environment access bypasses Varlock**
`githubHeaders` reads `GITHUB_TOKEN` directly from `process.env`, bypassing the repository's required typed and validated Varlock configuration path for every website build.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "docs(website): state the extension risk ..." | Re-trigger Greptile
|
|
||
| /** Headers GitHub wants, with the build's token when it has one. */ | ||
| function githubHeaders() { | ||
| const token = process.env.GITHUB_TOKEN; |
There was a problem hiding this comment.
Direct environment access bypasses Varlock
githubHeaders reads GITHUB_TOKEN directly from process.env, bypassing the repository's required typed and validated Varlock configuration path for every website build.
Context Used: guidelines.mdc Cursor rule (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: website/src/data/extensions.ts
Line: 164
Comment:
**Direct environment access bypasses Varlock**
`githubHeaders` reads `GITHUB_TOKEN` directly from `process.env`, bypassing the repository's required typed and validated Varlock configuration path for every website build.
**Context Used:** guidelines.mdc Cursor rule ([source](https://github.com/modem-dev/modem/blob/main/.cursor/rules/guidelines.mdc))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Not applying this one — the rule doesn't hold in this repository.
Varlock isn't used anywhere in hunk: no dependency in either package.json, no config, no import, no .cursor/rules directory. The cited rule comes from modem-dev/modem, a different repository. There is no typed configuration path here to bypass.
The existing pattern in this codebase is the same direct read — website/src/pages/index.astro:20 does process.env.GITHUB_TOKEN for the landing page's star count, and this file follows it deliberately so both build-time GitHub reads work the same way.
Worth noting what the token is here: entirely optional. It raises GitHub's unauthenticated rate limit for one build-time metadata read; when it's absent the request goes out unauthenticated, and when that fails the page renders without stars and warns on the build. Nothing is authorized by it and nothing breaks without it.
Happy to revisit if Varlock is adopted in this repo — at that point index.astro should move too, and it belongs in one change rather than half a migration here.
Generated by Claude Code
There was a problem hiding this comment.
You're right, and I appreciate the thorough correction. The Varlock rule came from a different repository context — it doesn't apply here, and the implementation correctly follows the established codebase pattern of direct process.env reads for optional build-time tokens. Withdrawing this comment.
`JSON.stringify` leaves `<` alone, so a listing whose name or summary contained `</script>` would close the raw JSON-LD element and turn the rest of the payload into markup on `/extensions/`. The catalog is hand-reviewed today, which is exactly why this is worth fixing now: the plan is to generate it from repository descriptions nobody reviews. Escaping happens in `toJsonLdScriptBody` rather than inline in the page, so a hostile fixture can prove it — the test feeds it a script-closing name and checks both that nothing can close the element and that the result still parses as the JSON-LD it claims to be. Reported by Greptile on #811. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
…draft `cursor-line-off drafts still reveal their default target and full composer` pressed space and snapshotted the screen in the same tick, so it asserted against the pre-scroll frame whenever paging had not rendered yet. It fails on `origin/main` on its own — 5 of 5 runs here, with no changes — and took this PR's CI down with it. The same page-down assertion in `cursor-line.test.ts` already brackets the keypress with `waitIdle` on both sides; this test just never got them. With the waits it passes 5 of 5, and the whole notes file is 18/18. Unrelated to the extension directory this branch is about, but it blocks every PR until it lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY
Adds a searchable, filterable community extension directory to hunk.dev at
/extensions/, enabling users to discover and install published extensions.Summary
This change introduces a new marketing page that catalogs community extensions published to GitHub with the
hunk-extensiontopic. The directory is fully functional without JavaScript (all extensions render in HTML), with client-side search, filtering, sorting, and pagination enhancing the experience.Key changes
website/src/pages/extensions.astrorenders the directory with all extensions pre-loaded in HTML, supporting instant search and filtering without network callswebsite/src/styles/extensions.cssimplements a responsive two-column grid of fixed-anatomy cards that remain legible from 5 to hundreds of listingswebsite/src/data/extensions.tsdefines the extension catalog schema, loads curated listings, and fetches volatile metadata (stars, dates) from GitHub at build timeEXTENSION_CATALOG, with schema supporting categories, versions, and API compatibilityscripts/check-extension-catalog.test.tsvalidates catalog integrity: API version compatibility, CLI command correctness, category facet accuracy, and recency formattingwebsite/tests/extensions-smoke.spec.tscovers search/filter/sort behavior, pagination, accessibility, and navigation integrationBrandHeader.astroto include Extensions link; updated link checker to recognize the new routeImplementation details
auto-fillwithminmax(20rem, 1fr)to adapt from single column on mobile to three columns on desktophttps://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY