diff --git a/.github/workflows/scheduled-deploy.yml b/.github/workflows/scheduled-deploy.yml index 5ad8535..1b9de6d 100644 --- a/.github/workflows/scheduled-deploy.yml +++ b/.github/workflows/scheduled-deploy.yml @@ -33,7 +33,9 @@ jobs: run: npm ci - name: Build site - run: npm run build + run: npm run build:with-stats + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy to Cloudflare uses: cloudflare/wrangler-action@v3 diff --git a/package-lock.json b/package-lock.json index 2fd0a36..3675f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "sanitize-html": "^2.17.0", "satori": "^0.19.1", "sharp": "^0.34.5", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "yaml": "^2.8.2" }, "devDependencies": { "@types/markdown-it": "^14.1.2", @@ -7510,6 +7511,22 @@ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index c96610f..c6f1011 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,15 @@ "scripts": { "dev": "node scripts/copy-originals.js && astro dev", "build": "node scripts/copy-originals.js && astro build", + "build:with-stats": "node scripts/fetch-project-stats.js && node scripts/copy-originals.js && astro build", "preview": "astro preview", "astro": "astro", "new": "node scripts/new-post.js", "new:project": "node scripts/new-project.js", "cover": "node scripts/generate-cover.js", "cover:project": "node scripts/generate-project-cover.js", - "copy-originals": "node scripts/copy-originals.js" + "copy-originals": "node scripts/copy-originals.js", + "fetch:stats": "node scripts/fetch-project-stats.js" }, "dependencies": { "@astrojs/rss": "^4.0.14", @@ -26,7 +28,8 @@ "sanitize-html": "^2.17.0", "satori": "^0.19.1", "sharp": "^0.34.5", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "yaml": "^2.8.2" }, "devDependencies": { "@types/markdown-it": "^14.1.2", diff --git a/scripts/fetch-project-stats.js b/scripts/fetch-project-stats.js new file mode 100644 index 0000000..01a0405 --- /dev/null +++ b/scripts/fetch-project-stats.js @@ -0,0 +1,314 @@ +/** + * Pre-build script to fetch all project stats from GitHub, VS Marketplace, and NuGet + * Run this before the Astro build to bake stats into the static site + * + * Stats fetched by category: + * - nuget-package: GitHub + NuGet + * - vs-extension: GitHub + VS Marketplace + * - vscode-extension: GitHub + VS Marketplace + * - cli-tool: GitHub only + * - desktop-app: GitHub only + * - documentation: GitHub only + * - github-action: GitHub only (marketplace stats could be added later) + */ + +import { readdir, readFile, writeFile, mkdir } from "fs/promises"; +import { existsSync } from "fs"; +import { join } from "path"; +import { parse } from "yaml"; + +const PROJECTS_DIR = "src/content/projects"; +const STATS_OUTPUT = "src/data/project-stats.json"; + +/** + * Parse frontmatter from markdown file + */ +function parseFrontmatter(content) { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) return null; + return parse(match[1]); +} + +/** + * Extract owner/repo from GitHub URL + */ +function parseGitHubUrl(repoUrl) { + const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/); + if (!match) return null; + return { owner: match[1], repo: match[2].replace(/\.git$/, "") }; +} + +/** + * Fetch GitHub repository stats + */ +async function fetchGitHubStats(repoUrl) { + try { + const parsed = parseGitHubUrl(repoUrl); + if (!parsed) return null; + + const { owner, repo } = parsed; + const token = process.env.GITHUB_TOKEN; + + const headers = { + Accept: "application/vnd.github.v3+json", + "User-Agent": "codingwithcalvin.net", + }; + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + // Fetch repo info and latest release in parallel + const [repoResponse, releaseResponse] = await Promise.all([ + fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }), + fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { + headers, + }), + ]); + + if (!repoResponse.ok) { + console.warn(` GitHub API error for ${repoUrl}: ${repoResponse.status}`); + return null; + } + + const repoData = await repoResponse.json(); + + let latestRelease = null; + if (releaseResponse.ok) { + const releaseData = await releaseResponse.json(); + latestRelease = { + version: releaseData.tag_name, + publishedAt: releaseData.published_at, + url: releaseData.html_url, + }; + } + + return { + stars: repoData.stargazers_count, + forks: repoData.forks_count, + openIssues: repoData.open_issues_count, + latestRelease, + }; + } catch (error) { + console.warn(` Failed to fetch GitHub stats for ${repoUrl}:`, error.message); + return null; + } +} + +/** + * Fetch VS Marketplace extension stats + */ +async function fetchVSMarketplaceStats(extensionId) { + try { + const response = await fetch( + "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json;api-version=7.1-preview.1", + }, + body: JSON.stringify({ + filters: [ + { + criteria: [{ filterType: 7, value: extensionId }], + }, + ], + flags: 914, // Include statistics + }), + } + ); + + if (!response.ok) { + console.warn(` VS Marketplace API error for ${extensionId}: ${response.status}`); + return null; + } + + const data = await response.json(); + const ext = data?.results?.[0]?.extensions?.[0]; + + if (!ext) { + console.warn(` Extension not found: ${extensionId}`); + return null; + } + + const stats = ext.statistics || []; + const getStatValue = (name) => { + const stat = stats.find((s) => s.statisticName === name); + return stat?.value; + }; + + return { + installs: getStatValue("install"), + downloads: getStatValue("downloadCount"), + rating: getStatValue("averagerating"), + ratingCount: getStatValue("ratingcount"), + lastUpdated: ext.lastUpdated, + }; + } catch (error) { + console.warn(` Failed to fetch VS Marketplace stats for ${extensionId}:`, error.message); + return null; + } +} + +/** + * Fetch NuGet package stats + */ +async function fetchNuGetStats(packageId) { + try { + const searchUrl = `https://azuresearch-usnc.nuget.org/query?q=packageid:${packageId}&prerelease=true&take=1`; + const response = await fetch(searchUrl); + + if (!response.ok) { + console.warn(` NuGet API error for ${packageId}: ${response.status}`); + return null; + } + + const data = await response.json(); + const pkg = data?.data?.[0]; + + if (!pkg) { + console.warn(` NuGet package not found: ${packageId}`); + return null; + } + + return { + downloads: pkg.totalDownloads, + latestVersion: pkg.versions?.[pkg.versions.length - 1]?.version, + }; + } catch (error) { + console.warn(` Failed to fetch NuGet stats for ${packageId}:`, error.message); + return null; + } +} + +/** + * Extract extension/package ID from marketplace URL + */ +function extractMarketplaceId(url, type) { + if (type === "vs-marketplace") { + const match = url.match(/itemName=([^&]+)/); + return match?.[1] || null; + } else if (type === "nuget") { + const match = url.match(/packages\/([^\/]+)/); + return match?.[1] || null; + } + return null; +} + +/** + * Determine what stats to fetch based on project category + */ +function getStatsToFetch(category) { + switch (category) { + case "nuget-package": + return { github: true, marketplace: "nuget" }; + case "vs-extension": + case "vscode-extension": + return { github: true, marketplace: "vs-marketplace" }; + case "cli-tool": + case "desktop-app": + case "documentation": + case "github-action": + default: + return { github: true, marketplace: null }; + } +} + +async function main() { + console.log("\nFetching project stats...\n"); + + const token = process.env.GITHUB_TOKEN; + if (token) { + console.log("Using GITHUB_TOKEN for API requests\n"); + } else { + console.warn("Warning: GITHUB_TOKEN not set, may hit rate limits\n"); + } + + // Read all project directories + const projectDirs = await readdir(PROJECTS_DIR); + const stats = {}; + + for (const dir of projectDirs) { + const indexPath = join(PROJECTS_DIR, dir, "index.md"); + if (!existsSync(indexPath)) continue; + + console.log(`Processing: ${dir}`); + + const content = await readFile(indexPath, "utf-8"); + const frontmatter = parseFrontmatter(content); + if (!frontmatter) { + console.warn(` Could not parse frontmatter`); + continue; + } + + const projectStats = { + slug: dir, + github: null, + marketplace: null, + }; + + const category = frontmatter.category; + const statsConfig = getStatsToFetch(category); + + console.log(` Category: ${category} → GitHub: ${statsConfig.github}, Marketplace: ${statsConfig.marketplace || "none"}`); + + // Fetch GitHub stats (all projects) + if (statsConfig.github && frontmatter.repoUrl) { + console.log(` Fetching GitHub stats...`); + projectStats.github = await fetchGitHubStats(frontmatter.repoUrl); + if (projectStats.github) { + console.log(` Stars: ${projectStats.github.stars}, Forks: ${projectStats.github.forks}`); + } + } + + // Fetch marketplace stats based on category + if (statsConfig.marketplace && frontmatter.marketplace?.url) { + const { url, type } = frontmatter.marketplace; + const id = extractMarketplaceId(url, type); + + if (id) { + if (statsConfig.marketplace === "nuget" && type === "nuget") { + console.log(` Fetching NuGet stats for ${id}...`); + projectStats.marketplace = await fetchNuGetStats(id); + } else if (statsConfig.marketplace === "vs-marketplace" && type === "vs-marketplace") { + console.log(` Fetching VS Marketplace stats for ${id}...`); + projectStats.marketplace = await fetchVSMarketplaceStats(id); + } + + if (projectStats.marketplace) { + const downloads = projectStats.marketplace.downloads ?? projectStats.marketplace.installs; + if (downloads) { + console.log(` Downloads/Installs: ${downloads}`); + } + if (projectStats.marketplace.rating) { + console.log(` Rating: ${projectStats.marketplace.rating.toFixed(1)}`); + } + } + } + } + + stats[dir] = projectStats; + } + + // Ensure output directory exists + const outputDir = "src/data"; + if (!existsSync(outputDir)) { + await mkdir(outputDir, { recursive: true }); + } + + // Write stats to JSON file + const output = { + generatedAt: new Date().toISOString(), + projects: stats, + }; + + await writeFile(STATS_OUTPUT, JSON.stringify(output, null, 2)); + + console.log(`\nStats written to ${STATS_OUTPUT}`); + console.log(`Total projects processed: ${Object.keys(stats).length}`); +} + +main().catch((err) => { + console.error("Error:", err); + process.exit(1); +}); diff --git a/src/content/projects/otel4vsix/index.md b/src/content/projects/otel4vsix/index.md index a44c139..eb8acef 100644 --- a/src/content/projects/otel4vsix/index.md +++ b/src/content/projects/otel4vsix/index.md @@ -11,7 +11,7 @@ startDate: "2025-12-23" stars: 0 marketplace: type: "nuget" - url: "https://www.nuget.org/packages/Otel4Vsix" + url: "https://www.nuget.org/packages/CodingWithCalvin.Otel4Vsix" --- diff --git a/src/content/projects/vsixsdk/index.md b/src/content/projects/vsixsdk/index.md index 06c9f4d..27d3e47 100644 --- a/src/content/projects/vsixsdk/index.md +++ b/src/content/projects/vsixsdk/index.md @@ -11,7 +11,7 @@ startDate: "2025-12-24" stars: 21 marketplace: type: "nuget" - url: "https://www.nuget.org/packages/VsixSdk" + url: "https://www.nuget.org/packages/CodingWithCalvin.VsixSdk" --- diff --git a/src/data/project-stats.json b/src/data/project-stats.json new file mode 100644 index 0000000..252d9df --- /dev/null +++ b/src/data/project-stats.json @@ -0,0 +1,151 @@ +{ + "generatedAt": "2026-01-31T16:25:57.846Z", + "projects": { + "dtvem-cli": { + "slug": "dtvem-cli", + "github": null, + "marketplace": null + }, + "gha-jbmarketplacepublisher": { + "slug": "gha-jbmarketplacepublisher", + "github": null, + "marketplace": null + }, + "gha-vsmarketplacepublisher": { + "slug": "gha-vsmarketplacepublisher", + "github": null, + "marketplace": null + }, + "otel4vsix": { + "slug": "otel4vsix", + "github": null, + "marketplace": { + "downloads": 734, + "latestVersion": "0.2.2" + } + }, + "rnr-cli": { + "slug": "rnr-cli", + "github": null, + "marketplace": null + }, + "vs-breakpointnotifier": { + "slug": "vs-breakpointnotifier", + "github": null, + "marketplace": { + "installs": 127, + "lastUpdated": "2026-01-30T17:03:04.64Z" + } + }, + "vs-couchbaseexplorer": { + "slug": "vs-couchbaseexplorer", + "github": null, + "marketplace": { + "installs": 16, + "lastUpdated": "2026-01-30T17:13:31.22Z" + } + }, + "vs-debugalizers": { + "slug": "vs-debugalizers", + "github": null, + "marketplace": { + "installs": 48, + "lastUpdated": "2026-01-30T17:03:13.72Z" + } + }, + "vs-gitranger": { + "slug": "vs-gitranger", + "github": null, + "marketplace": { + "installs": 147, + "lastUpdated": "2026-01-30T17:03:37.8Z" + } + }, + "vs-mcpserver": { + "slug": "vs-mcpserver", + "github": null, + "marketplace": { + "installs": 207, + "rating": 5, + "ratingCount": 1, + "lastUpdated": "2026-01-30T17:04:41.153Z" + } + }, + "vs-openbinfolder": { + "slug": "vs-openbinfolder", + "github": null, + "marketplace": { + "installs": 905, + "rating": 5, + "ratingCount": 1, + "lastUpdated": "2026-01-30T17:05:33.84Z" + } + }, + "vs-openinnotepadplusplus": { + "slug": "vs-openinnotepadplusplus", + "github": null, + "marketplace": { + "installs": 2780, + "rating": 4.5, + "ratingCount": 2, + "lastUpdated": "2026-01-31T15:07:23.477Z" + } + }, + "vs-projectrenamifier": { + "slug": "vs-projectrenamifier", + "github": null, + "marketplace": { + "installs": 70, + "lastUpdated": "2026-01-30T17:03:37.367Z" + } + }, + "vs-superclean": { + "slug": "vs-superclean", + "github": null, + "marketplace": { + "installs": 697, + "lastUpdated": "2026-01-30T17:05:47.96Z" + } + }, + "vs-vsixmanifestdesigner": { + "slug": "vs-vsixmanifestdesigner", + "github": null, + "marketplace": { + "installs": 4, + "lastUpdated": "2026-01-30T17:03:15.773Z" + } + }, + "vsc-mcpserver": { + "slug": "vsc-mcpserver", + "github": null, + "marketplace": { + "installs": 15, + "downloads": 34, + "lastUpdated": "2026-01-14T16:32:45.703Z" + } + }, + "vscwhere": { + "slug": "vscwhere", + "github": null, + "marketplace": null + }, + "vsix-guide": { + "slug": "vsix-guide", + "github": null, + "marketplace": null + }, + "vsixsdk": { + "slug": "vsixsdk", + "github": null, + "marketplace": { + "downloads": 556, + "latestVersion": "0.4.0" + } + }, + "vstoolbox": { + "slug": "vstoolbox", + "github": null, + "marketplace": null + } + } +} \ No newline at end of file diff --git a/src/lib/marketplace-stats.ts b/src/lib/marketplace-stats.ts deleted file mode 100644 index 313e675..0000000 --- a/src/lib/marketplace-stats.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Fetch marketplace statistics at build time - */ - -export interface MarketplaceStats { - downloads?: number; - installs?: number; - rating?: number; - ratingCount?: number; - lastUpdated?: string; -} - -export interface GitHubRelease { - version: string; - publishedAt: string; - url: string; -} - -export interface GitHubStats { - stars: number; - forks: number; - openIssues: number; - latestRelease?: GitHubRelease; -} - -/** - * Extract owner/repo from GitHub URL - */ -function parseGitHubUrl(repoUrl: string): { owner: string; repo: string } | null { - const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/); - if (!match) return null; - return { owner: match[1], repo: match[2] }; -} - -/** - * Fetch GitHub repository stats including stars, forks, and latest release - */ -export async function fetchGitHubStats(repoUrl: string): Promise { - try { - const parsed = parseGitHubUrl(repoUrl); - if (!parsed) return undefined; - - const { owner, repo } = parsed; - - // Use GITHUB_TOKEN if available to avoid rate limits - const token = import.meta.env.GITHUB_TOKEN || process.env.GITHUB_TOKEN; - const headers: Record = { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': 'codingwithcalvin.net', - }; - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - // Fetch repo info and latest release in parallel - const [repoResponse, releaseResponse] = await Promise.all([ - fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }), - fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { headers }) - ]); - - if (!repoResponse.ok) { - console.warn(`GitHub API error for ${repoUrl}: ${repoResponse.status}`); - return undefined; - } - - const repoData = await repoResponse.json(); - - let latestRelease: GitHubRelease | undefined; - if (releaseResponse.ok) { - const releaseData = await releaseResponse.json(); - latestRelease = { - version: releaseData.tag_name, - publishedAt: releaseData.published_at, - url: releaseData.html_url, - }; - } - - return { - stars: repoData.stargazers_count, - forks: repoData.forks_count, - openIssues: repoData.open_issues_count, - latestRelease, - }; - } catch (error) { - console.warn(`Failed to fetch GitHub stats for ${repoUrl}:`, error); - return undefined; - } -} - -/** - * Fetch VS Marketplace extension stats - */ -export async function fetchVSMarketplaceStats(extensionId: string): Promise { - try { - // extensionId format: "CodingWithCalvin.VS-MCPServer" - const [publisher, extension] = extensionId.split('.'); - - const response = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json;api-version=7.1-preview.1', - }, - body: JSON.stringify({ - filters: [{ - criteria: [ - { filterType: 7, value: extensionId } - ] - }], - flags: 914 // Include statistics - }) - }); - - if (!response.ok) { - console.warn(`VS Marketplace API error for ${extensionId}: ${response.status}`); - return {}; - } - - const data = await response.json(); - const ext = data?.results?.[0]?.extensions?.[0]; - - if (!ext) { - console.warn(`Extension not found: ${extensionId}`); - return {}; - } - - const stats = ext.statistics || []; - const getStatValue = (name: string) => { - const stat = stats.find((s: any) => s.statisticName === name); - return stat?.value; - }; - - return { - installs: getStatValue('install'), - downloads: getStatValue('downloadCount'), - rating: getStatValue('averagerating'), - ratingCount: getStatValue('ratingcount'), - lastUpdated: ext.lastUpdated, - }; - } catch (error) { - console.warn(`Failed to fetch VS Marketplace stats for ${extensionId}:`, error); - return {}; - } -} - -/** - * Fetch NuGet package stats using the search API - */ -export async function fetchNuGetStats(packageId: string): Promise { - try { - // Use the search API which is more reliable - const searchUrl = `https://azuresearch-usnc.nuget.org/query?q=packageid:${packageId}&prerelease=true&take=1`; - const response = await fetch(searchUrl); - - if (!response.ok) { - console.warn(`NuGet API error for ${packageId}: ${response.status}`); - return {}; - } - - const data = await response.json(); - const pkg = data?.data?.[0]; - - if (!pkg) { - console.warn(`NuGet package not found: ${packageId}`); - return {}; - } - - return { - downloads: pkg.totalDownloads, - lastUpdated: pkg.versions?.[pkg.versions.length - 1]?.version, - }; - } catch (error) { - console.warn(`Failed to fetch NuGet stats for ${packageId}:`, error); - return {}; - } -} - -/** - * Extract extension/package ID from marketplace URL - */ -export function extractMarketplaceId(url: string, type: string): string | null { - if (type === 'vs-marketplace') { - // https://marketplace.visualstudio.com/items?itemName=CodingWithCalvin.VS-MCPServer - const match = url.match(/itemName=([^&]+)/); - return match?.[1] || null; - } else if (type === 'nuget') { - // https://www.nuget.org/packages/VsixSdk - const match = url.match(/packages\/([^\/]+)/); - return match?.[1] || null; - } - return null; -} - -/** - * Fetch stats based on marketplace type - */ -export async function fetchMarketplaceStats( - url: string, - type: 'vs-marketplace' | 'nuget' | 'npm' | 'other' -): Promise { - const id = extractMarketplaceId(url, type); - if (!id) return {}; - - switch (type) { - case 'vs-marketplace': - return fetchVSMarketplaceStats(id); - case 'nuget': - return fetchNuGetStats(id); - default: - return {}; - } -} diff --git a/src/lib/projects.ts b/src/lib/projects.ts index 7f280a0..77c347f 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -1,7 +1,9 @@ import type { ImageMetadata } from 'astro'; import { getCollection } from 'astro:content'; import defaultCover from '../assets/default-cover.png'; -import { fetchMarketplaceStats, fetchGitHubStats, type MarketplaceStats, type GitHubStats } from './marketplace-stats'; + +// Import pre-fetched stats (generated by scripts/fetch-project-stats.js) +import projectStatsData from '../data/project-stats.json'; // Import all cover images from project directories const coverImages = import.meta.glob<{ default: ImageMetadata }>( @@ -9,6 +11,28 @@ const coverImages = import.meta.glob<{ default: ImageMetadata }>( { eager: true } ); +export interface MarketplaceStats { + downloads?: number; + installs?: number; + rating?: number; + ratingCount?: number; + lastUpdated?: string; + latestVersion?: string; +} + +export interface GitHubRelease { + version: string; + publishedAt: string; + url: string; +} + +export interface GitHubStats { + stars: number; + forks: number; + openIssues: number; + latestRelease?: GitHubRelease; +} + export type ProjectWithImage = Awaited>>[number] & { resolvedImage: ImageMetadata; marketplaceStats?: MarketplaceStats; @@ -30,6 +54,23 @@ export function getProjectImage(project: Awaited { /** * Get all projects with resolved cover images AND marketplace/GitHub stats - * Use this for pages that need live data + * Stats are loaded from pre-fetched cache (generated at build time) */ export async function getProjectsWithStats(): Promise { const projects = await getCollection('projects'); - // Fetch all stats in parallel - const projectsWithStats = await Promise.all( - projects.map(async (project) => { - // Fetch marketplace and GitHub stats in parallel - const [marketplaceStats, githubStats] = await Promise.all([ - project.data.marketplace - ? fetchMarketplaceStats(project.data.marketplace.url, project.data.marketplace.type) - : Promise.resolve(undefined), - fetchGitHubStats(project.data.repoUrl), - ]); - - return { - ...project, - resolvedImage: getProjectImage(project), - marketplaceStats, - githubStats, - }; - }) - ); - - return projectsWithStats; + return projects.map((project) => { + const { github, marketplace } = getCachedStats(project.id); + + return { + ...project, + resolvedImage: getProjectImage(project), + marketplaceStats: marketplace || undefined, + githubStats: github || undefined, + }; + }); } /** diff --git a/src/pages/projects/[...page].astro b/src/pages/projects/[...page].astro index b01bed6..acf0b23 100644 --- a/src/pages/projects/[...page].astro +++ b/src/pages/projects/[...page].astro @@ -3,13 +3,15 @@ import type { GetStaticPaths } from 'astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; import ProjectCard from '../../components/ProjectCard.astro'; import Pagination from '../../components/Pagination.astro'; -import { getProjectsWithImages, getProjectSlug } from '../../lib/projects'; +import { getProjectsWithStats, getProjectSlug } from '../../lib/projects'; export const getStaticPaths = (async ({ paginate }) => { - const allProjects = await getProjectsWithImages(); - // Sort by GitHub stars (highest first) + const allProjects = await getProjectsWithStats(); + // Sort by GitHub stars (highest first), using live stats when available const sortedProjects = allProjects.sort((a, b) => { - return (b.data.stars ?? 0) - (a.data.stars ?? 0); + const aStars = a.githubStats?.stars ?? a.data.stars ?? 0; + const bStars = b.githubStats?.stars ?? b.data.stars ?? 0; + return bStars - aStars; }); return paginate(sortedProjects, { pageSize: 9 }); @@ -27,18 +29,22 @@ const { page } = Astro.props;

- {page.data.map(project => ( - - ))} + {page.data.map(project => { + const stars = project.githubStats?.stars ?? project.data.stars; + const downloads = project.marketplaceStats?.downloads ?? project.marketplaceStats?.installs ?? project.data.downloads; + return ( + + ); + })}
{page.lastPage > 1 && (