Skip to content
Open
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
45 changes: 45 additions & 0 deletions .github/workflows/generate-trending.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Generate Trending Snapshots

on:
schedule:
- cron: "0 0 * * *" # daily at 00:00 UTC
workflow_dispatch: # manual trigger

jobs:
generate:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
Comment on lines +18 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cache configuration mismatch: using npm cache with pnpm.

The workflow uses pnpm for package management but configures cache: 'npm' in setup-node. This means the cache won't be utilized effectively.

🔧 Proposed fix
       - name: Setup Node.js
         uses: actions/setup-node@v4
         with:
           node-version: 20
-          cache: 'npm'
+          cache: 'pnpm'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
🤖 Prompt for AI Agents
In @.github/workflows/generate-trending.yml around lines 18 - 27, The workflow
sets up Node.js with cache: 'npm' while the job installs pnpm
(pnpm/action-setup@v4), causing a cache mismatch; update the Setup Node.js step
to use cache: 'pnpm' (or remove/align the cache setting) so the package manager
(pnpm) and the cache configuration match, targeting the Setup Node.js step and
the cache: 'npm' value in the diff.


- name: Install dependencies
run: pnpm install

- name: Generate Prisma Client
run: pnpm prisma generate

- name: Generate trending data
run: pnpm run generate:trending
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}

- name: Commit updated snapshots
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add new-api-details/trending
git diff --staged --quiet || (git commit -m "chore: update trending snapshots [skip ci]" && git push)
16 changes: 16 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
'/blog',
]

// Trending routes - one per entity (default monthly range)
const trendingRoutes = [
'/trending/organizations',
'/trending/projects',
'/trending/tech-stack',
'/trending/topics',
]

// Generate year-based routes (2016 to current year - 1, excluding future years)
// Only include years that have actually completed GSoC
// Using new /yearly/google-summer-of-code-YYYY format for SEO
Expand All @@ -162,6 +170,14 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: route === '' ? 1.0 : route === '/organizations' ? 0.9 : 0.8,
})),

// Trending routes - medium priority (updated frequently)
...trendingRoutes.map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 0.75,
})),

// Organization detail pages - high priority (money pages for SEO)
...orgSlugs.map((slug) => ({
url: `${baseUrl}/organizations/${slug}`,
Expand Down
25 changes: 25 additions & 0 deletions app/trending/[entity]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ReactNode } from "react";
import { Header } from "@/components/header";
import { Footer } from "@/components/Footer";

interface TrendingLayoutProps {
children: ReactNode;
}

/**
* Layout wrapper for all /trending/* routes
* Includes header and footer with proper spacing
*/
export default function TrendingLayout({
children,
}: TrendingLayoutProps) {
return (
<div className="min-h-screen bg-background flex flex-col">
<Header />
<main className="flex-1 pt-20 lg:pt-24">
{children}
</main>
<Footer />
</div>
);
}
175 changes: 175 additions & 0 deletions app/trending/[entity]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import {
Heading,
Text,
} from "@/components/ui";
import { getFullUrl } from "@/lib/constants";
import {
loadTrendingSnapshot,
isValidTrendingEntity,
isValidTrendingRange,
type TrendingRange,
} from "@/lib/trending-types";
import { TrendingPageClient } from "./trending-page-client";

/**
* Trending Page
* Route: /trending/:entity
*
* Shows trending snapshots of entities over time.
* Supported entities: organizations | projects | tech-stack | topics
* Time range is selected via ?range=daily|weekly|monthly|yearly
*
* Uses static JSON snapshots - no API calls, no database queries.
*/
export const revalidate = 3600; // 1 hour

interface PageProps {
params: Promise<{ entity: string }>;
searchParams: Promise<{
range?: string;
year?: string;
month?: string;
}>;
}

export async function generateStaticParams() {
return [
{ entity: "organizations" },
{ entity: "projects" },
{ entity: "tech-stack" },
{ entity: "topics" },
];
}

export async function generateMetadata({
params,
searchParams,
}: PageProps): Promise<Metadata> {
const { entity } = await params;
const { range, year, month } = await searchParams;

if (!isValidTrendingEntity(entity)) {
return {
title: "Trending - GSoC Organizations Guide",
};
}

const validRange: TrendingRange = isValidTrendingRange(range ?? null) ? (range as TrendingRange) : "monthly";
const entityName = entity === "tech-stack" ? "Tech Stack" : entity.charAt(0).toUpperCase() + entity.slice(1);
const rangeName = validRange.charAt(0).toUpperCase() + validRange.slice(1);
const isArchive = year !== undefined;

let title: string;
let description: string;

if (isArchive) {
const yearNum = parseInt(year || "", 10);
const monthNum = month ? parseInt(month, 10) : undefined;
const monthName = monthNum
? new Date(2000, monthNum - 1).toLocaleString("default", { month: "long" })
: "";
const archiveLabel = monthNum ? `${monthName} ${yearNum}` : `${yearNum}`;

title = `Trending ${entityName} - ${archiveLabel} Archive - GSoC Organizations Guide`;
description = `Historical trending data for ${entityName.toLowerCase()} in Google Summer of Code ${archiveLabel}. Explore archived snapshots and see how trends have evolved.`;
} else {
title = `Trending ${entityName} - ${rangeName} - GSoC Organizations Guide`;
description = `Discover trending ${entityName.toLowerCase()} in Google Summer of Code. See what's gaining momentum ${validRange === "daily" ? "today" : validRange === "weekly" ? "this week" : validRange === "monthly" ? "this month" : "this year"}.`;
}

const paramsObj = new URLSearchParams();
if (validRange !== "monthly") paramsObj.set("range", validRange);
if (year) paramsObj.set("year", year);
if (month) paramsObj.set("month", month);
const queryString = paramsObj.toString();
const canonicalUrl = getFullUrl(`/trending/${entity}${queryString ? `?${queryString}` : ""}`);

return {
title,
description,
alternates: {
canonical: canonicalUrl,
},
robots: {
index: true,
follow: true,
},
openGraph: {
title: `Trending ${entityName} - ${rangeName}`,
description,
url: canonicalUrl,
type: "website",
siteName: "GSoC Organizations Guide",
images: [
{
url: getFullUrl("/og/gsoc-organizations-guide.jpg"),
width: 1200,
height: 630,
alt: "GSoC Organizations Guide",
},
],
},
twitter: {
card: "summary_large_image",
title: `Trending ${entityName} - ${rangeName}`,
description,
images: [getFullUrl("/og/gsoc-organizations-guide.jpg")],
},
};
}

export default async function TrendingPage({
params,
searchParams,
}: PageProps) {
const { entity } = await params;
const { range, year, month } = await searchParams;

if (!isValidTrendingEntity(entity)) {
notFound();
}

const validRange: TrendingRange = isValidTrendingRange(range ?? null)
? (range as TrendingRange)
: "monthly";

const yearNum = year ? parseInt(year, 10) : undefined;
const monthNum = month ? parseInt(month, 10) : undefined;

const snapshot = await loadTrendingSnapshot(
entity,
validRange,
yearNum,
monthNum
);

if (!snapshot) {
return (
<div className="container mx-auto px-4 py-16 lg:py-24 max-w-4xl">
<div className="text-center">
<Heading variant="section" className="mb-4 text-2xl lg:text-3xl">
Trending data not available
</Heading>
<Text className="mt-4 text-muted-foreground text-base max-w-md mx-auto">
The trending snapshot for {entity} ({validRange}) has not been generated yet.
<br />
<br />
Please run the trending data generation script to populate this page.
</Text>
</div>
</div>
);
Comment on lines +148 to +163
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Improve user-facing error message.

The fallback UI mentions "run the trending data generation script," which exposes implementation details to end users. Consider a more user-friendly message.

Proposed fix
 <Text className="mt-4 text-muted-foreground text-base max-w-md mx-auto">
-  The trending snapshot for {entity} ({validRange}) has not been generated yet.
-  <br />
-  <br />
-  Please run the trending data generation script to populate this page.
+  Trending data for {entity} is not yet available for the selected time range.
+  <br />
+  <br />
+  Please check back later or try a different time range.
 </Text>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!snapshot) {
return (
<div className="container mx-auto px-4 py-16 lg:py-24 max-w-4xl">
<div className="text-center">
<Heading variant="section" className="mb-4 text-2xl lg:text-3xl">
Trending data not available
</Heading>
<Text className="mt-4 text-muted-foreground text-base max-w-md mx-auto">
The trending snapshot for {entity} ({validRange}) has not been generated yet.
<br />
<br />
Please run the trending data generation script to populate this page.
</Text>
</div>
</div>
);
if (!snapshot) {
return (
<div className="container mx-auto px-4 py-16 lg:py-24 max-w-4xl">
<div className="text-center">
<Heading variant="section" className="mb-4 text-2xl lg:text-3xl">
Trending data not available
</Heading>
<Text className="mt-4 text-muted-foreground text-base max-w-md mx-auto">
Trending data for {entity} is not yet available for the selected time range.
<br />
<br />
Please check back later or try a different time range.
</Text>
</div>
</div>
);
🤖 Prompt for AI Agents
In `@app/trending/`[entity]/page.tsx around lines 148 - 163, The fallback UI
inside the if (!snapshot) branch exposes implementation details; replace the
content in that conditional (the Heading and Text JSX that references entity and
validRange) with a user-friendly message that omits "run the trending data
generation script" and instead explains the data is temporarily unavailable
(e.g., "Trending data for {entity} ({validRange}) is currently unavailable.
Please try again later or contact support."). Keep the existing layout and use
the same Heading and Text components so styling is unchanged, and ensure any
references to entity and validRange remain for context.

}

return (
<TrendingPageClient
entity={entity}
snapshot={snapshot}
currentRange={validRange}
archiveYear={yearNum}
archiveMonth={monthNum}
/>
);
}
Loading
Loading