Skip to content

Give every plugin a resource page, and start the catalogue's move into the database - #277

Merged
dmccoystephenson merged 6 commits into
mainfrom
feature/resource-hub-foundation
Aug 8, 2026
Merged

Give every plugin a resource page, and start the catalogue's move into the database#277
dmccoystephenson merged 6 commits into
mainfrom
feature/resource-hub-foundation

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Aug 8, 2026

Copy link
Copy Markdown
Member

Turns the flat plugin grid into the first phase of a SpigotMC-style resource hub, and writes down where the rest of it is going.

Summary

  • RESOURCE_HUB.md — the design: what SpigotMC does that this site doesn't, which of it is worth copying, and the decisions behind it (mirror GitHub releases rather than host files; keep reviews separate from discussion and from bug reports; what it would take to mark a review as coming from a server that actually runs the plugin — see the correction below). The road map gains the six phases that follow from it.
  • /resources/[slug] — a page per plugin: icon, description, server count, latest release, like button, and download / guide / source / SpigotMC links. Bug reports go to the plugin's issue tracker and feature ideas to the Dev Portal, rather than being collected on the page. The sitemap lists a resource URL per plugin, so each plugin finally has a page that can rank for its own name instead of sharing the home page with fifteen others.
  • V15 + GET /api/v1/plugins[/{slug}] — the catalogue as a table, seeded from pages/data/plugins.json and served read-only. The site still renders from the file; __tests__/pluginCatalogue.test.ts fails if the two disagree on which plugins exist or what they are called (descriptions and icons are not compared — they are cosmetic, and the guard is deleted at switchover). Splitting the move this way keeps the schema change separate from changing what the home page renders from, so neither mistake can produce a blank catalogue. The second half is Allow editing the plugin cards data JSON file from the website #87.
  • Two extractions rather than second copiesGuideLikeButton becomes SelfLoadingLikeButton taking its target as props, and the avatar's colour-from-title helper moves to utils/pluginAvatar.ts so a plugin looks the same on its card and on its page.

Changes to existing behaviour are limited to the plugin card: it gained a Details button and a linked title, GitHub dropped from the primary (contained) button to a secondary one now that Details is the primary action, and the actions row wraps instead of overflowing on a narrow card.

Correction included in this branch

The design doc originally claimed a verified-review mark could be derived at read time from state dpc-api already holds, "exactly as SERVER_OWNER is derived". A review of the branch found that to be wrong: the only inbound server data is faction sync from Medieval Factions, which carries no plugin identity; ApiKeyAuthFilter only checks that some valid key was presented, so nothing reported is attributable to a user; api_keys.server_name and factions.server_id are unrelated free text; and bStats is aggregate-only. The last commit states the gap, the three steps to close it, and that reviews ship without the mark — now tracked as #278.

Test plan

  • npm test — 219 passing, including new coverage for utils/resources.ts, releasesUrl, utils/pluginAvatar.ts, the resource page's getServerSideProps (not-found, missing param, empty-string normalisation, bStats down, no releases, both upstreams throwing), the catalogue drift guard, and the sitemap's new resource URLs
  • npm run lint — clean
  • npm run build — clean, /resources/[slug] builds as a server-rendered route
  • ./mvnw test -Dtest=PluginControllerTest — 6 passing
  • FlywayMigrationTest — skipped locally (no Docker on this machine), but run in CI against real Postgres: the V15 seed applies, the escaped apostrophes in "Dan's Essentials" parse, and the empty-string-to-NULL normalisation holds

Closes #272
Part of #271

🤖 Generated with Claude Code


drafted by Claude on behalf of Daniel Stephenson

dmccoystephenson and others added 3 commits August 8, 2026 16:47
RESOURCE_HUB.md records what a SpigotMC-style hub for Dan's Plugins would
need, which parts of SpigotMC are worth copying, and the decisions behind
them: mirror GitHub releases rather than host files, keep reviews separate
from discussion and from bug reports, and mark reviews from people whose
servers actually run the plugin.

The road map gains the six phases that follow from it, so the plan is
visible on the site rather than only in the repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…file

V15 creates the plugins table and seeds it from pages/data/plugins.json,
and dpc-api serves it read-only at GET /api/v1/plugins and
/api/v1/plugins/{slug}. The slug is the id the catalogue already uses, so
existing guide URLs and likes.target_id rows keep resolving.

This is the first half of the move: the site still renders from the file,
which means the catalogue is written down twice for now, so
__tests__/pluginCatalogue.test.ts fails if the two ever disagree. Both the
file and that guard go away when the site switches to the API.

FlywayMigrationTest now executes the seed against real Postgres, which is
what actually checks the escaped apostrophes in "Dan's Essentials" and the
empty-string-to-NULL normalisation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The home page carries all sixteen plugins at once, so it can rank for none
of them individually and a card has nowhere to put anything the grid can't
fit. Each plugin now has an addressable page: icon, description, server
count, latest release, like button, and download/guide/source/SpigotMC
links, with bug reports routed to the plugin's issue tracker and feature
ideas to the Dev Portal rather than collected on the page.

The sitemap lists a resource URL per plugin. Server counts and release tags
stay best-effort — an unreachable bStats or GitHub omits a chip rather than
failing the page.

Two extractions rather than second copies: GuideLikeButton becomes
SelfLoadingLikeButton, taking its target as props, and the avatar's
colour-from-title helper moves to utils/pluginAvatar.ts so a plugin looks
the same on its card and on its page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Review

The full diff was read against the repo's own conventions. The shape of the change holds up: the two extractions (SelfLoadingLikeButton, utils/pluginAvatar.ts) remove duplication rather than adding a second copy, the new route follows the sibling pattern in pages/guides/[id].tsx exactly (GetServerSideProps typed with an explicit props interface, Seo/TopBar/BottomBar, MUI throughout, no raw HTML, no any), both upstream fetches degrade to a missing chip instead of a failed page, and the notFound branch is covered. resourcePath being the single definition shared by links, the sitemap and canonical URLs is the right call — the sitemap test asserting a resource URL is not a guide URL is a nice touch. No new environment variables are introduced, so CONFIG.md correctly needed no edit. V15 is unclaimed, the seed matches all 16 entries in pages/data/plugins.json, and the catalogue is read-only with no write path left unguarded.

No blocking defects were found. Six notes follow, five of them cosmetic.

Findings

  • dpc-api/src/test/java/com/dansplugins/api/controller/PluginControllerTest.java:80 — the test is named servesTheOptionalFieldsAsNullWhenAbsent, but jsonPath(...).doesNotExist() passes both when the key is omitted from the JSON and when it is present with a null value (Spring's matcher asserts only that the evaluated value is null). The documented contract in dpc-api/README.md is an explicit "spigotmcUrl": null, and that is what this assertion cannot currently distinguish from an omitted key. .value(nullValue()) would pin the documented shape; doesNotHaveJsonPath() would pin omission. Either is a stronger guard than what is there now.

  • __tests__/pluginCatalogue.test.ts — the drift guard covers slug and title and explicitly excuses "cosmetic" fields, but github_url sits in neither category: it is what the download, source and bug-report links resolve to once the site reads from the table, so a drift there is a broken link rather than a cosmetic mismatch. Adding it to the comparison would cost one more capture group in the existing regex.

  • CHANGELOG.md:14 and CHANGELOG.md:29 — a blank line was left between the new bullets and the pre-existing ones, splitting each section's list into two Markdown lists and rendering the whole section loose. Removing both blank lines restores a single list per section.

  • CHANGELOG.md:12 — the plugins table / V15 entry is attributed to (#272), but Resource pages: give every plugin a page of its own at /resources/[slug] (#271 Phase 1) #272 is the resource page; this entry is the first half of Allow editing the plugin cards data JSON file from the website #87 and part of the Epic: turn the plugin catalogue into a resource hub #271 epic, which is how the PR description itself frames it. (#87) or (#271) would be the accurate reference.

  • utils/sitemap.ts:78 — "carries all sixteen plugins at once" hard-codes a count that the next catalogue addition invalidates. The point being made does not depend on the number, so phrasing it without one keeps the comment true.

  • pages/resources/[slug].tsx:68 — non-blocking, and noted for a follow-up rather than for this PR: each request performs two uncached upstream calls, one of them to the unauthenticated GitHub API (60 requests per hour per IP). The home page already does this for every plugin at once, so nothing is made worse here in kind, but the number of routes depending on that budget is now larger and no route in the repo sets Cache-Control. The release mirror described in RESOURCE_HUB.md removes the GitHub half of this entirely, so this is best left until that phase rather than patched now.

This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).

dmccoystephenson and others added 3 commits August 8, 2026 17:08
… yet

The design doc asserted that a verified mark could be derived at read time
from state dpc-api already holds, "exactly as SERVER_OWNER is derived". That
is wrong, and would have been discovered mid-implementation:

- the only inbound server data is faction sync, from Medieval Factions, and
  it carries no plugin identity — fifteen of sixteen plugins report nothing;
- ApiKeyAuthFilter only checks that some valid key was presented, so it never
  resolves the owner and nothing reported is attributable to a user;
- api_keys.server_name and factions.server_id are unrelated free text;
- bStats is aggregate-only and cannot identify a reviewer.

What is derivable today is SERVER_OWNER — "runs a server that syncs with
DPC" — which is a different claim. The doc now states the gap, the three
steps to close it (owner resolution, a plugin-side install report, then the
derived mark), and that reviews ship without the mark until enough plugins
report.

Also: state exactly which fields the catalogue drift guard compares, rather
than implying it compares everything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Address the self-review on this PR:

- The drift guard now compares github_url as well as slug and title. The
  download, source and bug-report links on a resource page are all derived
  from that URL, so a drift there is a broken link rather than a cosmetic
  mismatch. A new assertion pins which capture group is which, so a
  mis-numbered group cannot compare the wrong column.
- servesTheOptionalFieldsAsNullWhenAbsent now asserts an explicit JSON
  null. doesNotExist() passes for an omitted key too, so it could not have
  noticed the response dropping those fields entirely -- a different shape
  from the one dpc-api/README.md documents.
- Drop the blank lines that split each CHANGELOG section into two lists,
  attribute the plugins-table entry to #87 rather than #272, and stop
  hard-coding the plugin count in a comment in utils/sitemap.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two doc inaccuracies left by the verified-reviews correction and by the
drift guard gaining a third field:

- The road map's "Plugin Reviews" item still promised verified marks,
  which RESOURCE_HUB.md now says wait on an install signal dpc-api does
  not receive. The road map renders on the site, so it was promising
  something the design doc had just retracted.
- RESOURCE_HUB.md described the drift guard as comparing two fields; it
  compares three now that github_url is included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson
dmccoystephenson merged commit e5eb6d6 into main Aug 8, 2026
2 checks passed
@dmccoystephenson
dmccoystephenson deleted the feature/resource-hub-foundation branch August 8, 2026 17:14
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.

Resource pages: give every plugin a page of its own at /resources/[slug] (#271 Phase 1)

1 participant