Skip to content

Add extension directory page to hunk.dev - #811

Merged
benvinegar merged 9 commits into
mainfrom
claude/hunk-plugin-marketplace-m10rnw
Aug 18, 2026
Merged

benvinegar merged 9 commits into
mainfrom
claude/hunk-plugin-marketplace-m10rnw

Conversation

@benvinegar

Copy link
Copy Markdown
Member

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-extension topic. 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

  • New page: website/src/pages/extensions.astro renders the directory with all extensions pre-loaded in HTML, supporting instant search and filtering without network calls
  • Styling: website/src/styles/extensions.css implements a responsive two-column grid of fixed-anatomy cards that remain legible from 5 to hundreds of listings
  • Data layer: website/src/data/extensions.ts defines the extension catalog schema, loads curated listings, and fetches volatile metadata (stars, dates) from GitHub at build time
  • Catalog: Hand-curated seed of 5 extensions in EXTENSION_CATALOG, with schema supporting categories, versions, and API compatibility
  • Client script: Inline JavaScript handles search (by name/owner/description), category filtering, sorting (by stars/recency/name), and pagination without reloading
  • Testing:
    • scripts/check-extension-catalog.test.ts validates catalog integrity: API version compatibility, CLI command correctness, category facet accuracy, and recency formatting
    • website/tests/extensions-smoke.spec.ts covers search/filter/sort behavior, pagination, accessibility, and navigation integration
  • Integration: Updated navigation in BrandHeader.astro to include Extensions link; updated link checker to recognize the new route

Implementation details

  • No network calls on page load: All extension data is embedded in HTML; GitHub API calls happen only at build time to fetch stars and update timestamps
  • Graceful degradation: Directory works without JavaScript; script enhances with instant filtering and sorting
  • Stable ordering: Secondary sort by catalog order prevents shuffling when primary sort values are equal (e.g., all extensions with zero stars)
  • Accessibility: Semantic HTML with ARIA labels, live regions for result counts, proper heading hierarchy, and no serious automated violations
  • Responsive design: Grid uses auto-fill with minmax(20rem, 1fr) to adapt from single column on mobile to three columns on desktop
  • Build-time validation: Test suite ensures every listed extension is installable via the CLI, compatible with current API version, and properly categorized

https://claude.ai/code/session_01EVBuvgPtv3n2WwGUKAjxzY

claude added 6 commits August 18, 2026 21:17
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
@vercel

vercel Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hunk-web Ignored Ignored Preview Aug 18, 2026 10:50pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a new static extension directory with build-time GitHub metadata and client-side search, filtering, sorting, and pagination.

  • Introduces a curated extension catalog and metadata-loading utilities.
  • Adds the /extensions/ page, responsive styling, navigation links, documentation, and browser/tooling tests.
  • Extends website link validation and CI coverage for the new route.

Confidence Score: 4/5

The 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

Security Review

The JSON-LD block inserts catalog strings through raw HTML without escaping script-closing sequences. A malicious catalog contribution containing </script> can therefore become executable markup after merge.

Important Files Changed

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]
Loading
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

Comment thread website/src/pages/extensions.astro Outdated

/** Headers GitHub wants, with the build's token when it has one. */
function githubHeaders() {
const token = process.env.GITHUB_TOKEN;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
claude added 2 commits August 18, 2026 22:48
…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
@benvinegar
benvinegar merged commit c09b58d into main Aug 18, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants