@grove-dev/astro 0.2.6 → 0.2.13

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.
Files changed (136) hide show
  1. package/dist/lib/display.d.ts +11 -17
  2. package/dist/lib/display.d.ts.map +1 -1
  3. package/dist/lib/display.js +43 -43
  4. package/dist/lib/display.js.map +1 -1
  5. package/dist/lib/format.d.ts +6 -7
  6. package/dist/lib/format.d.ts.map +1 -1
  7. package/dist/lib/format.js +11 -12
  8. package/dist/lib/format.js.map +1 -1
  9. package/dist/lib/lenses.d.ts +35 -73
  10. package/dist/lib/lenses.d.ts.map +1 -1
  11. package/dist/lib/lenses.js +144 -126
  12. package/dist/lib/lenses.js.map +1 -1
  13. package/dist/lib/repo.d.ts +11 -22
  14. package/dist/lib/repo.d.ts.map +1 -1
  15. package/dist/lib/repo.js +12 -25
  16. package/dist/lib/repo.js.map +1 -1
  17. package/dist/lib/scores.d.ts +15 -16
  18. package/dist/lib/scores.d.ts.map +1 -1
  19. package/dist/lib/scores.js +4 -11
  20. package/dist/lib/scores.js.map +1 -1
  21. package/dist/lib/search.d.ts +47 -129
  22. package/dist/lib/search.d.ts.map +1 -1
  23. package/dist/lib/search.js +195 -180
  24. package/dist/lib/search.js.map +1 -1
  25. package/dist/lib/taxonomy-counts.d.ts +7 -21
  26. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  27. package/dist/lib/taxonomy-counts.js +12 -31
  28. package/dist/lib/taxonomy-counts.js.map +1 -1
  29. package/package.json +4 -4
  30. package/src/components/AppsIndexRow.astro +55 -184
  31. package/src/components/AppsPagination.astro +45 -108
  32. package/src/components/CategoryGrid.astro +43 -31
  33. package/src/components/ContributorsGrid.astro +3 -3
  34. package/src/components/CurationGrid.astro +41 -0
  35. package/src/components/ExploreByStack.astro +6 -0
  36. package/src/components/FilterGroupMenu.astro +49 -0
  37. package/src/components/FilterOptions.astro +44 -0
  38. package/src/components/Hero.astro +198 -147
  39. package/src/components/Icon.astro +10 -3
  40. package/src/components/ItemCard.astro +266 -248
  41. package/src/components/RecordSection.astro +68 -33
  42. package/src/components/RefinePanel.astro +115 -141
  43. package/src/components/StackGrid.astro +32 -24
  44. package/src/components/WhyThisExists.astro +32 -32
  45. package/src/layouts/BaseLayout.astro +104 -49
  46. package/src/layouts/Container.astro +5 -44
  47. package/src/layouts/Footer.astro +31 -20
  48. package/src/layouts/Header.astro +40 -19
  49. package/src/layouts/SectionHeader.astro +27 -43
  50. package/src/layouts/Seo.astro +38 -56
  51. package/src/layouts/ThemeToggle.astro +35 -48
  52. package/src/lib/display.ts +46 -53
  53. package/src/lib/format.ts +11 -12
  54. package/src/lib/lenses.ts +151 -172
  55. package/src/lib/repo.ts +16 -28
  56. package/src/lib/scores.ts +15 -19
  57. package/src/lib/search.ts +222 -290
  58. package/src/lib/taxonomy-counts.ts +12 -46
  59. package/src/styles.css +207 -1
  60. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  61. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  62. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
  63. package/templates/default/.github/pull_request_template.md +27 -0
  64. package/templates/default/.github/workflows/build.yml +55 -0
  65. package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
  66. package/templates/default/.github/workflows/daily-refresh.yml +53 -0
  67. package/templates/default/.github/workflows/sync-contributors.yml +53 -0
  68. package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
  69. package/templates/default/.github/workflows/validate-data.yml +35 -0
  70. package/templates/default/data/records/coolify.yml +46 -0
  71. package/templates/default/data/records/gitea.yml +46 -0
  72. package/templates/default/data/records/hoppscotch.yml +47 -0
  73. package/templates/default/data/records/inventree.yml +47 -0
  74. package/templates/default/data/records/label-studio.yml +46 -0
  75. package/templates/default/data/records/logseq.yml +48 -0
  76. package/templates/default/data/records/private-gpt.yml +47 -0
  77. package/templates/default/data/records/wakapi.yml +44 -0
  78. package/templates/default/data/taxonomy/categories.yml +47 -0
  79. package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
  80. package/templates/default/data/taxonomy/platforms.yml +27 -0
  81. package/templates/default/data/taxonomy/stacks.yml +41 -0
  82. package/templates/default/grove.config.ts +1 -1
  83. package/templates/default/package.json +5 -4
  84. package/templates/default/public/icons/stacks/bun.svg +1 -0
  85. package/templates/default/public/icons/stacks/clojure.svg +1 -0
  86. package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
  87. package/templates/default/public/icons/stacks/deno.svg +1 -0
  88. package/templates/default/public/icons/stacks/django.svg +1 -0
  89. package/templates/default/public/icons/stacks/docker.svg +1 -0
  90. package/templates/default/public/icons/stacks/go.svg +1 -0
  91. package/templates/default/public/icons/stacks/llm.svg +1 -0
  92. package/templates/default/public/icons/stacks/node.js.svg +1 -0
  93. package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
  94. package/templates/default/public/icons/stacks/vue.svg +1 -0
  95. package/templates/default/public/llms-full.txt +197 -5
  96. package/templates/default/public/llms.txt +107 -6
  97. package/templates/default/public/sitemap.xml +63 -0
  98. package/templates/default/scripts/build-llms.mjs +123 -0
  99. package/templates/default/scripts/build-records-json.mjs +27 -0
  100. package/templates/default/scripts/build-sitemap.mjs +20 -0
  101. package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
  102. package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
  103. package/templates/default/scripts/fetch-icons.mjs +137 -0
  104. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
  105. package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
  106. package/templates/default/scripts/refresh-records-activity.mjs +24 -0
  107. package/templates/default/scripts/repair-license-format.mjs +72 -0
  108. package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
  109. package/templates/default/scripts/seed-from-github.mjs +62 -0
  110. package/templates/default/scripts/sync-contributors.mjs +145 -0
  111. package/templates/default/scripts/sync-github-metadata.mjs +28 -0
  112. package/templates/default/scripts/validate-records.mjs +27 -0
  113. package/templates/default/src/data/records.ts +134 -51
  114. package/templates/default/src/pages/404.astro +64 -0
  115. package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
  116. package/templates/default/src/pages/[slug]/index.astro +476 -0
  117. package/templates/default/src/pages/about.astro +165 -82
  118. package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
  119. package/templates/default/src/pages/apps/index.astro +146 -0
  120. package/templates/default/src/pages/contributors.astro +184 -0
  121. package/templates/default/src/pages/index.astro +119 -162
  122. package/templates/default/src/pages/submit.astro +276 -114
  123. package/templates/default/wrangler.jsonc +28 -0
  124. package/LICENSE +0 -21
  125. package/templates/default/data/generated/apps.json +0 -1
  126. package/templates/default/data/generated/records.full.json +0 -8
  127. package/templates/default/data/generated/records.index.json +0 -7
  128. package/templates/default/data/generated/records.json +0 -8
  129. package/templates/default/data/generated/repo-stats.json +0 -1
  130. package/templates/default/data/generated/site-config.json +0 -31
  131. package/templates/default/src/components/layout/BaseLayout.astro +0 -47
  132. package/templates/default/src/components/layout/Container.astro +0 -18
  133. package/templates/default/src/components/layout/Footer.astro +0 -27
  134. package/templates/default/src/components/layout/Header.astro +0 -43
  135. package/templates/default/src/lib/markdown.ts +0 -171
  136. package/templates/default/src/pages/projects/index.astro +0 -193
@@ -1,28 +1,16 @@
1
1
  ---
2
2
  /**
3
- * Home page — the showcase's front door.
3
+ * Home page — generic.
4
4
  *
5
- * Sections (in order):
6
- * 1. Hero — brand + search + stats row
7
- * 2. WhyThisExists — 3 short value-prop points
8
- * 3. RecordSection "Trending" — hot lens (stars >= 1000 OR pushed <= 30d)
9
- * 4. RecordSection "Recently added" — new lens (pushed <= 7d OR curated "new")
10
- * 5. RecordSection "Established" — mature lens (tier=curated, pushed > 365d)
11
- * 6. StackGrid — browse by stack
12
- * 7. CategoryGrid — browse by category
13
- * 8. ContributorsGrid — top contributors (when available)
14
- * 9. OriginalCollection — origin-card link to the source repo
5
+ * Mirrors the openapps structure: Hero → WhyThisExists → three
6
+ * lens sections (hot / new / mature) → StackGrid → CategoryGrid →
7
+ * ContributorsGrid OriginalCollection.
15
8
  *
16
- * Every section is defensive: with 0 records (fresh scaffold) it
17
- * renders a graceful empty state instead of throwing.
18
- *
19
- * V1 source-of-truth types are from `@grove-dev/core`. The slim
20
- * `IndexProjectRecord` shape is what the home page consumes (no
21
- * `as ProjectRecord` casts); the full `Resource` only flows into
22
- * the detail page.
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()`.
23
13
  */
24
- import siteConfig from "../../data/generated/site-config.json";
25
- import { records, fullRecords, projects, indexSlug, itemLabel } from "../data/records";
26
14
  import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
27
15
  import Hero from "@grove-dev/astro/components/Hero.astro";
28
16
  import WhyThisExists from "@grove-dev/astro/components/WhyThisExists.astro";
@@ -31,35 +19,16 @@ import StackGrid from "@grove-dev/astro/components/StackGrid.astro";
31
19
  import CategoryGrid from "@grove-dev/astro/components/CategoryGrid.astro";
32
20
  import ContributorsGrid from "@grove-dev/astro/components/ContributorsGrid.astro";
33
21
  import OriginalCollection from "@grove-dev/astro/components/OriginalCollection.astro";
34
- import { countByCategory, countByStack } from "@grove-dev/astro";
22
+
23
+ import { items, fullRecords, fullItems } from "../data/records";
35
24
  import { existsSync, readFileSync } from "node:fs";
36
25
  import { fileURLToPath } from "node:url";
37
26
  import { dirname, resolve } from "node:path";
38
- import type { IndexProjectRecord } from "@grove-dev/core";
39
-
40
- const slug = indexSlug();
41
- const projectKind = projects;
27
+ import siteConfig from "../../data/generated/site-config.json";
42
28
 
43
- // ── Hero stats ──────────────────────────────────────────────────
44
- const totalApps = fullRecords.filter((r) => r.kind === "project").length;
45
- const totalCategories = new Set(projectKind.map((r) => r.category).filter(Boolean))
46
- .size;
47
- const totalStacks = new Set(
48
- projectKind.flatMap((r) => [r.stack, ...(r.stacks ?? [])]).filter(Boolean),
49
- ).size;
50
- const totalContributors = new Set(
51
- projectKind
52
- .map((r) => {
53
- const url = r.repoUrl ?? r.links?.github ?? "";
54
- const m = /github\.com\/([^/]+)\//.exec(url);
55
- return m?.[1];
56
- })
57
- .filter(Boolean),
58
- ).size;
59
- const totalStars = projectKind.reduce(
60
- (acc, r) => acc + (r.github?.stars ?? 0),
61
- 0,
62
- );
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;
63
32
 
64
33
  // ── Lens sectioning (Hot / New / Mature) ────────────────────────
65
34
  const DAY_MS = 86_400_000;
@@ -69,54 +38,66 @@ function daysSince(iso: string | null | undefined): number {
69
38
  if (Number.isNaN(d.valueOf())) return Infinity;
70
39
  return (Date.now() - d.valueOf()) / DAY_MS;
71
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
+ }
72
56
 
73
- const hot = projectKind
74
- .filter((r: IndexProjectRecord) => {
75
- const stars = r.github?.stars ?? 0;
76
- const pushedAt = r.github?.pushedAt ?? null;
77
- const pushed = daysSince(pushedAt);
78
- const labelledHot = r.curation?.labels?.includes("hot") ?? false;
79
- return labelledHot || stars >= 1000 || pushed <= 30;
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;
80
62
  })
81
63
  .slice(0, 6);
82
-
83
- const newItems = projectKind
84
- .filter((r: IndexProjectRecord) => {
85
- const pushedAt = r.github?.pushedAt ?? null;
86
- const pushed = daysSince(pushedAt);
87
- const labelledNew = r.curation?.labels?.includes("new") ?? false;
88
- return pushed <= 7 || labelledNew;
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;
89
69
  })
90
70
  .slice(0, 6);
91
-
92
- const mature = projectKind
93
- .filter((r: IndexProjectRecord) => {
94
- const tier = r.health?.tier;
95
- const pushedAt = r.github?.pushedAt ?? null;
96
- const pushed = daysSince(pushedAt);
97
- return tier === "curated" && pushed > 365;
98
- })
71
+ const matureList = items
72
+ .filter((r) => r.kind === "project")
73
+ .filter((r) => labelsOf(r).includes("mature") || tierOf(r) === "curated")
99
74
  .slice(0, 6);
100
75
 
101
- // ── Stack / category aggregates ─────────────────────────────────
102
- const stackCounts = countByStack(projectKind);
103
- const stacks = [...stackCounts.entries()]
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()]
104
88
  .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
105
89
  .slice(0, 12)
106
90
  .map(([name, count]) => ({ name, slug: name, count }));
107
-
108
- const categoryCounts = countByCategory(projectKind);
109
- const categories = [...categoryCounts.entries()]
91
+ const categories = [...catMap.entries()]
110
92
  .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
111
93
  .map(([name, count]) => ({ name, slug: name, count }));
112
94
 
113
- // ── Contributors (best-effort, from records) ────────────────────
95
+ // Contributors (best-effort from data/generated/contributors.json)
114
96
  const here = dirname(fileURLToPath(import.meta.url));
115
97
  const contributorsPath = [
116
98
  resolve(here, "..", "..", "data", "generated", "contributors.json"),
117
99
  resolve(process.cwd(), "data", "generated", "contributors.json"),
118
100
  ].find((p) => existsSync(p));
119
-
120
101
  interface ContributorsFile {
121
102
  contributors?: Array<{
122
103
  username: string;
@@ -126,7 +107,6 @@ interface ContributorsFile {
126
107
  contributions?: number;
127
108
  }>;
128
109
  }
129
-
130
110
  let contributors: ContributorsFile["contributors"] = [];
131
111
  if (contributorsPath) {
132
112
  try {
@@ -136,63 +116,52 @@ if (contributorsPath) {
136
116
  contributors = [];
137
117
  }
138
118
  }
139
- const hasContributors = contributors && contributors.length > 0;
140
119
 
141
- // ── OriginalCollection (repo-stats.json if present) ─────────────
142
- interface RepoStats {
143
- originalRepo?: string;
144
- stars?: number;
145
- forks?: number;
146
- contributors?: number;
147
- description?: string;
148
- }
149
- let repoStats: RepoStats = {};
150
- const repoStatsPath = [
151
- resolve(here, "..", "..", "data", "generated", "repo-stats.json"),
152
- resolve(process.cwd(), "data", "generated", "repo-stats.json"),
153
- ].find((p) => existsSync(p));
154
- if (repoStatsPath) {
155
- try {
156
- repoStats = JSON.parse(readFileSync(repoStatsPath, "utf8")) as RepoStats;
157
- } catch {
158
- repoStats = {};
159
- }
160
- }
120
+ const stats = {
121
+ originalRepo: siteConfig.repoUrl ?? "",
122
+ apps: siteConfig.stats?.totalApps ?? 0,
123
+ categories: siteConfig.stats?.totalCategories ?? 0,
124
+ stacks: siteConfig.stats?.totalStacks ?? 0,
125
+ contributors: siteConfig.stats?.totalOwners ?? 0,
126
+ stars: siteConfig.stats?.totalStars ?? 0,
127
+ };
161
128
 
162
129
  const title = `${siteConfig.name} — ${siteConfig.tagline}`;
163
130
  const description =
164
- siteConfig.description ?? `A growing community knowledge site powered by Grove.`;
165
-
166
- // ── Render-time helpers ─────────────────────────────────────────
167
- function detailHrefFor(r: IndexProjectRecord): string {
168
- return `/${slug}/${r.slug}`;
169
- }
131
+ siteConfig.description ??
132
+ `A searchable directory of real ${itemPlural} — organized by stack, category, platform, license, activity, and maturity.`;
170
133
  ---
171
134
 
172
- <BaseLayout
173
- title={title}
174
- description={description}
175
- site={siteConfig}
176
- submitHref="/submit"
177
- submitLabel={`Submit ${itemLabel()}`}
178
- >
135
+ <BaseLayout title={title} description={description} site={siteConfig}>
179
136
  <Hero
180
- name={siteConfig.name}
181
- tagline={siteConfig.tagline}
137
+ name={`Find ${itemPlural} you can actually`}
138
+ tagline="run, study, and contribute to."
182
139
  description={description}
183
- eyebrow="Curated directory"
184
- searchPlaceholder={`Search ${slug}...`}
185
- searchHref={`/${slug}`}
140
+ eyebrow={`A directory of real curated ${itemPlural}`}
141
+ eyebrowBadge="Beta"
186
142
  stats={{
187
- apps: totalApps,
188
- categories: totalCategories,
189
- stacks: totalStacks,
190
- contributors: totalContributors || undefined,
191
- stars: totalStars || undefined,
143
+ totalApps: stats.apps,
144
+ totalCategories: stats.categories,
145
+ totalStacks: stats.stacks,
146
+ totalOwners: stats.contributors,
147
+ totalStars: stats.stars,
148
+ originalRepo: stats.originalRepo,
192
149
  }}
150
+ itemsLabel={itemPlural}
151
+ itemLabel={itemSingular}
152
+ searchHref={`/${slug}`}
153
+ searchPlaceholder={`Search ${itemPlural}, stacks, categories, licenses, platforms...`}
154
+ quickFilters={[
155
+ { label: `Trending ${itemPlural}`, href: `/${slug}?label=hot` },
156
+ { label: "Recently added", href: `/${slug}?label=new` },
157
+ { label: "Established", href: `/${slug}?label=mature` },
158
+ ]}
193
159
  ctas={[
194
- { label: `Browse ${slug}`, href: `/${slug}`, variant: "primary" },
195
- { label: "About", href: "/about", variant: "outline" },
160
+ { label: `Browse ${itemPlural}`, href: `/${slug}`, variant: "primary" },
161
+ { label: `Submit ${itemSingular} via GitHub`, href: "/submit", variant: "outline" },
162
+ ...(siteConfig.repoUrl
163
+ ? [{ label: "View source", href: siteConfig.repoUrl, variant: "outline" as const }]
164
+ : []),
196
165
  ]}
197
166
  />
198
167
 
@@ -200,68 +169,56 @@ function detailHrefFor(r: IndexProjectRecord): string {
200
169
 
201
170
  <RecordSection
202
171
  id="hot"
203
- title="Trending"
204
- description="The projects getting attention right now high-star repos and recently active codebases."
205
- items={hot}
206
- hrefFor={detailHrefFor}
207
- viewAllHref={`/${slug}?lens=hot`}
172
+ title="Trending now"
173
+ description={`${itemPlural.charAt(0).toUpperCase() + itemPlural.slice(1)} gaining stars, activity, or attention recently.`}
208
174
  eyebrow="Hot"
209
- emptyLabel="No trending projects yet."
175
+ items={hotList}
176
+ hrefFor={(r) => `/${slug}/${r.slug}`}
177
+ viewAllHref={`/${slug}?label=hot`}
178
+ viewAllLabel={`View all trending ${itemPlural}`}
210
179
  />
211
180
 
212
181
  <RecordSection
213
182
  id="new"
214
183
  title="Recently added"
215
- description="Fresh entries pushed within the last week or carrying the curated 'new' label."
216
- items={newItems}
217
- hrefFor={detailHrefFor}
218
- viewAllHref={`/${slug}?lens=new`}
184
+ description={`${itemPlural.charAt(0).toUpperCase() + itemPlural.slice(1)} added to the directory in the last few weeks.`}
219
185
  eyebrow="New"
220
- emptyLabel="No new projects yet."
186
+ items={newList}
187
+ hrefFor={(r) => `/${slug}/${r.slug}`}
188
+ viewAllHref={`/${slug}?label=new`}
189
+ viewAllLabel={`View all recently added`}
221
190
  />
222
191
 
223
192
  <RecordSection
224
193
  id="mature"
225
194
  title="Established"
226
- description="Long-running curated projects with deep history and stable architecture."
227
- items={mature}
228
- hrefFor={detailHrefFor}
229
- viewAllHref={`/${slug}?lens=mature`}
195
+ description={`Long-running curated ${itemPlural} with strong history, active maintainers, and real-world usage.`}
230
196
  eyebrow="Mature"
231
- emptyLabel="No established projects yet."
197
+ items={matureList}
198
+ hrefFor={(r) => `/${slug}/${r.slug}`}
199
+ viewAllHref={`/${slug}?label=mature`}
200
+ viewAllLabel={`View all established`}
232
201
  />
233
202
 
234
- {stacks.length > 0 && (
235
- <StackGrid
236
- stacks={stacks}
237
- pathPrefix={`/${slug}`}
238
- />
239
- )}
240
-
241
- {categories.length > 0 && (
242
- <CategoryGrid
243
- categories={categories}
244
- pathPrefix={`/${slug}`}
245
- />
246
- )}
203
+ <StackGrid stacks={stacks} pathPrefix={`/${slug}`} itemLabelPlural={itemPlural} />
204
+ <CategoryGrid categories={categories} pathPrefix={`/${slug}`} itemLabelPlural={itemPlural} />
247
205
 
248
- {hasContributors && (
206
+ {contributors.length > 0 && (
249
207
  <ContributorsGrid
250
- contributors={contributors!}
251
- total={contributors!.length}
252
- viewAllHref={siteConfig.repoUrl || undefined}
253
- viewAllLabel="View on GitHub"
208
+ contributors={contributors}
209
+ total={contributors.length}
210
+ viewAllHref={`/contributors`}
211
+ viewAllLabel="View all contributors"
254
212
  />
255
213
  )}
256
214
 
257
- {repoStats.originalRepo && (
215
+ {siteConfig.stats?.originalRepo && (
258
216
  <OriginalCollection
259
- repoUrl={repoStats.originalRepo}
217
+ repoUrl={siteConfig.stats.originalRepo}
260
218
  stats={{
261
- stars: repoStats.stars,
262
- forks: repoStats.forks,
263
- contributors: repoStats.contributors,
264
- description: repoStats.description,
219
+ stars: siteConfig.stats.originalStars,
220
+ forks: siteConfig.stats.originalForks,
221
+ contributors: siteConfig.stats.originalContributors,
265
222
  }}
266
223
  />
267
224
  )}