@grove-dev/astro 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -26
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +1 -103
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +1 -28
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +1 -71
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -57
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +1 -198
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +1 -19
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +1 -30
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +1 -36
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +1 -75
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +1 -113
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +1 -379
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -10
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +1 -21
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +21 -9
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +2 -2
- package/src/components/DirectoryIndexClient.astro +240 -0
- package/src/components/FilterGroupMenu.astro +8 -4
- package/src/components/FinalCta.astro +41 -0
- package/src/components/Hero.astro +15 -18
- package/src/components/Icon.astro +49 -7
- package/src/components/RefinePanel.astro +7 -7
- package/src/components/SmartLensTabs.astro +5 -4
- package/src/components/StackGrid.astro +1 -1
- package/src/components/SubmissionClient.astro +133 -0
- package/src/index.ts +11 -1
- package/src/layouts/BaseLayout.astro +19 -1
- package/src/layouts/Footer.astro +25 -56
- package/src/layouts/Header.astro +33 -15
- package/src/layouts/ThemeToggle.astro +7 -3
- package/src/lib/display.ts +1 -111
- package/src/lib/format.ts +1 -64
- package/src/lib/lenses.ts +1 -220
- package/src/lib/repo.ts +1 -32
- package/src/lib/scores.ts +1 -90
- package/src/lib/search.ts +1 -418
- package/src/lib/taxonomy-counts.ts +1 -26
- package/{templates/default/src/data/records.ts → src/server/directory.ts} +12 -8
- package/src/server/index.ts +2 -0
- package/src/server/models.ts +432 -0
- package/dist/template-routes.test.d.ts +0 -2
- package/dist/template-routes.test.d.ts.map +0 -1
- package/dist/template-routes.test.js +0 -63
- package/dist/template-routes.test.js.map +0 -1
- package/dist/theme.test.d.ts +0 -2
- package/dist/theme.test.d.ts.map +0 -1
- package/dist/theme.test.js +0 -60
- package/dist/theme.test.js.map +0 -1
- package/src/template-routes.test.ts +0 -103
- package/src/theme.test.ts +0 -76
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
- package/templates/default/.github/pull_request_template.md +0 -27
- package/templates/default/.github/workflows/build.yml +0 -53
- package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -46
- package/templates/default/.github/workflows/daily-refresh.yml +0 -51
- package/templates/default/.github/workflows/sync-contributors.yml +0 -51
- package/templates/default/.github/workflows/sync-github-metadata.yml +0 -56
- package/templates/default/.github/workflows/validate-data.yml +0 -33
- package/templates/default/astro.config.mjs +0 -25
- package/templates/default/content/pages/about.md +0 -66
- package/templates/default/content/pages/methodology.md +0 -103
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/data/records/coolify.yml +0 -46
- package/templates/default/data/records/gitea.yml +0 -46
- package/templates/default/data/records/hoppscotch.yml +0 -47
- package/templates/default/data/records/inventree.yml +0 -47
- package/templates/default/data/records/label-studio.yml +0 -46
- package/templates/default/data/records/logseq.yml +0 -48
- package/templates/default/data/records/private-gpt.yml +0 -47
- package/templates/default/data/records/wakapi.yml +0 -44
- package/templates/default/data/taxonomy/categories.yml +0 -47
- package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
- package/templates/default/data/taxonomy/platforms.yml +0 -27
- package/templates/default/data/taxonomy/stacks.yml +0 -41
- package/templates/default/grove.config.ts +0 -36
- package/templates/default/package.json +0 -43
- package/templates/default/public/icons/platforms/android.svg +0 -1
- package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
- package/templates/default/public/icons/platforms/apple-light.svg +0 -1
- package/templates/default/public/icons/platforms/chrome.svg +0 -1
- package/templates/default/public/icons/platforms/chromeos.svg +0 -5
- package/templates/default/public/icons/platforms/desktop.svg +0 -4
- package/templates/default/public/icons/platforms/embedded.svg +0 -5
- package/templates/default/public/icons/platforms/ios.svg +0 -1
- package/templates/default/public/icons/platforms/linux.svg +0 -1
- package/templates/default/public/icons/platforms/macos.svg +0 -1
- package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
- package/templates/default/public/icons/platforms/web.svg +0 -5
- package/templates/default/public/icons/platforms/windows.svg +0 -6
- package/templates/default/public/icons/stacks/android.svg +0 -1
- package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
- package/templates/default/public/icons/stacks/apple-light.svg +0 -1
- package/templates/default/public/icons/stacks/bun.svg +0 -1
- package/templates/default/public/icons/stacks/capacitor.svg +0 -14
- package/templates/default/public/icons/stacks/clojure.svg +0 -1
- package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
- package/templates/default/public/icons/stacks/dart.svg +0 -1
- package/templates/default/public/icons/stacks/deno.svg +0 -1
- package/templates/default/public/icons/stacks/django.svg +0 -1
- package/templates/default/public/icons/stacks/docker.svg +0 -1
- package/templates/default/public/icons/stacks/firebase.svg +0 -1
- package/templates/default/public/icons/stacks/flutter.svg +0 -1
- package/templates/default/public/icons/stacks/go.svg +0 -1
- package/templates/default/public/icons/stacks/graphql.svg +0 -1
- package/templates/default/public/icons/stacks/ionic.svg +0 -4
- package/templates/default/public/icons/stacks/java.svg +0 -1
- package/templates/default/public/icons/stacks/javascript.svg +0 -1
- package/templates/default/public/icons/stacks/kotlin.svg +0 -1
- package/templates/default/public/icons/stacks/llm.svg +0 -1
- package/templates/default/public/icons/stacks/mongodb.svg +0 -1
- package/templates/default/public/icons/stacks/node.js.svg +0 -1
- package/templates/default/public/icons/stacks/nodejs.svg +0 -1
- package/templates/default/public/icons/stacks/python.svg +0 -1
- package/templates/default/public/icons/stacks/rag.svg +0 -7
- package/templates/default/public/icons/stacks/react-native.svg +0 -1
- package/templates/default/public/icons/stacks/react.svg +0 -1
- package/templates/default/public/icons/stacks/rust.svg +0 -1
- package/templates/default/public/icons/stacks/solidity.svg +0 -1
- package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
- package/templates/default/public/icons/stacks/swift.svg +0 -1
- package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
- package/templates/default/public/icons/stacks/typescript.svg +0 -1
- package/templates/default/public/icons/stacks/vue.svg +0 -1
- package/templates/default/public/llms-full.txt +0 -83
- package/templates/default/public/llms.txt +0 -11
- package/templates/default/public/og-image.svg +0 -48
- package/templates/default/public/robots.txt +0 -53
- package/templates/default/public/sitemap.xml +0 -63
- package/templates/default/src/pages/404.astro +0 -64
- package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -691
- package/templates/default/src/pages/[slug]/index.astro +0 -482
- package/templates/default/src/pages/about.astro +0 -189
- package/templates/default/src/pages/apps/[recordSlug].astro +0 -34
- package/templates/default/src/pages/contributors.astro +0 -175
- package/templates/default/src/pages/index.astro +0 -233
- package/templates/default/src/pages/sitemap.xml.ts +0 -89
- package/templates/default/src/pages/submit.astro +0 -281
- package/templates/default/src/styles/global.css +0 -33
- package/templates/default/tsconfig.json +0 -9
- package/templates/default/wrangler.jsonc +0 -28
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /apps/<slug> — V0-published alias URL → V1 canonical.
|
|
4
|
-
*
|
|
5
|
-
* The canonical V1 detail URL is `/<blueprint>/<recordSlug>` (e.g.
|
|
6
|
-
* `/projects/coolify`). The V0 published Astro template exposed
|
|
7
|
-
* `/apps/<slug>` as a static alias to keep the openapps-style URL
|
|
8
|
-
* working for project-directory blueprints. Now that the V1
|
|
9
|
-
* template uses the blueprint-aware dynamic route everywhere,
|
|
10
|
-
* this page is a pure redirect: any `/apps/<slug>` request is
|
|
11
|
-
* sent to the blueprint-canonical path.
|
|
12
|
-
*/
|
|
13
|
-
import { indexSlug, fullRecords, recordBySlug } from "../../data/records";
|
|
14
|
-
|
|
15
|
-
export async function getStaticPaths() {
|
|
16
|
-
if (indexSlug() === "apps") return [];
|
|
17
|
-
return fullRecords.map((r) => ({ params: { recordSlug: r.slug } }));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const slug = indexSlug();
|
|
21
|
-
const recordSlug = Astro.params.recordSlug ?? "";
|
|
22
|
-
|
|
23
|
-
if (slug !== "projects") {
|
|
24
|
-
return Astro.redirect(`/${slug}/${recordSlug}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const record = recordBySlug(recordSlug);
|
|
28
|
-
if (!record) {
|
|
29
|
-
return Astro.redirect(`/${slug}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Canonical V1 URL.
|
|
33
|
-
return Astro.redirect(`/${slug}/${recordSlug}`, 301);
|
|
34
|
-
---
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Contributors page — full list with per-user contribution counts.
|
|
4
|
-
*
|
|
5
|
-
* Generic: works for any blueprint (project-directory, resource-hub,
|
|
6
|
-
* ecosystem-map) and any source of contributors. Reads from
|
|
7
|
-
* `data/generated/contributors.json` if present, otherwise renders
|
|
8
|
-
* a graceful empty state. The file is produced by the
|
|
9
|
-
* `sync-contributors` GitHub Action (see `.github/workflows/`).
|
|
10
|
-
*
|
|
11
|
-
* Falls back to deriving contributors from the records themselves
|
|
12
|
-
* (record.github.fullName → owner handle) when no JSON file exists,
|
|
13
|
-
* so a fresh scaffold still shows something useful.
|
|
14
|
-
*/
|
|
15
|
-
import siteConfig from "../../data/generated/site-config.json";
|
|
16
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
17
|
-
import { projects } from "../data/records";
|
|
18
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { dirname, resolve } from "node:path";
|
|
21
|
-
|
|
22
|
-
interface Contributor {
|
|
23
|
-
username: string;
|
|
24
|
-
name?: string;
|
|
25
|
-
avatarUrl?: string;
|
|
26
|
-
profileUrl?: string;
|
|
27
|
-
contributions?: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
-
const contributorsPath = [
|
|
32
|
-
resolve(here, "..", "..", "data", "generated", "contributors.json"),
|
|
33
|
-
resolve(process.cwd(), "data", "generated", "contributors.json"),
|
|
34
|
-
].find((p) => existsSync(p));
|
|
35
|
-
|
|
36
|
-
let contributors: Contributor[] = [];
|
|
37
|
-
if (contributorsPath) {
|
|
38
|
-
try {
|
|
39
|
-
const parsed = JSON.parse(readFileSync(contributorsPath, "utf8")) as {
|
|
40
|
-
contributors?: Contributor[];
|
|
41
|
-
};
|
|
42
|
-
contributors = parsed.contributors ?? [];
|
|
43
|
-
} catch {
|
|
44
|
-
contributors = [];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Fallback: derive owners from record.github.fullName when no JSON
|
|
49
|
-
// is present (fresh scaffold before the first sync run).
|
|
50
|
-
if (contributors.length === 0) {
|
|
51
|
-
const seen = new Map<string, Contributor>();
|
|
52
|
-
for (const r of projects) {
|
|
53
|
-
const full = r.github?.fullName;
|
|
54
|
-
if (!full || !full.includes("/")) continue;
|
|
55
|
-
const username = full.split("/")[0];
|
|
56
|
-
if (seen.has(username)) continue;
|
|
57
|
-
seen.set(username, {
|
|
58
|
-
username,
|
|
59
|
-
avatarUrl: `https://avatars.githubusercontent.com/${username}?v=4&s=80`,
|
|
60
|
-
profileUrl: `https://github.com/${username}`,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
contributors = [...seen.values()].sort((a, b) =>
|
|
64
|
-
a.username.localeCompare(b.username),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const total = contributors.length;
|
|
69
|
-
const sorted = [...contributors].sort((a, b) => {
|
|
70
|
-
const ac = a.contributions ?? 0;
|
|
71
|
-
const bc = b.contributions ?? 0;
|
|
72
|
-
if (bc !== ac) return bc - ac;
|
|
73
|
-
return a.username.localeCompare(b.username);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const title = `Contributors — ${siteConfig.name}`;
|
|
77
|
-
const description =
|
|
78
|
-
`Every person who has helped build ${siteConfig.name} — from a single line to long-running stewardship.`;
|
|
79
|
-
|
|
80
|
-
const repoUrl = siteConfig.repoUrl;
|
|
81
|
-
const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/contributors` : null;
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
85
|
-
<section class="py-12 sm:py-16">
|
|
86
|
-
<div class="mx-auto w-full max-w-wide px-5 py-12 sm:px-7 sm:py-16">
|
|
87
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
88
|
-
Contributors
|
|
89
|
-
</span>
|
|
90
|
-
<h1 class="m-0 mt-2 text-[2rem] sm:text-[2.5rem] font-semibold leading-[1.05] tracking-tight text-ink-900 dark:text-ink-100">
|
|
91
|
-
{total > 0 ? `Showing all ${total} contributors` : "Contributors"}
|
|
92
|
-
</h1>
|
|
93
|
-
<p class="m-0 mt-3 max-w-2xl text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
94
|
-
{total > 0
|
|
95
|
-
? `Every person who has helped build ${siteConfig.name} — from a single line to long-running stewardship.`
|
|
96
|
-
: `Contributors will appear here once ${siteConfig.name} syncs its first set of GitHub metadata. The list is auto-generated from ${repoUrl ? repoUrl : "the project's source repository"}.`}
|
|
97
|
-
</p>
|
|
98
|
-
|
|
99
|
-
{contributorsGraphHref && (
|
|
100
|
-
<p class="m-0 mt-3 text-sm text-ink-500 dark:text-ink-400">
|
|
101
|
-
<a
|
|
102
|
-
href={contributorsGraphHref}
|
|
103
|
-
target="_blank"
|
|
104
|
-
rel="noopener noreferrer"
|
|
105
|
-
class="font-medium underline decoration-ink-300 underline-offset-2 hover:decoration-ink-500 dark:decoration-ink-700 dark:hover:decoration-ink-400"
|
|
106
|
-
>
|
|
107
|
-
GitHub contributors graph →
|
|
108
|
-
</a>
|
|
109
|
-
</p>
|
|
110
|
-
)}
|
|
111
|
-
</div>
|
|
112
|
-
</section>
|
|
113
|
-
|
|
114
|
-
{sorted.length === 0 ? (
|
|
115
|
-
<section class="py-12 sm:py-16">
|
|
116
|
-
<div class="mx-auto w-full max-w-container px-5 pb-16 sm:px-7">
|
|
117
|
-
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-col items-center gap-3 p-8 text-center">
|
|
118
|
-
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
119
|
-
No contributors synced yet. Once the first sync runs, every
|
|
120
|
-
contributor avatar lands here.
|
|
121
|
-
</p>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</section>
|
|
125
|
-
) : (
|
|
126
|
-
<section class="py-12 sm:py-16" aria-label="Contributors list">
|
|
127
|
-
<div class="mx-auto w-full max-w-wide px-5 pb-16 sm:px-7">
|
|
128
|
-
<ul
|
|
129
|
-
class="m-0 grid list-none grid-cols-2 gap-3 p-0 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"
|
|
130
|
-
aria-label="Contributors"
|
|
131
|
-
>
|
|
132
|
-
{sorted.map((c) => {
|
|
133
|
-
const href = c.profileUrl ?? `https://github.com/${c.username}`;
|
|
134
|
-
const avatar =
|
|
135
|
-
c.avatarUrl ??
|
|
136
|
-
`https://avatars.githubusercontent.com/${c.username}?v=4&s=80`;
|
|
137
|
-
return (
|
|
138
|
-
<li class="m-0 p-0">
|
|
139
|
-
<a
|
|
140
|
-
href={href}
|
|
141
|
-
target="_blank"
|
|
142
|
-
rel="noopener noreferrer"
|
|
143
|
-
class="group rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-col items-center gap-2 p-3 text-center no-underline hover:border-ink-300 dark:hover:border-ink-700"
|
|
144
|
-
data-event="contributor_click"
|
|
145
|
-
data-event-source="contributors_page"
|
|
146
|
-
>
|
|
147
|
-
<img
|
|
148
|
-
src={avatar}
|
|
149
|
-
alt={c.name ?? c.username}
|
|
150
|
-
width="56"
|
|
151
|
-
height="56"
|
|
152
|
-
loading="lazy"
|
|
153
|
-
decoding="async"
|
|
154
|
-
class="h-14 w-14 rounded-full border border-ink-200 object-cover dark:border-ink-800"
|
|
155
|
-
/>
|
|
156
|
-
<span class="line-clamp-1 text-sm font-medium text-ink-700 group-hover:text-ink-900 dark:text-ink-300 dark:group-hover:text-ink-100">
|
|
157
|
-
{c.name ?? c.username}
|
|
158
|
-
</span>
|
|
159
|
-
<span class="text-2xs text-ink-500 dark:text-ink-400">
|
|
160
|
-
@{c.username}
|
|
161
|
-
</span>
|
|
162
|
-
{typeof c.contributions === "number" && (
|
|
163
|
-
<span class="font-mono text-2xs text-ink-400">
|
|
164
|
-
{c.contributions.toLocaleString()} contributions
|
|
165
|
-
</span>
|
|
166
|
-
)}
|
|
167
|
-
</a>
|
|
168
|
-
</li>
|
|
169
|
-
);
|
|
170
|
-
})}
|
|
171
|
-
</ul>
|
|
172
|
-
</div>
|
|
173
|
-
</section>
|
|
174
|
-
)}
|
|
175
|
-
</BaseLayout>
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Home page — generic.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors the openapps structure: Hero → WhyThisExists → three
|
|
6
|
-
* lens sections (hot / new / mature) → StackGrid → CategoryGrid →
|
|
7
|
-
* ContributorsGrid → OriginalCollection.
|
|
8
|
-
*
|
|
9
|
-
* All data derives from `data/generated/records.{index,full}.json`
|
|
10
|
-
* and `data/generated/site-config.json`. Nothing here is hardcoded
|
|
11
|
-
* to a specific blueprint — section copy uses singular/plural
|
|
12
|
-
* labels from `itemLabel()`/`itemLabelPlural()`.
|
|
13
|
-
*/
|
|
14
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
15
|
-
import Hero from "@grove-dev/astro/components/Hero.astro";
|
|
16
|
-
import WhyThisExists from "@grove-dev/astro/components/WhyThisExists.astro";
|
|
17
|
-
import RecordSection from "@grove-dev/astro/components/RecordSection.astro";
|
|
18
|
-
import StackGrid from "@grove-dev/astro/components/StackGrid.astro";
|
|
19
|
-
import CategoryGrid from "@grove-dev/astro/components/CategoryGrid.astro";
|
|
20
|
-
import ContributorsGrid from "@grove-dev/astro/components/ContributorsGrid.astro";
|
|
21
|
-
import OriginalCollection from "@grove-dev/astro/components/OriginalCollection.astro";
|
|
22
|
-
|
|
23
|
-
import { items, fullItems, taxonomyLabel } from "../data/records";
|
|
24
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { dirname, resolve } from "node:path";
|
|
27
|
-
import siteConfig from "../../data/generated/site-config.json";
|
|
28
|
-
|
|
29
|
-
const slug = (siteConfig.blueprintConfig?.routeSlug ?? "projects") as string;
|
|
30
|
-
const itemSingular = (siteConfig.blueprintConfig?.labelSingular ?? "project") as string;
|
|
31
|
-
const itemPlural = (siteConfig.blueprintConfig?.labelPlural ?? "projects") as string;
|
|
32
|
-
|
|
33
|
-
// ── Lens sectioning (Hot / New / Mature) ────────────────────────
|
|
34
|
-
const DAY_MS = 86_400_000;
|
|
35
|
-
function daysSince(iso: string | null | undefined): number {
|
|
36
|
-
if (!iso) return Infinity;
|
|
37
|
-
const d = new Date(iso);
|
|
38
|
-
if (Number.isNaN(d.valueOf())) return Infinity;
|
|
39
|
-
return (Date.now() - d.valueOf()) / DAY_MS;
|
|
40
|
-
}
|
|
41
|
-
function starsOf(r: typeof items[number]): number {
|
|
42
|
-
return (r as { github?: { stars?: number } }).github?.stars ?? 0;
|
|
43
|
-
}
|
|
44
|
-
function pushedAtOf(r: typeof items[number]): string | null {
|
|
45
|
-
return (r as { github?: { pushedAt?: string | null } }).github?.pushedAt ?? null;
|
|
46
|
-
}
|
|
47
|
-
function reviewedAtOf(r: typeof items[number]): string | null {
|
|
48
|
-
return r.curation?.reviewedAt ?? null;
|
|
49
|
-
}
|
|
50
|
-
function labelsOf(r: typeof items[number]): string[] {
|
|
51
|
-
return r.curation?.labels ?? [];
|
|
52
|
-
}
|
|
53
|
-
function tierOf(r: typeof items[number]): string | undefined {
|
|
54
|
-
return (r as { health?: { tier?: string } }).health?.tier;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const hotList = items
|
|
58
|
-
.filter((r) => r.kind === "project")
|
|
59
|
-
.filter((r) => {
|
|
60
|
-
const labelledHot = labelsOf(r).includes("hot");
|
|
61
|
-
return labelledHot || starsOf(r) >= 1000 || daysSince(pushedAtOf(r)) <= 30;
|
|
62
|
-
})
|
|
63
|
-
.slice(0, 6);
|
|
64
|
-
const newList = items
|
|
65
|
-
.filter((r) => r.kind === "project")
|
|
66
|
-
.filter((r) => {
|
|
67
|
-
const labelledNew = labelsOf(r).includes("new");
|
|
68
|
-
return daysSince(reviewedAtOf(r)) <= 21 || labelledNew;
|
|
69
|
-
})
|
|
70
|
-
.slice(0, 6);
|
|
71
|
-
const matureList = items
|
|
72
|
-
.filter((r) => r.kind === "project")
|
|
73
|
-
.filter((r) => labelsOf(r).includes("mature") || tierOf(r) === "curated")
|
|
74
|
-
.slice(0, 6);
|
|
75
|
-
|
|
76
|
-
// Stacks and categories derived from items
|
|
77
|
-
const stackMap = new Map<string, number>();
|
|
78
|
-
const catMap = new Map<string, number>();
|
|
79
|
-
for (const r of fullItems) {
|
|
80
|
-
const cat = (r as { category?: string }).category;
|
|
81
|
-
if (cat) catMap.set(cat, (catMap.get(cat) ?? 0) + 1);
|
|
82
|
-
const s1 = (r as { stack?: string }).stack;
|
|
83
|
-
if (s1) stackMap.set(s1, (stackMap.get(s1) ?? 0) + 1);
|
|
84
|
-
const s2 = (r as { stacks?: string[] }).stacks ?? [];
|
|
85
|
-
for (const s of s2) stackMap.set(s, (stackMap.get(s) ?? 0) + 1);
|
|
86
|
-
}
|
|
87
|
-
const stacks = [...stackMap.entries()]
|
|
88
|
-
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
89
|
-
.slice(0, 12)
|
|
90
|
-
.map(([name, count]) => ({
|
|
91
|
-
name: taxonomyLabel("stacks", name),
|
|
92
|
-
slug: name,
|
|
93
|
-
count,
|
|
94
|
-
}));
|
|
95
|
-
const categories = [...catMap.entries()]
|
|
96
|
-
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
97
|
-
.map(([name, count]) => ({
|
|
98
|
-
name: taxonomyLabel("categories", name),
|
|
99
|
-
slug: name,
|
|
100
|
-
count,
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
// Contributors (best-effort from data/generated/contributors.json)
|
|
104
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
105
|
-
const contributorsPath = [
|
|
106
|
-
resolve(here, "..", "..", "data", "generated", "contributors.json"),
|
|
107
|
-
resolve(process.cwd(), "data", "generated", "contributors.json"),
|
|
108
|
-
].find((p) => existsSync(p));
|
|
109
|
-
interface ContributorsFile {
|
|
110
|
-
contributors?: Array<{
|
|
111
|
-
username: string;
|
|
112
|
-
name?: string;
|
|
113
|
-
avatarUrl?: string;
|
|
114
|
-
profileUrl?: string;
|
|
115
|
-
contributions?: number;
|
|
116
|
-
}>;
|
|
117
|
-
}
|
|
118
|
-
let contributors: ContributorsFile["contributors"] = [];
|
|
119
|
-
if (contributorsPath) {
|
|
120
|
-
try {
|
|
121
|
-
const parsed = JSON.parse(readFileSync(contributorsPath, "utf8")) as ContributorsFile;
|
|
122
|
-
contributors = parsed.contributors ?? [];
|
|
123
|
-
} catch {
|
|
124
|
-
contributors = [];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const stats = {
|
|
129
|
-
originalRepo: siteConfig.repoUrl ?? "",
|
|
130
|
-
apps: siteConfig.stats?.totalApps ?? 0,
|
|
131
|
-
categories: siteConfig.stats?.totalCategories ?? 0,
|
|
132
|
-
stacks: siteConfig.stats?.totalStacks ?? 0,
|
|
133
|
-
contributors: siteConfig.stats?.totalOwners ?? 0,
|
|
134
|
-
stars: siteConfig.stats?.totalStars ?? 0,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const title = `${siteConfig.name} — ${siteConfig.tagline}`;
|
|
138
|
-
const description =
|
|
139
|
-
siteConfig.description ??
|
|
140
|
-
`A searchable directory of real ${itemPlural} — organized by stack, category, platform, license, activity, and maturity.`;
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
144
|
-
<Hero
|
|
145
|
-
name={`Find ${itemPlural} you can actually`}
|
|
146
|
-
tagline="run, study, and contribute to."
|
|
147
|
-
description={description}
|
|
148
|
-
eyebrow={`A directory of real curated ${itemPlural}`}
|
|
149
|
-
eyebrowBadge="Beta"
|
|
150
|
-
stats={{
|
|
151
|
-
totalApps: stats.apps,
|
|
152
|
-
totalCategories: stats.categories,
|
|
153
|
-
totalStacks: stats.stacks,
|
|
154
|
-
totalOwners: stats.contributors,
|
|
155
|
-
totalStars: stats.stars,
|
|
156
|
-
originalRepo: stats.originalRepo,
|
|
157
|
-
}}
|
|
158
|
-
itemsLabel={itemPlural}
|
|
159
|
-
itemLabel={itemSingular}
|
|
160
|
-
searchHref={`/${slug}`}
|
|
161
|
-
searchPlaceholder={`Search ${itemPlural}, stacks, categories, licenses, platforms...`}
|
|
162
|
-
quickFilters={[
|
|
163
|
-
{ label: `Trending ${itemPlural}`, href: `/${slug}?label=hot` },
|
|
164
|
-
{ label: "Recently added", href: `/${slug}?label=new` },
|
|
165
|
-
{ label: "Established", href: `/${slug}?label=mature` },
|
|
166
|
-
]}
|
|
167
|
-
ctas={[
|
|
168
|
-
{ label: `Browse ${itemPlural}`, href: `/${slug}`, variant: "primary" },
|
|
169
|
-
{ label: `Submit ${itemSingular} via GitHub`, href: "/submit", variant: "outline" },
|
|
170
|
-
...(siteConfig.repoUrl
|
|
171
|
-
? [{ label: "View source", href: siteConfig.repoUrl, variant: "outline" as const }]
|
|
172
|
-
: []),
|
|
173
|
-
]}
|
|
174
|
-
/>
|
|
175
|
-
|
|
176
|
-
<WhyThisExists />
|
|
177
|
-
|
|
178
|
-
<RecordSection
|
|
179
|
-
id="hot"
|
|
180
|
-
title="Trending now"
|
|
181
|
-
description={`${itemPlural.charAt(0).toUpperCase() + itemPlural.slice(1)} gaining stars, activity, or attention recently.`}
|
|
182
|
-
eyebrow="Hot"
|
|
183
|
-
items={hotList}
|
|
184
|
-
hrefFor={(r) => `/${slug}/${r.slug}`}
|
|
185
|
-
viewAllHref={`/${slug}?label=hot`}
|
|
186
|
-
viewAllLabel={`View all trending ${itemPlural}`}
|
|
187
|
-
/>
|
|
188
|
-
|
|
189
|
-
<RecordSection
|
|
190
|
-
id="new"
|
|
191
|
-
title="Recently added"
|
|
192
|
-
description={`${itemPlural.charAt(0).toUpperCase() + itemPlural.slice(1)} added to the directory in the last few weeks.`}
|
|
193
|
-
eyebrow="New"
|
|
194
|
-
items={newList}
|
|
195
|
-
hrefFor={(r) => `/${slug}/${r.slug}`}
|
|
196
|
-
viewAllHref={`/${slug}?label=new`}
|
|
197
|
-
viewAllLabel={`View all recently added`}
|
|
198
|
-
/>
|
|
199
|
-
|
|
200
|
-
<RecordSection
|
|
201
|
-
id="mature"
|
|
202
|
-
title="Established"
|
|
203
|
-
description={`Long-running curated ${itemPlural} with strong history, active maintainers, and real-world usage.`}
|
|
204
|
-
eyebrow="Mature"
|
|
205
|
-
items={matureList}
|
|
206
|
-
hrefFor={(r) => `/${slug}/${r.slug}`}
|
|
207
|
-
viewAllHref={`/${slug}?label=mature`}
|
|
208
|
-
viewAllLabel={`View all established`}
|
|
209
|
-
/>
|
|
210
|
-
|
|
211
|
-
<StackGrid stacks={stacks} pathPrefix={`/${slug}`} itemLabelPlural={itemPlural} />
|
|
212
|
-
<CategoryGrid categories={categories} pathPrefix={`/${slug}`} itemLabelPlural={itemPlural} />
|
|
213
|
-
|
|
214
|
-
{contributors.length > 0 && (
|
|
215
|
-
<ContributorsGrid
|
|
216
|
-
contributors={contributors}
|
|
217
|
-
total={contributors.length}
|
|
218
|
-
viewAllHref={`/contributors`}
|
|
219
|
-
viewAllLabel="View all contributors"
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
|
|
223
|
-
{siteConfig.stats?.originalRepo && (
|
|
224
|
-
<OriginalCollection
|
|
225
|
-
repoUrl={siteConfig.stats.originalRepo}
|
|
226
|
-
stats={{
|
|
227
|
-
stars: siteConfig.stats.originalStars,
|
|
228
|
-
forks: siteConfig.stats.originalForks,
|
|
229
|
-
contributors: siteConfig.stats.originalContributors,
|
|
230
|
-
}}
|
|
231
|
-
/>
|
|
232
|
-
)}
|
|
233
|
-
</BaseLayout>
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sitemap.xml.ts — Astro endpoint that serves the site map.
|
|
3
|
-
*
|
|
4
|
-
* The CLI's `grove sitemap` command also writes a sitemap into
|
|
5
|
-
* `public/sitemap.xml`. This endpoint is the consumer-facing
|
|
6
|
-
* version: it builds the sitemap from the live record set at
|
|
7
|
-
* request time, so a manual `astro build` (without running the
|
|
8
|
-
* CLI first) still gets a correct map.
|
|
9
|
-
*
|
|
10
|
-
* In practice the CLI's output is preferred for SEO (it's
|
|
11
|
-
* deterministic and includes the build timestamp); the consumer
|
|
12
|
-
* is expected to wire `public/sitemap.xml` and this endpoint to
|
|
13
|
-
* the same source. We keep the endpoint thin so it can serve as
|
|
14
|
-
* either a fallback or a live override.
|
|
15
|
-
*/
|
|
16
|
-
import type { APIRoute } from "astro";
|
|
17
|
-
import { fullRecords, indexSlug } from "../data/records";
|
|
18
|
-
import siteConfig from "../../data/generated/site-config.json";
|
|
19
|
-
|
|
20
|
-
interface SitemapEntry {
|
|
21
|
-
loc: string;
|
|
22
|
-
lastmod?: string;
|
|
23
|
-
changefreq?: "daily" | "weekly" | "monthly" | "yearly";
|
|
24
|
-
priority?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function absoluteUrl(siteUrl: string, path: string): string {
|
|
28
|
-
const base = siteUrl.replace(/\/$/, "");
|
|
29
|
-
const tail = path.startsWith("/") ? path : `/${path}`;
|
|
30
|
-
return `${base}${tail}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function buildEntries(siteUrl: string): SitemapEntry[] {
|
|
34
|
-
const slug = indexSlug();
|
|
35
|
-
const now = new Date().toISOString().slice(0, 10);
|
|
36
|
-
|
|
37
|
-
const top: SitemapEntry[] = [
|
|
38
|
-
{ loc: absoluteUrl(siteUrl, "/"), lastmod: now, changefreq: "daily", priority: 1.0 },
|
|
39
|
-
{ loc: absoluteUrl(siteUrl, "/about"), lastmod: now, changefreq: "monthly", priority: 0.5 },
|
|
40
|
-
{ loc: absoluteUrl(siteUrl, "/submit"), lastmod: now, changefreq: "yearly", priority: 0.3 },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
if (fullRecords.length > 0) {
|
|
44
|
-
top.push({
|
|
45
|
-
loc: absoluteUrl(siteUrl, `/${slug}`),
|
|
46
|
-
lastmod: now,
|
|
47
|
-
changefreq: "daily",
|
|
48
|
-
priority: 0.9,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const recordEntries: SitemapEntry[] = fullRecords
|
|
53
|
-
.filter((r) => r.kind === "project")
|
|
54
|
-
.map((r) => ({
|
|
55
|
-
loc: absoluteUrl(siteUrl, `/${slug}/${r.slug}`),
|
|
56
|
-
// detail pages don't carry a `pushedAt` in the typed union;
|
|
57
|
-
// we use the build date so crawlers see a recent timestamp.
|
|
58
|
-
lastmod: now,
|
|
59
|
-
changefreq: "weekly",
|
|
60
|
-
priority: 0.7,
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
return [...top, ...recordEntries];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function renderXml(entries: SitemapEntry[]): string {
|
|
67
|
-
const body = entries
|
|
68
|
-
.map((e) => {
|
|
69
|
-
const lines: string[] = [` <url>`, ` <loc>${e.loc}</loc>`];
|
|
70
|
-
if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);
|
|
71
|
-
if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);
|
|
72
|
-
if (e.priority !== undefined) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);
|
|
73
|
-
lines.push(` </url>`);
|
|
74
|
-
return lines.join("\n");
|
|
75
|
-
})
|
|
76
|
-
.join("\n");
|
|
77
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const GET: APIRoute = () => {
|
|
81
|
-
const xml = renderXml(buildEntries(siteConfig.siteUrl ?? ""));
|
|
82
|
-
return new Response(xml, {
|
|
83
|
-
status: 200,
|
|
84
|
-
headers: {
|
|
85
|
-
"Content-Type": "application/xml; charset=utf-8",
|
|
86
|
-
"Cache-Control": "public, max-age=3600",
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
};
|