Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/cursor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions apps/cursor/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -22,7 +22,7 @@ export const metadata: Metadata = {
};

function toLeaderboardItem(
p: NonNullable<Awaited<ReturnType<typeof getPlugins>>["data"]>[number],
p: NonNullable<Awaited<ReturnType<typeof getPluginListing>>["data"]>[number],
installs30d: number,
): LeaderboardItem {
return {
Expand Down Expand Up @@ -65,7 +65,7 @@ export default async function Page() {
{ data: velocityMap },
] = await Promise.all([
getTotalUsers(),
getPlugins({ fetchAll: true }),
getPluginListing(),
getPluginInstallVelocity(30),
]);

Expand Down
4 changes: 2 additions & 2 deletions apps/cursor/src/app/plugins/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 }>;

Expand Down Expand Up @@ -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 }));
}

Expand Down
4 changes: 2 additions & 2 deletions apps/cursor/src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -42,7 +42,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
},
];

const { data: plugins } = await getPlugins({ fetchAll: true });
const { data: plugins } = await getPluginListing();
if (plugins) {
for (const plugin of plugins) {
routes.push({
Expand Down
68 changes: 40 additions & 28 deletions apps/cursor/src/data/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PluginListing>(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<PluginRow>(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");
Expand Down
Loading