diff --git a/apps/cursor/package.json b/apps/cursor/package.json index 811e9f34..0ed0268a 100644 --- a/apps/cursor/package.json +++ b/apps/cursor/package.json @@ -2,6 +2,9 @@ "name": "@directories/cursor", "version": "0.1.0", "private": true, + "engines": { + "node": "24.x" + }, "scripts": { "dev": "next dev --turbopack", "build": "next build", diff --git a/apps/cursor/src/app/page.tsx b/apps/cursor/src/app/page.tsx index 253ac204..b71bb5a9 100644 --- a/apps/cursor/src/app/page.tsx +++ b/apps/cursor/src/app/page.tsx @@ -4,7 +4,7 @@ import type { LeaderboardItem } from "@/components/plugins/plugin-leaderboard"; import { Startpage } from "@/components/startpage"; import { getPluginInstallVelocity, - getPlugins, + getPluginListing, getTotalUsers, } from "@/data/queries"; @@ -22,7 +22,7 @@ export const metadata: Metadata = { }; function toLeaderboardItem( - p: NonNullable>["data"]>[number], + p: NonNullable>["data"]>[number], installs30d: number, ): LeaderboardItem { return { @@ -65,7 +65,7 @@ export default async function Page() { { data: velocityMap }, ] = await Promise.all([ getTotalUsers(), - getPlugins({ fetchAll: true }), + getPluginListing(), getPluginInstallVelocity(30), ]); diff --git a/apps/cursor/src/app/plugins/[slug]/page.tsx b/apps/cursor/src/app/plugins/[slug]/page.tsx index 5e6e6d37..8e789ae9 100644 --- a/apps/cursor/src/app/plugins/[slug]/page.tsx +++ b/apps/cursor/src/app/plugins/[slug]/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { PluginDetailView } from "@/components/plugins/plugin-detail"; -import { getPluginBySlug, getPlugins } from "@/data/queries"; +import { getPluginBySlug, getPluginListing } from "@/data/queries"; type Params = Promise<{ slug: string }>; @@ -41,7 +41,7 @@ export async function generateMetadata({ } export async function generateStaticParams() { - const { data: plugins } = await getPlugins({ fetchAll: true }); + const { data: plugins } = await getPluginListing(); return (plugins ?? []).map((p) => ({ slug: p.slug })); } diff --git a/apps/cursor/src/app/sitemap.ts b/apps/cursor/src/app/sitemap.ts index 50c49433..ea80066e 100644 --- a/apps/cursor/src/app/sitemap.ts +++ b/apps/cursor/src/app/sitemap.ts @@ -1,6 +1,6 @@ import type { MetadataRoute } from "next"; import { cacheLife, cacheTag } from "next/cache"; -import { getCompanies, getPlugins } from "@/data/queries"; +import { getCompanies, getPluginListing } from "@/data/queries"; const BASE_URL = "https://cursor.directory"; @@ -42,7 +42,7 @@ export default async function sitemap(): Promise { }, ]; - const { data: plugins } = await getPlugins({ fetchAll: true }); + const { data: plugins } = await getPluginListing(); if (plugins) { for (const plugin of plugins) { routes.push({ diff --git a/apps/cursor/src/data/queries.ts b/apps/cursor/src/data/queries.ts index bfb9dbd8..86e2f64e 100644 --- a/apps/cursor/src/data/queries.ts +++ b/apps/cursor/src/data/queries.ts @@ -330,47 +330,59 @@ export async function getMCPBySlug(slug: string) { // Plugins (Open Plugins spec) // --------------------------------------------------------------------------- -export async function getPlugins({ - page = 1, - limit = 36, - fetchAll = false, -}: { - page?: number; - limit?: number; - fetchAll?: boolean; -} = {}): Promise<{ data: PluginRow[] | null; error: unknown }> { +type PluginListing = Pick< + PluginRow, + | "id" + | "name" + | "slug" + | "description" + | "logo" + | "author_name" + | "author_url" + | "verified" + | "install_count" + | "star_count" + | "created_at" + | "updated_at" + | "permanently_blocked" + | "flag_severity" + | "scan_status" +>; + +// Every active plugin for the homepage leaderboard, plugin static params and +// the sitemap. Keep component content out: with it the list is ~30 MB and the +// homepage's cache fill times out during builds. Pages are 100 rows because +// that's the production Data API's max rows per request; with a larger page +// size `fetchAllPages` would stop after the first page. +export async function getPluginListing(): Promise<{ + data: PluginListing[] | null; + error: unknown; +}> { "use cache"; cacheLife("hours"); cacheTag("plugins"); const supabase = await createClient(); - const baseQuery = () => - supabase + return fetchAllPages(async (from, to) => { + const { data, error } = await supabase .from("plugins") - .select("*, plugin_components(*)") + .select( + "id, name, slug, description, logo, author_name, author_url, verified, install_count, star_count, created_at, updated_at, permanently_blocked, flag_severity, scan_status", + ) .eq("active", true) - .order("created_at", { ascending: false }); - - if (fetchAll) { - return fetchAllPages(async (from, to) => { - const { data, error } = await baseQuery().range(from, to); - return { data: data as PluginRow[] | null, error }; - }, 100); - } - - const { data, error } = await baseQuery() - .limit(limit) - .range((page - 1) * limit, page * limit - 1); - - return { data: data as PluginRow[] | null, error }; + .order("created_at", { ascending: false }) + .order("id") + .range(from, to); + return { data: data as PluginListing[] | null, error }; + }, 100); } // Slugs of every `rule` component on an active plugin. Legacy rule URLs // (`/{rule-slug}`) prerender a redirect to the parent plugin, and there are // thousands of them — they need a slugs-only query. Building the list from -// `getPlugins({ fetchAll: true })` (full table, all columns, serial pages) -// inside every prerendering page caused `USE_CACHE_TIMEOUT` build failures. +// the full plugins list inside every prerendering page caused +// `USE_CACHE_TIMEOUT` build failures. export async function getRuleRedirectSlugs() { "use cache"; cacheLife("hours");