Adopt Cache Components, retire dead surface area, and add CI#410
Merged
Conversation
None of these modules are imported anywhere: the subscribe form/action, rules search, save-rule button, how-to, filter input, search title, the 628-line ui/cursor.tsx animation, the videos dataset, and the pricing/ format/auth-client utils. formatNumber callers move to the existing formatCount in lib/utils so utils/format.ts can go too. Also drops the unused getSupabaseCookies helper, dead state in the mobile menu, and renames update-mcp-listing.tsx -> .ts since it contains no JSX. Co-authored-by: Cursor <cursoragent@cursor.com>
Vercel observability shows the hourly Airtable sync has 500'd on every run (~172/week) since the Airtable credentials went stale, so it only produces noise and wasted invocations. Ambassador flags can be set directly in Supabase; drop the cron, its route, and the AIRTABLE_* env docs. Co-authored-by: Cursor <cursoragent@cursor.com>
Bot protection has been serving a browser challenge to non-browser clients since mid-February, so /api, /api/[slug], /api/popular, /api/plugins/[slug], and the npx install-plugin CLI that consumed them have all been hard down for months with zero successful consumers in the logs. Rather than carve out firewall exceptions for endpoints with no known users, delete them: rules-era integrations are superseded by plugin pages and Cursor deeplinks. /api/members stays, as the members page uses it. Co-authored-by: Cursor <cursoragent@cursor.com>
The page was nothing but a redirect() to /plugins/mcp-[slug], yet it rendered ~57K times a week as a serverless function. A permanent redirect in next.config handles it at the edge for free. The regex excludes /mcp/new (the submission form) and can't match two-segment paths like /mcp/x/edit. Co-authored-by: Cursor <cursoragent@cursor.com>
The safe-action middleware logged every action's input and result to stdout, which lands user PII (emails, profile fields) in Vercel logs. Log only failures, and only the action name plus error. The server client also lost its `admin` flag: privileged access now lives solely in utils/supabase/admin-client, so a cookie-scoped client can never silently escalate to the service role, and the two access paths are documented at their source. Co-authored-by: Cursor <cursoragent@cursor.com>
plugin-detail.tsx had grown past 1,000 lines and plugin-form.tsx past 800, mixing data shaping, deeplink construction, clipboard handling, and per-component-type rendering in single files. - lib/plugins/types.ts is now the single source for plugin domain types (PluginRow, PluginComponent, scan verdicts/flags) and the component input schema; data/queries, the scanner, actions, and admin UI import from it instead of re-declaring drifting copies. - plugin-detail is split into focused modules under plugins/detail/ (rules/MCP/generic sections, deeplinks, scan banner, logo, copy button, add-to-cursor CTA). - The duplicated draft-component editor in plugin-form and edit-plugin-form moves to forms/component-draft-editor.tsx. - The hand-rolled 1,000-row pagination loops in data queries collapse into utils/supabase/pagination.fetchAllPages. - github-plugin/parse exports its auth headers + rate-limited fetch for the scanner instead of keeping private near-duplicates. Co-authored-by: Cursor <cursoragent@cursor.com>
Observability showed the homepage alone burning 3.57M ISR writes and
45.8 GB-hrs a week (7.6s p50 / 183s p99 renders): every plugin install
called revalidatePath("/"), forcing a full re-render of a page that
fetches all plugins, and getSession() in shared layout pieces forced
~230K dynamic renders a week across /members, profiles, and
/plugins/new.
With cacheComponents enabled:
- Public reads in data/queries are cached with "use cache" +
cacheLife, tagged per entity (plugins, plugin-{slug}, users,
user-{slug}, followers-{id}, stars-{userId}, companies, mcps).
Viewer-scoped reads (own profile, settings, admin) stay uncached.
- Server actions invalidate tags instead of paths: updateTag for
content edits that must be read-your-own-writes, revalidateTag
(max profile) for counters like stars/installs that can refresh in
the background, so an install no longer re-renders the homepage.
- Pages move session/params reads behind Suspense boundaries with
static fallbacks (loader/gate pattern), so shells prerender and only
the personalized hole streams. Header/JoinCTA/ScrollToTop wrap their
usePathname readers in Suspense for the same reason.
- OG image routes cache rendered PNG bytes (ImageResponse itself is
not serializable); the scan drain/recover routes invalidate plugin
tags when verdicts land.
Co-authored-by: Cursor <cursoragent@cursor.com>
The repo had no CI: nothing ran Biome or tsc on push, which is how 56 type errors and a pile of lint violations accumulated on main. Adds a GitHub Actions workflow that runs biome ci and bun tsc --noEmit on pushes and PRs. Biome now respects .gitignore, skips dist/, formats Tailwind CSS directives, and the remaining violations are fixed: array-index React keys replaced with stable keys, unused params/state dropped, and the clickable upload-logo div becomes a real button (keyboard accessible for free). .gitignore also excludes local research scratch data so PII-bearing CSVs can never be committed. Co-authored-by: Cursor <cursoragent@cursor.com>
Reconciliations beyond textual conflicts: - ComponentDraft carries the stored component slug (and the GitHub parser slug in the create form) so the extracted draft editor preserves PR #408's slug-preservation fix instead of regenerating slugs from names on every save. - getUserPlugins keeps PR #405's includeInactive option; the owner variant stays uncached so publish/unpublish/delete are immediately visible, while the public variant is cached under the plugins tag. - The owner-menu actions (delete-plugin, toggle-plugin-listing) move from revalidatePath to updateTag, matching the Cache Components invalidation model. - plugin-owner-menu imports PluginRow from lib/plugins/types. - plugin-detail keeps the split layout with PR #405's owner menu and unpublished banner; scan.ts keeps PR #409's tmpfs fix plus the shared type constants. Co-authored-by: Cursor <cursoragent@cursor.com>
The owner menu, alert dialog, and its actions merged from main predate the stricter Biome setup on this branch; biome check --write only. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Rescan leaves plugin cache stale
- rescanPluginAction now selects the slug and calls updateTag("plugins") and updateTag(
plugin-${slug}) (previously it only revalidated the admin path, leaving the public detail page stale), and updateMCPListingAction now also invalidatesmcp-${slug}to match toggleMCPListingAction.
- rescanPluginAction now selects the slug and calls updateTag("plugins") and updateTag(
- ✅ Fixed: Follow skips members list tag
- toggleFollowAction now also calls updateTag("users") so the members directory cached under the
userstag refreshes its follower_count after a follow or unfollow.
- toggleFollowAction now also calls updateTag("users") so the members directory cached under the
Or push these changes by commenting:
@cursor push 5f7ed45f42
Preview (5f7ed45f42)
diff --git a/apps/cursor/src/actions/review-flagged-plugin.ts b/apps/cursor/src/actions/review-flagged-plugin.ts
--- a/apps/cursor/src/actions/review-flagged-plugin.ts
+++ b/apps/cursor/src/actions/review-flagged-plugin.ts
@@ -76,14 +76,16 @@
.action(async ({ parsedInput: { pluginId } }) => {
const supabase = await createClient();
- const { error: resetError } = await supabase
+ const { data: plugin, error: resetError } = await supabase
.from("plugins")
.update({ scan_status: "pending" })
- .eq("id", pluginId);
+ .eq("id", pluginId)
+ .select("slug")
+ .single();
- if (resetError) {
+ if (resetError || !plugin) {
throw new ActionError(
- `Failed to reset scan state: ${resetError.message}`,
+ `Failed to reset scan state: ${resetError?.message ?? "not found"}`,
);
}
@@ -97,5 +99,7 @@
}
revalidatePath("/admin/plugins");
+ updateTag("plugins");
+ updateTag(`plugin-${plugin.slug}`);
return { success: true };
});
diff --git a/apps/cursor/src/actions/toggle-follow-action.ts b/apps/cursor/src/actions/toggle-follow-action.ts
--- a/apps/cursor/src/actions/toggle-follow-action.ts
+++ b/apps/cursor/src/actions/toggle-follow-action.ts
@@ -24,8 +24,10 @@
const supabase = await createClient();
const invalidate = () => {
- // Profile follower counts, the followed user's followers list, and
- // the current user's following list must all reflect the change.
+ // Profile follower counts, the members directory's cached follower
+ // counts, the followed user's followers list, and the current user's
+ // following list must all reflect the change.
+ updateTag("users");
updateTag(`user-${slug}`);
updateTag(`followers-${userId}`);
updateTag(`following-${currentUserId}`);
diff --git a/apps/cursor/src/actions/update-mcp-listing.ts b/apps/cursor/src/actions/update-mcp-listing.ts
--- a/apps/cursor/src/actions/update-mcp-listing.ts
+++ b/apps/cursor/src/actions/update-mcp-listing.ts
@@ -39,7 +39,7 @@
})
.eq("id", id)
.eq("owner_id", userId)
- .select("id")
+ .select("id, slug")
.single();
if (error) {
@@ -47,6 +47,7 @@
}
updateTag("mcps");
+ updateTag(`mcp-${data.slug}`);
return data;
},You can send follow-ups to the cloud agent here.
leerob
commented
Jun 8, 2026
The nuqs next/app adapter reads useSearchParams(), which is runtime data under Cache Components, so every useQueryState consumer suspended during prerendering and collapsed into empty Suspense fallbacks — the homepage shell shipped without the leaderboard. Replace it with a static adapter that renders defaults at prerender time and syncs from location.search after hydration, thread a cache-scoped timestamp through the leaderboard to replace its non-deterministic Date.now() calls, cache both pages wholesale with use cache + cacheLife (SWR), move the members join CTA to a client-side cookie check, and tighten action tag invalidation. Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Rescan pending not cache-flushed
- Moved the revalidatePath/updateTag cache invalidation to run immediately after the scan_status='pending' DB update commits and before the enqueue try-catch, so a queue failure can no longer leave cached views showing the stale status.
Or push these changes by commenting:
@cursor push 4e48bb73d7
Preview (4e48bb73d7)
diff --git a/apps/cursor/src/actions/review-flagged-plugin.ts b/apps/cursor/src/actions/review-flagged-plugin.ts
--- a/apps/cursor/src/actions/review-flagged-plugin.ts
+++ b/apps/cursor/src/actions/review-flagged-plugin.ts
@@ -89,6 +89,14 @@
);
}
+ // The status reset must reach cached readers (detail banner, leaderboard)
+ // right away, so invalidate before enqueueing — a queue failure must not
+ // leave cached views showing the stale status when the row is already
+ // pending. The drain route only invalidates again once the scan ends.
+ revalidatePath("/admin/plugins");
+ updateTag("plugins");
+ updateTag(`plugin-${plugin.slug}`);
+
try {
await enqueuePluginScan(pluginId);
kickDrainAfterResponse();
@@ -98,10 +106,5 @@
);
}
- revalidatePath("/admin/plugins");
- // The status reset must reach cached readers (detail banner, leaderboard)
- // right away; the drain route only invalidates again once the scan ends.
- updateTag("plugins");
- updateTag(`plugin-${plugin.slug}`);
return { success: true };
});You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 9834409. Configure here.
Collaborator
Author
In rescanPluginAction the plugin's scan_status is committed to 'pending' before enqueuePluginScan runs. Cache invalidation lived after the enqueue try/catch, so an enqueue failure threw before invalidation and left cached readers (detail banner, leaderboard) showing the stale status until tag expiry. Move revalidatePath/updateTag to run immediately after the DB update commits so the reset always reaches cached readers. Applied via @cursor push command
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
This PR lands the combined output of several parallel agent sessions on the directory app: a Vercel observability audit that turned into cost/bleeding fixes, a Cache Components (PPR) migration, a staff-level maintainability pass, and a CI baseline. It merges cleanly with
main, reconciling the recently-merged owner menu (#405), slug preservation (#408), and scan sandbox fix (#409).Why
Digging through Vercel observability, firewall, and logs surfaced concrete bleeding:
sync-ambassadorscron 500'd on all ~172 runs/week (stale Airtable credentials) — pure noise and wasted invocations./api,/api/[slug],/api/popular,/api/plugins/[slug]) and thenpx install-pluginCLI have been hard-down since mid-February: bot protection serves challenge HTML to non-browser clients, and logs show zero successful consumers. Deleting beats carving firewall exceptions for unused endpoints.revalidatePath("/"), re-rendering a page that fetches all plugins.getSession()in shared components forced ~230K dynamic renders/week on/members, profiles, and/plugins/new— pages that are 95% static shell./mcp/[slug]rendered ~57K times/week as a serverless function just to issue aredirect().Separately, a maintainability review found the safe-action middleware logging every action's inputs/results (user PII) to stdout, an
adminflag on the cookie-scoped Supabase client that made privilege escalation a one-liner, ~2,000 lines of orphaned dead code, two 800–1,000-line components, and 56 type errors onmain— unsurprising, since nothing rantscor Biome.What changed
Roughly one commit per workstream, in order:
ui/cursor.tsx, pricing/format utils, etc.);formatNumbercallers consolidate onformatCount.AIRTABLE_*env docs.install-pluginCLI (/api/membersstays — the members page uses it)./mcp/[slug]as a permanent edge redirect vianext.config.mjs(excludes/mcp/new; can't match/mcp/x/edit).utils/supabase/servercan no longer escalate: service-role access lives solely inadmin-clientwith documented responsibilities.lib/plugins/types.tsis the single source for plugin domain types/schemas (queries, scanner, actions, and admin UI used to re-declare drifting copies);plugin-detail.tsx(1,020 lines) splits into focusedplugins/detail/*modules; the duplicated draft-component editor moves toforms/component-draft-editor.tsx; hand-rolled pagination loops collapse intofetchAllPages.cacheComponents: true) —data/queries.tsget"use cache"+cacheLife+ entity tags (plugins,plugin-{slug},users,followers-{id},stars-{userId}, …). Viewer-scoped reads (own profile, settings, admin) stay uncached.updateTagfor read-your-own-writes mutations,revalidateTag(…, "max")for counters (stars/installs) that can refresh in the background — an install no longer re-renders the homepage.params/getSession()reads behindSuspenseso shells prerender and only the personalized hole streams;Header/JoinCTA/ScrollToTopwrap theirusePathnamereaders inSuspense.ImageResponseisn't serializable); scan drain/recover routes invalidate plugin tags when verdicts land.tsc --noEmiton every push/PR, and fix what it caught (stable React keys, unused state, the clickable-div upload control becomes a real button).The merge commit reconciles with what landed on
mainmeanwhile:ComponentDraftnow carries stored slugs so the extracted editor preserves #408's fix (including parser-derived slugs in the create flow);getUserPluginskeeps #405'sincludeInactivewith the owner variant uncached; the new owner-menu actions move fromrevalidatePathtoupdateTagso publish/unpublish/delete stay immediately visible.Test plan
Verified locally:
bunx biome ci .— clean (errors: 0)bun run typecheck— clean (was 56 errors on the pre-PR baseline)bun run build— passes: 3,343 pages, homepage fully static, every dynamic route shows◐(partial prerender), no route regressed to fully dynamicWorth checking on the preview deploy:
/mcp/<some-slug>308s to/plugins/mcp-<some-slug>;/mcp/newstill serves the submission form/, a plugin, a profile, a company/admin/pluginsmoderation queue loads (admin-gated, uncached)Caveats
.env.localfiles were rotated; grab thesb_publishable_…key from the dashboard).getTotalUsers; event-driven tags handle everything plugin-related./u/[slug]/settingsnow see "User not found" instead of a redirect.Made with Cursor
Note
Medium Risk
Broad caching and invalidation changes affect how quickly listings, profiles, and moderation state update site-wide; mis-tagged cache entries could show stale plugin or user data until invalidation runs.
Overview
Adopts Next.js Cache Components (
cacheComponents: true) so public reads cache with entity tags (plugins,plugin-{slug},users, etc.) and server actions invalidate viaupdateTag/revalidateTaginstead ofrevalidatePath("/")—so installs, stars, and moderation no longer force full homepage rebuilds.Removes dead or failing surface area: legacy public rules/plugin JSON APIs, the
install-pluginCLI consumers, the Airtable ambassadors cron (and env docs),subscribeAction, and assorted unused UI/utils./mcp/:slugbecomes a config redirect to/plugins/mcp-:slug(excludingnew/ edit routes).Hardens ops and DX: GitHub CI runs Biome + typecheck; safe-action middleware logs failures only (no client inputs/results). Plugin domain types and
componentInputSchemamove tolib/plugins/types; plugin detail and forms split intoplugins/detail/*andcomponent-draft-editor.Rendering model: homepage, members, sitemap, and OG images use
"use cache"; auth-, admin-, and owner-gated content streams behindSuspense; header/nav/CTA wrapusePathnamereaders; scan drain invalidates plugin tags when scans finish.Reviewed by Cursor Bugbot for commit d8d2c03. Bugbot is set up for automated code reviews on this repo. Configure here.