Skip to content

Commit 9513216

Browse files
initial pass Auto-Showcase (#798)
* initial pass * Fix typo * Show star count * Switch getDependents from runtime query to build-time prerender and parallelize GraphQL batch requests to reduce fetch time from ~44s to ~6s. * pass built-in GITHUB_TOKEN when building for prerender() --------- Co-authored-by: Sean Lynch <techniq35@gmail.com>
1 parent 9b0bc70 commit 9513216

5 files changed

Lines changed: 278 additions & 27 deletions

File tree

.github/workflows/build-preview.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828

2929
- name: Build site
3030
run: pnpm --filter docs build
31+
env:
32+
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3133

3234
- name: Upload build artifact
3335
uses: actions/upload-artifact@v4

.github/workflows/deploy-prod.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030

3131
- name: Build site
3232
run: pnpm --filter docs build
33+
env:
34+
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3335

3436
- name: Deploy to Cloudflare Pages
3537
uses: AdrianGonz97/refined-cf-pages-action@v1
Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
11
<script lang="ts">
2-
import { Button, Tooltip } from 'svelte-ux';
2+
import Showcase from './Showcase.svelte';
3+
import { getDependents } from './dependency.remote';
34

4-
import LucideGithub from '~icons/lucide/github';
5-
import LucideSquareArrowOutUpRight from '~icons/lucide/square-arrow-out-up-right'
6-
7-
import { sites } from './data';
5+
const { featuredSites, supporterSites, popularSites, otherSites } = await getDependents();
86
</script>
97

108
# Showcase
119

12-
<div class="grid grid-cols-sm gap-3">
13-
{#each sites as site}
14-
<div class="flex flex-col border border-primary/20 rounded-lg px-3 py-2 bg-linear-to-b from-primary/8 to-primary/2 backdrop-blur">
15-
<a href={site.url ?? site.source} target="_blank" class="text-lg font-medium">
16-
{site.name}
17-
</a>
18-
{#if site.description}
19-
<p class="text-sm text-surface-content/50">{site.description}</p>
20-
{/if}
21-
<div class="grow flex items-end justify-end gap-1">
22-
{#if site.source}
23-
<Button href={site.source} target="_blank" icon={LucideGithub} class="size-7 text-surface-content/50 hover:text-surface-content" />
24-
{/if}
25-
{#if site.url}
26-
<Button href={site.url} target="_blank" icon={LucideSquareArrowOutUpRight} class="size-7 text-surface-content/50 hover:text-surface-content" />
27-
{/if}
28-
</div>
29-
</div>
30-
{/each}
31-
</div>
32-
33-
[More](https://github.com/techniq/layerchart/network/dependents)
10+
## Featured
11+
12+
<Showcase sites={featuredSites} />
13+
14+
## [Supporters](https://github.com/techniq/layerchart?tab=readme-ov-file#sponsors)
15+
16+
<Showcase sites={supporterSites} />
17+
18+
## Popular
19+
20+
<Showcase sites={popularSites} />
21+
22+
## Other
23+
24+
<Showcase sites={otherSites} />
25+
26+
## [More](https://github.com/techniq/layerchart/network/dependents)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<script lang="ts">
2+
import { Button } from 'svelte-ux';
3+
import LucideGithub from '~icons/lucide/github';
4+
import LucideStar from '~icons/lucide/star';
5+
import LucideSquareArrowOutUpRight from '~icons/lucide/square-arrow-out-up-right';
6+
import type { Dependent } from './dependency.remote';
7+
8+
let { sites }: { sites: Dependent[] } = $props();
9+
</script>
10+
11+
<div class="grid grid-cols-sm gap-3">
12+
{#each sites as site}
13+
<div
14+
class="flex flex-col border border-primary/20 rounded-lg px-3 py-2 bg-linear-to-b from-primary/8 to-primary/2 backdrop-blur"
15+
>
16+
<a href={site.repourl ?? site.homepageurl} target="_blank" class="text-lg font-medium">
17+
{site.name ?? site.reponame}
18+
</a>
19+
{#if site.description}
20+
<p class="text-sm text-surface-content/50">{site.description}</p>
21+
{/if}
22+
<div class="grow flex items-end justify-end gap-1">
23+
{#if site.stars}
24+
<span class="flex items-center gap-1 text-sm text-surface-content/50 mr-auto">
25+
<LucideStar class="size-4" />
26+
{site.stars.toLocaleString()}
27+
</span>
28+
{/if}
29+
{#if site.repourl}
30+
<Button
31+
href={site.repourl}
32+
target="_blank"
33+
icon={LucideGithub}
34+
class="size-7 text-surface-content/50 hover:text-surface-content"
35+
/>
36+
{/if}
37+
{#if site.homepageurl}
38+
<Button
39+
href={site.homepageurl}
40+
target="_blank"
41+
icon={LucideSquareArrowOutUpRight}
42+
class="size-7 text-surface-content/50 hover:text-surface-content"
43+
/>
44+
{/if}
45+
</div>
46+
</div>
47+
{/each}
48+
</div>
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { prerender } from '$app/server';
2+
import { env } from '$env/dynamic/private';
3+
4+
const POPULAR_STAR_THRESHOLD = 100;
5+
const OTHER_STAR_THRESHOLD = 10;
6+
7+
export type Dependent = {
8+
name?: string;
9+
reponame?: string;
10+
owner?: string;
11+
description: string;
12+
repourl?: string;
13+
homepageurl?: string;
14+
stars?: number;
15+
};
16+
17+
export const getDependents = prerender(async () => {
18+
19+
const featuredSites: Dependent[] = [
20+
{
21+
name: 'Github Analysis',
22+
description: 'Analyze your GitHub repositories and NPM packages',
23+
repourl: 'https://github.com/techniq/github-analysis',
24+
homepageurl: 'https://github.techniq.dev'
25+
},
26+
{
27+
name: 'Strava Analysis',
28+
description: 'Analyze your Strava activities',
29+
repourl: 'https://github.com/techniq/strava-analysis',
30+
homepageurl: 'https://strava.techniq.dev'
31+
},
32+
{
33+
name: 'Zipline AI',
34+
description: 'Features, context and embeddings for real-time AI/ML',
35+
repourl: 'https://zipline.ai/',
36+
homepageurl: 'https://github.com/zipline-ai'
37+
}
38+
];
39+
40+
const supporterSites: Dependent[] = [
41+
// could this be automated? pull sponsers, check dependents by owner === sponser, and add them here?
42+
{
43+
name: 'Tenzir',
44+
description: 'Open source data pipelines for security teams',
45+
repourl: 'https://github.com/tenzir',
46+
homepageurl: 'https://tenzir.com/'
47+
},
48+
{
49+
name: 'shadcn-svelte',
50+
description: 'shadcn/ui, but for Svelte.',
51+
repourl: 'https://github.com/huntabyte/shadcn-svelte',
52+
homepageurl: 'https://shadcn-svelte.com/'
53+
},
54+
{
55+
name: 'Sky Zoo',
56+
description: 'Bluesky stats',
57+
repourl: 'https://skyzoo.blue/',
58+
homepageurl: 'https://github.com/jycouet/jyc.dev'
59+
}
60+
];
61+
62+
// These do not have a GH repo, but will be promoted by adding to the top of popular sites.
63+
const highlightedSites: Dependent[] = [
64+
{
65+
name: 'GEO audit',
66+
description: 'GEO / AI audit that tracks your visibility impact',
67+
homepageurl: 'https://www.geoaud.it/'
68+
},
69+
{
70+
name: 'RetireNumber',
71+
description: 'Get a second opinion on your retirement number.',
72+
homepageurl: 'https://retirenumber.com/'
73+
},
74+
{
75+
name: 'PowerOutage.com',
76+
description: 'Tracks, records, and aggregates power outage data across the World',
77+
homepageurl: 'https://poweroutage.com/'
78+
},
79+
{
80+
name: 'IOM UN Migration: Ukraine Regional Response',
81+
description: 'Needs, Intentions, and Border Crossings',
82+
homepageurl:
83+
'https://dtm.iom.int/online-interactive-resources/ukraine-regional-response-dashboard/index.html'
84+
},
85+
{
86+
name: 'Loyola Chicago: Center for Criminal Justice',
87+
description: 'The First Year of the Pretrial Fairness Act',
88+
homepageurl: 'https://pfa-1yr.loyolaccj.org/'
89+
},
90+
{
91+
name: 'ftop',
92+
description: 'Comperative performance metrics for Fortnite islands',
93+
homepageurl: 'https://ftop.app/'
94+
},
95+
{
96+
name: 'Nocturne',
97+
description: 'A next-generation platform for diabetes management',
98+
homepageurl: 'https://nocturne.app/'
99+
}
100+
];
101+
102+
const githubHeaders: Record<string, string> = {
103+
Accept: 'application/vnd.github.v3+json',
104+
'User-Agent': 'LayerChart docs'
105+
};
106+
107+
if (env.GITHUB_API_TOKEN) {
108+
const prefix = env.GITHUB_API_TOKEN.startsWith('ghp_') ? 'token' : 'Bearer';
109+
githubHeaders['Authorization'] = `${prefix} ${env.GITHUB_API_TOKEN}`;
110+
}
111+
112+
const totalStart = performance.now();
113+
114+
// Step 1: Find repos with "layerchart" in package.json via code search
115+
// NOTE: Code search API has a strict rate limit, so pages are fetched sequentially
116+
const repoSet = new Set<string>();
117+
let page = 1;
118+
const perPage = 100;
119+
120+
const step1Start = performance.now();
121+
while (true) {
122+
const searchUrl = `https://api.github.com/search/code?q=${encodeURIComponent('"layerchart" filename:package.json')}&per_page=${perPage}&page=${page}`;
123+
const res = await fetch(searchUrl, { headers: githubHeaders });
124+
125+
if (!res.ok) {
126+
console.error(`GitHub code search failed: ${res.status} ${res.statusText}`);
127+
break;
128+
}
129+
130+
const data = await res.json();
131+
const items = data.items ?? [];
132+
133+
for (const item of items) {
134+
const fullName = item.repository?.full_name;
135+
if (fullName && fullName !== 'techniq/layerchart') {
136+
repoSet.add(fullName);
137+
}
138+
}
139+
140+
if (items.length < perPage || repoSet.size >= data.total_count) break;
141+
page++;
142+
}
143+
console.log(
144+
`[getDependents] Step 1 - Code search: ${((performance.now() - step1Start) / 1000).toFixed(2)}s (${repoSet.size} repos found, ${page} pages)`
145+
);
146+
147+
// Step 2: Batch-fetch repo details via GitHub GraphQL (parallel)
148+
const step2Start = performance.now();
149+
const repos = [...repoSet];
150+
const batchSize = 50;
151+
152+
const batchPromises = [];
153+
for (let i = 0; i < repos.length; i += batchSize) {
154+
const batch = repos.slice(i, i + batchSize);
155+
const fragments = batch
156+
.map((fullName, idx) => {
157+
const [owner, name] = fullName.split('/');
158+
return `repo${idx}: repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) { stargazerCount description homepageUrl url owner { login } name }`;
159+
})
160+
.join('\n');
161+
162+
batchPromises.push(
163+
fetch('https://api.github.com/graphql', {
164+
method: 'POST',
165+
headers: githubHeaders,
166+
body: JSON.stringify({ query: `{ ${fragments} }` })
167+
}).then(async (res) => {
168+
if (!res.ok) return [];
169+
const { data } = await res.json();
170+
if (!data) return [];
171+
return Object.values(data)
172+
.filter(Boolean)
173+
.map((repo: any) => ({
174+
owner: repo.owner.login,
175+
reponame: repo.name,
176+
description: repo.description || null,
177+
repourl: repo.url,
178+
homepageurl: repo.homepageUrl || null,
179+
stars: repo.stargazerCount
180+
}));
181+
})
182+
);
183+
}
184+
185+
const batchResults = await Promise.all(batchPromises);
186+
const dependents: Dependent[] = batchResults.flat();
187+
188+
console.log(
189+
`[getDependents] Step 2 - GraphQL details: ${((performance.now() - step2Start) / 1000).toFixed(2)}s (${dependents.length} repos, ${batchPromises.length} batches)`
190+
);
191+
console.log(`[getDependents] Total: ${((performance.now() - totalStart) / 1000).toFixed(2)}s`);
192+
193+
dependents
194+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0)) // Sort by stars descending
195+
.filter((d) => featuredSites.some((f) => f.reponame === d.reponame)) // Filter out any featured sites
196+
.filter((d) => supporterSites.some((s) => s.reponame === d.reponame)); // Filter out any supporter sites
197+
const popularSites = [
198+
...highlightedSites,
199+
...dependents.filter((d) => (d.stars ?? 0) >= POPULAR_STAR_THRESHOLD)
200+
];
201+
const otherSites = dependents.filter(
202+
(d) => (d.stars ?? 0) >= OTHER_STAR_THRESHOLD && (d.stars ?? 0) < POPULAR_STAR_THRESHOLD
203+
);
204+
205+
return { featuredSites, supporterSites, popularSites, otherSites };
206+
});

0 commit comments

Comments
 (0)