@grove-dev/astro 0.2.10 → 0.2.14

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 (237) hide show
  1. package/README.md +26 -18
  2. package/dist/lib/display.d.ts +11 -17
  3. package/dist/lib/display.d.ts.map +1 -1
  4. package/dist/lib/display.js +43 -43
  5. package/dist/lib/display.js.map +1 -1
  6. package/dist/lib/format.d.ts +6 -7
  7. package/dist/lib/format.d.ts.map +1 -1
  8. package/dist/lib/format.js +11 -12
  9. package/dist/lib/format.js.map +1 -1
  10. package/dist/lib/lenses.d.ts +35 -73
  11. package/dist/lib/lenses.d.ts.map +1 -1
  12. package/dist/lib/lenses.js +144 -126
  13. package/dist/lib/lenses.js.map +1 -1
  14. package/dist/lib/repo.d.ts +12 -22
  15. package/dist/lib/repo.d.ts.map +1 -1
  16. package/dist/lib/repo.js +13 -25
  17. package/dist/lib/repo.js.map +1 -1
  18. package/dist/lib/scores.d.ts +15 -16
  19. package/dist/lib/scores.d.ts.map +1 -1
  20. package/dist/lib/scores.js +4 -11
  21. package/dist/lib/scores.js.map +1 -1
  22. package/dist/lib/search.d.ts +44 -129
  23. package/dist/lib/search.d.ts.map +1 -1
  24. package/dist/lib/search.js +192 -180
  25. package/dist/lib/search.js.map +1 -1
  26. package/dist/lib/taxonomy-counts.d.ts +7 -21
  27. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  28. package/dist/lib/taxonomy-counts.js +12 -31
  29. package/dist/lib/taxonomy-counts.js.map +1 -1
  30. package/package.json +4 -4
  31. package/src/components/CategoryGrid.astro +43 -31
  32. package/src/components/ContributorsGrid.astro +3 -3
  33. package/src/components/CurationGrid.astro +41 -0
  34. package/src/components/ExploreByStack.astro +6 -0
  35. package/src/components/FilterGroupMenu.astro +49 -0
  36. package/src/components/FilterOptions.astro +44 -0
  37. package/src/components/Hero.astro +198 -147
  38. package/src/components/Icon.astro +10 -3
  39. package/src/components/IndexRow.astro +75 -0
  40. package/src/components/ItemCard.astro +266 -248
  41. package/src/components/Pagination.astro +63 -0
  42. package/src/components/RecordSection.astro +70 -34
  43. package/src/components/RefinePanel.astro +115 -141
  44. package/src/components/StackGrid.astro +32 -24
  45. package/src/components/WhyThisExists.astro +32 -32
  46. package/src/layouts/BaseLayout.astro +104 -49
  47. package/src/layouts/Container.astro +5 -44
  48. package/src/layouts/Footer.astro +31 -20
  49. package/src/layouts/Header.astro +40 -19
  50. package/src/layouts/SectionHeader.astro +27 -43
  51. package/src/layouts/Seo.astro +38 -56
  52. package/src/layouts/ThemeToggle.astro +35 -48
  53. package/src/lib/display.ts +46 -53
  54. package/src/lib/format.ts +11 -12
  55. package/src/lib/lenses.ts +151 -172
  56. package/src/lib/repo.ts +17 -28
  57. package/src/lib/scores.ts +15 -19
  58. package/src/lib/search.ts +218 -291
  59. package/src/lib/taxonomy-counts.ts +12 -46
  60. package/src/styles.css +209 -1
  61. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  62. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  63. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
  64. package/templates/default/.github/pull_request_template.md +27 -0
  65. package/templates/default/.github/workflows/build.yml +55 -0
  66. package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
  67. package/templates/default/.github/workflows/daily-refresh.yml +53 -0
  68. package/templates/default/.github/workflows/sync-contributors.yml +53 -0
  69. package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
  70. package/templates/default/.github/workflows/validate-data.yml +35 -0
  71. package/templates/default/data/records/coolify.yml +46 -0
  72. package/templates/default/data/records/gitea.yml +46 -0
  73. package/templates/default/data/records/hoppscotch.yml +47 -0
  74. package/templates/default/data/records/inventree.yml +47 -0
  75. package/templates/default/data/records/label-studio.yml +46 -0
  76. package/templates/default/data/records/logseq.yml +48 -0
  77. package/templates/default/data/records/private-gpt.yml +47 -0
  78. package/templates/default/data/records/wakapi.yml +44 -0
  79. package/templates/default/data/taxonomy/categories.yml +47 -0
  80. package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
  81. package/templates/default/data/taxonomy/platforms.yml +27 -0
  82. package/templates/default/data/taxonomy/stacks.yml +41 -0
  83. package/templates/default/grove.config.ts +1 -1
  84. package/templates/default/package.json +5 -4
  85. package/templates/default/public/icons/stacks/bun.svg +1 -0
  86. package/templates/default/public/icons/stacks/clojure.svg +1 -0
  87. package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
  88. package/templates/default/public/icons/stacks/deno.svg +1 -0
  89. package/templates/default/public/icons/stacks/django.svg +1 -0
  90. package/templates/default/public/icons/stacks/docker.svg +1 -0
  91. package/templates/default/public/icons/stacks/go.svg +1 -0
  92. package/templates/default/public/icons/stacks/llm.svg +1 -0
  93. package/templates/default/public/icons/stacks/node.js.svg +1 -0
  94. package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
  95. package/templates/default/public/icons/stacks/vue.svg +1 -0
  96. package/templates/default/public/llms-full.txt +197 -5
  97. package/templates/default/public/llms.txt +107 -6
  98. package/templates/default/public/sitemap.xml +63 -0
  99. package/templates/default/scripts/build-llms.mjs +123 -0
  100. package/templates/default/scripts/build-records-json.mjs +27 -0
  101. package/templates/default/scripts/build-sitemap.mjs +20 -0
  102. package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
  103. package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
  104. package/templates/default/scripts/fetch-icons.mjs +137 -0
  105. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
  106. package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
  107. package/templates/default/scripts/refresh-records-activity.mjs +24 -0
  108. package/templates/default/scripts/repair-license-format.mjs +72 -0
  109. package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
  110. package/templates/default/scripts/seed-from-github.mjs +62 -0
  111. package/templates/default/scripts/sync-contributors.mjs +145 -0
  112. package/templates/default/scripts/sync-github-metadata.mjs +28 -0
  113. package/templates/default/scripts/validate-records.mjs +27 -0
  114. package/templates/default/src/data/records.ts +138 -51
  115. package/templates/default/src/pages/404.astro +64 -0
  116. package/templates/default/src/pages/{projects/[slug].astro → [slug]/[recordSlug].astro} +236 -66
  117. package/templates/default/src/pages/[slug]/index.astro +477 -0
  118. package/templates/default/src/pages/about.astro +165 -82
  119. package/templates/default/src/pages/apps/[recordSlug].astro +33 -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/src/components/AppsIndexRow.astro +0 -204
  126. package/src/components/AppsPagination.astro +0 -126
  127. package/templates/default/.astro/content.d.ts +0 -155
  128. package/templates/default/.astro/settings.json +0 -5
  129. package/templates/default/.astro/types.d.ts +0 -1
  130. package/templates/default/data/generated/apps.json +0 -1
  131. package/templates/default/data/generated/records.full.json +0 -8
  132. package/templates/default/data/generated/records.index.json +0 -7
  133. package/templates/default/data/generated/records.json +0 -8
  134. package/templates/default/data/generated/repo-stats.json +0 -1
  135. package/templates/default/data/generated/site-config.json +0 -31
  136. package/templates/default/dist/_astro/BaseLayout.Dz_HR-68.css +0 -1
  137. package/templates/default/dist/about/index.html +0 -165
  138. package/templates/default/dist/icons/platforms/android.svg +0 -1
  139. package/templates/default/dist/icons/platforms/apple-dark.svg +0 -1
  140. package/templates/default/dist/icons/platforms/apple-light.svg +0 -1
  141. package/templates/default/dist/icons/platforms/chrome.svg +0 -1
  142. package/templates/default/dist/icons/platforms/chromeos.svg +0 -5
  143. package/templates/default/dist/icons/platforms/desktop.svg +0 -4
  144. package/templates/default/dist/icons/platforms/embedded.svg +0 -5
  145. package/templates/default/dist/icons/platforms/ios.svg +0 -1
  146. package/templates/default/dist/icons/platforms/linux.svg +0 -1
  147. package/templates/default/dist/icons/platforms/macos.svg +0 -1
  148. package/templates/default/dist/icons/platforms/ubuntu.svg +0 -1
  149. package/templates/default/dist/icons/platforms/web.svg +0 -5
  150. package/templates/default/dist/icons/platforms/windows.svg +0 -6
  151. package/templates/default/dist/icons/stacks/android.svg +0 -1
  152. package/templates/default/dist/icons/stacks/apple-dark.svg +0 -1
  153. package/templates/default/dist/icons/stacks/apple-light.svg +0 -1
  154. package/templates/default/dist/icons/stacks/capacitor.svg +0 -14
  155. package/templates/default/dist/icons/stacks/dart.svg +0 -1
  156. package/templates/default/dist/icons/stacks/firebase.svg +0 -1
  157. package/templates/default/dist/icons/stacks/flutter.svg +0 -1
  158. package/templates/default/dist/icons/stacks/graphql.svg +0 -1
  159. package/templates/default/dist/icons/stacks/ionic.svg +0 -4
  160. package/templates/default/dist/icons/stacks/java.svg +0 -1
  161. package/templates/default/dist/icons/stacks/javascript.svg +0 -1
  162. package/templates/default/dist/icons/stacks/kotlin.svg +0 -1
  163. package/templates/default/dist/icons/stacks/mongodb.svg +0 -1
  164. package/templates/default/dist/icons/stacks/nodejs.svg +0 -1
  165. package/templates/default/dist/icons/stacks/python.svg +0 -1
  166. package/templates/default/dist/icons/stacks/react-native.svg +0 -1
  167. package/templates/default/dist/icons/stacks/react.svg +0 -1
  168. package/templates/default/dist/icons/stacks/rust.svg +0 -1
  169. package/templates/default/dist/icons/stacks/solidity.svg +0 -1
  170. package/templates/default/dist/icons/stacks/swift.svg +0 -1
  171. package/templates/default/dist/icons/stacks/tensorflow.svg +0 -1
  172. package/templates/default/dist/icons/stacks/typescript.svg +0 -1
  173. package/templates/default/dist/index.html +0 -55
  174. package/templates/default/dist/llms-full.txt +0 -14
  175. package/templates/default/dist/llms.txt +0 -11
  176. package/templates/default/dist/og-image.svg +0 -48
  177. package/templates/default/dist/projects/index.html +0 -61
  178. package/templates/default/dist/robots.txt +0 -53
  179. package/templates/default/dist/sitemap.xml +0 -21
  180. package/templates/default/dist/submit/index.html +0 -69
  181. package/templates/default/node_modules/.bin/astro +0 -21
  182. package/templates/default/node_modules/.bin/astro-check +0 -21
  183. package/templates/default/node_modules/.bin/grove +0 -21
  184. package/templates/default/node_modules/.bin/jiti +0 -21
  185. package/templates/default/node_modules/.bin/prettier +0 -21
  186. package/templates/default/node_modules/.bin/rollup +0 -21
  187. package/templates/default/node_modules/.bin/tsc +0 -21
  188. package/templates/default/node_modules/.bin/tsserver +0 -21
  189. package/templates/default/node_modules/.bin/tsx +0 -21
  190. package/templates/default/node_modules/.bin/vite +0 -21
  191. package/templates/default/node_modules/.bin/yaml +0 -21
  192. package/templates/default/node_modules/.vite/deps/_metadata.json +0 -79
  193. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +0 -433
  194. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +0 -7
  195. package/templates/default/node_modules/.vite/deps/astro___aria-query.js +0 -5
  196. package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +0 -7
  197. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +0 -5
  198. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +0 -7
  199. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +0 -9
  200. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +0 -7
  201. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -241
  202. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -7
  203. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +0 -1592
  204. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +0 -7
  205. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
  206. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
  207. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +0 -186
  208. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +0 -7
  209. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +0 -48
  210. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +0 -7
  211. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +0 -6779
  212. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +0 -7
  213. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +0 -33
  214. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +0 -7
  215. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +0 -88
  216. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +0 -7
  217. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +0 -72
  218. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +0 -7
  219. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +0 -3757
  220. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +0 -7
  221. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +0 -54
  222. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +0 -7
  223. package/templates/default/node_modules/.vite/deps/package.json +0 -3
  224. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +0 -213
  225. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +0 -7
  226. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +0 -525
  227. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +0 -7
  228. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +0 -1020
  229. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +0 -7
  230. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +0 -147
  231. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +0 -7
  232. package/templates/default/src/components/layout/BaseLayout.astro +0 -47
  233. package/templates/default/src/components/layout/Container.astro +0 -18
  234. package/templates/default/src/components/layout/Footer.astro +0 -27
  235. package/templates/default/src/components/layout/Header.astro +0 -43
  236. package/templates/default/src/lib/markdown.ts +0 -171
  237. package/templates/default/src/pages/projects/index.astro +0 -193
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sync-contributors — fetch contributors for every record's repoUrl
4
+ * and write `data/generated/contributors.json` (consumed by the home
5
+ * page and the /contributors page).
6
+ *
7
+ * Generic: walks every record in data/generated/records.index.json,
8
+ * looks at record.github.fullName or links.github, and calls the
9
+ * GitHub /repos/{owner}/{repo}/contributors endpoint. Aggregates by
10
+ * owner (across all of an owner's projects in the directory).
11
+ *
12
+ * Anonymous contributors are excluded — the home grid renders <a>
13
+ * avatars, and a name-less contributor is more confusing than
14
+ * missing.
15
+ *
16
+ * Usage: node scripts/sync-contributors.mjs
17
+ * Env: GH_TOKEN — required for >60 req/h. Falls back to unauth.
18
+ *
19
+ * V1 intentionally small: no retries, no rate-limit backoff beyond
20
+ * the basic 403-detection. The weekly schedule in
21
+ * .github/workflows/sync-contributors.yml is loose enough that
22
+ * re-running the workflow is a fine recovery path.
23
+ */
24
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
25
+ import { dirname, resolve } from "node:path";
26
+
27
+ const HERE = new URL(".", import.meta.url).pathname;
28
+ const ROOT = resolve(HERE, "..");
29
+ const GENERATED = resolve(ROOT, "data", "generated");
30
+ const INDEX_PATH = resolve(GENERATED, "records.index.json");
31
+ const OUT_PATH = resolve(GENERATED, "contributors.json");
32
+
33
+ const GH_TOKEN = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
34
+ const HEADERS = {
35
+ Accept: "application/vnd.github+json",
36
+ "User-Agent": "grove-sync-contributors",
37
+ "X-GitHub-Api-Version": "2022-11-28",
38
+ ...(GH_TOKEN ? { Authorization: `Bearer ${GH_TOKEN}` } : {}),
39
+ };
40
+
41
+ function ownerRepoFromFullName(fullName) {
42
+ if (!fullName || !fullName.includes("/")) return null;
43
+ const [owner, repo] = fullName.split("/", 2);
44
+ return { owner, repo };
45
+ }
46
+
47
+ async function ghJson(url) {
48
+ const res = await fetch(url, { headers: HEADERS });
49
+ if (res.status === 204) return null;
50
+ if (res.status === 403) {
51
+ const remaining = res.headers.get("x-ratelimit-remaining");
52
+ if (remaining === "0") {
53
+ throw new Error(`rate-limited (403) on ${url}`);
54
+ }
55
+ return null;
56
+ }
57
+ if (res.status === 404) return null;
58
+ if (!res.ok) throw new Error(`HTTP ${res.status} on ${url}`);
59
+ return res.json();
60
+ }
61
+
62
+ async function fetchContributorsForRepo(owner, repo) {
63
+ // GitHub caps /contributors at 500; for our purposes (fanning into
64
+ // a home grid) we only need the first page. Anonymous entries
65
+ // (no `login`) are dropped at parse time.
66
+ const url = `https://api.github.com/repos/${owner}/${repo}/contributors?per_page=100&anon=false`;
67
+ const data = await ghJson(url);
68
+ if (!Array.isArray(data)) return [];
69
+ return data
70
+ .filter((c) => c && c.login)
71
+ .map((c) => ({
72
+ username: c.login,
73
+ avatarUrl: c.avatar_url,
74
+ profileUrl: c.html_url,
75
+ contributions: c.contributions ?? 0,
76
+ }));
77
+ }
78
+
79
+ async function main() {
80
+ let index;
81
+ try {
82
+ index = JSON.parse(await readFile(INDEX_PATH, "utf8"));
83
+ } catch (err) {
84
+ console.error(`Could not read ${INDEX_PATH}: ${err.message}`);
85
+ console.error("Run `pnpm run build:data` first.");
86
+ process.exit(1);
87
+ }
88
+
89
+ const records = Array.isArray(index.records) ? index.records : [];
90
+ // Group by owner — a single person with three projects in the
91
+ // directory shows up once with the summed contribution count.
92
+ const byOwner = new Map();
93
+ const seenRepos = new Set();
94
+
95
+ for (const r of records) {
96
+ const gh = r.github || {};
97
+ const ref = ownerRepoFromFullName(gh.fullName) || (() => {
98
+ const url = r.repoUrl || (r.links && r.links.github) || "";
99
+ const m = /github\.com\/([^/]+)\/([^/?#]+)/.exec(url);
100
+ return m ? { owner: m[1], repo: m[2].replace(/\.git$/, "") } : null;
101
+ })();
102
+ if (!ref) continue;
103
+ if (seenRepos.has(`${ref.owner}/${ref.repo}`)) continue;
104
+ seenRepos.add(`${ref.owner}/${ref.repo}`);
105
+
106
+ try {
107
+ const list = await fetchContributorsForRepo(ref.owner, ref.repo);
108
+ for (const c of list) {
109
+ const existing = byOwner.get(c.username);
110
+ if (existing) {
111
+ existing.contributions = (existing.contributions ?? 0) + c.contributions;
112
+ } else {
113
+ byOwner.set(c.username, c);
114
+ }
115
+ }
116
+ console.log(` ${ref.owner}/${ref.repo}: ${list.length} contributors`);
117
+ } catch (err) {
118
+ console.warn(` ${ref.owner}/${ref.repo}: ${err.message}`);
119
+ }
120
+ }
121
+
122
+ const contributors = [...byOwner.values()].sort(
123
+ (a, b) => (b.contributions ?? 0) - (a.contributions ?? 0),
124
+ );
125
+
126
+ await mkdir(dirname(OUT_PATH), { recursive: true });
127
+ await writeFile(
128
+ OUT_PATH,
129
+ JSON.stringify(
130
+ {
131
+ generatedAt: new Date().toISOString(),
132
+ contributors,
133
+ },
134
+ null,
135
+ 2,
136
+ ),
137
+ "utf8",
138
+ );
139
+ console.log(`\nWrote ${contributors.length} contributors → ${OUT_PATH}`);
140
+ }
141
+
142
+ main().catch((err) => {
143
+ console.error(err.stack || err.message);
144
+ process.exit(1);
145
+ });
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * sync-github-metadata.mjs — full metadata sync from GitHub's
6
+ * REST API. Wrapper around `@grove-dev/cli sync github`.
7
+ *
8
+ * Refreshes stargazers_count, forks_count, license, default branch,
9
+ * pushed_at, language, topics, and the contributor list for every
10
+ * record whose `repoUrl` is a GitHub repository.
11
+ *
12
+ * Writes back into the corresponding `data/records/<slug>.yml` (the
13
+ * CLI is conservative — only fills in fields that are currently
14
+ * empty).
15
+ */
16
+ import { spawnSync } from "node:child_process";
17
+
18
+ const result = spawnSync("grove", ["sync", "github"], {
19
+ stdio: "inherit",
20
+ env: process.env,
21
+ });
22
+
23
+ if (result.error) {
24
+ console.error("Failed to spawn `grove sync github`:", result.error.message);
25
+ process.exit(1);
26
+ }
27
+
28
+ process.exit(result.status ?? 0);
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * validate-records.mjs — Zod-schema validation for every record yml.
6
+ *
7
+ * Thin wrapper around `@grove-dev/cli`'s `validate` command so the
8
+ * `pnpm run validate:data` script works without V0 command names.
9
+ *
10
+ * For schema authoring see `packages/core/src/schema.ts`. The CLI
11
+ * handles every blueprint (project-directory, resource-hub,
12
+ * ecosystem-map) and every record kind (project, resource, entity).
13
+ */
14
+ import { spawnSync } from "node:child_process";
15
+
16
+ const result = spawnSync("grove", ["validate"], {
17
+ stdio: "inherit",
18
+ env: process.env,
19
+ });
20
+
21
+ if (result.error) {
22
+ console.error("Failed to spawn `grove validate`:", result.error.message);
23
+ console.error("Make sure `@grove-dev/cli` is installed and on PATH.");
24
+ process.exit(1);
25
+ }
26
+
27
+ process.exit(result.status ?? 0);
@@ -1,28 +1,33 @@
1
1
  /**
2
- * Source of truth: `data/generated/records.{full,index,json}`,
3
- * produced at build time by `grove generate` from
4
- * `data/records/*.yml`.
2
+ * Source of truth: `data/generated/records.{full,index,json}` and
3
+ * `data/generated/site-config.json`, produced at build time by
4
+ * `grove generate` from `data/records/*.yml` and `grove.config.ts`.
5
5
  *
6
- * Three flavors are written by the generator:
6
+ * Three flavors of records are written by the generator:
7
7
  *
8
- * - `records.full.json` — every record, all visibility. Carries
9
- * every normalized field (including `content`, `bestFor`,
10
- * `whyListed`, `caveats`, full `github.repository`, ...).
11
- * Use this for the detail page.
12
- * - `records.index.json` — slim projection, visible-only. Use
13
- * this for the list page and any home-page sectioning. Shape
14
- * is the `IndexRecord` discriminated union from `@grove-dev/core`.
15
- * - `records.json` — alias of `records.full.json`.
8
+ * - `records.full.json` — every record, all visibility. Carries
9
+ * every normalized field (including `content`, `bestFor`,
10
+ * `whyListed`, `caveats`, full `github.repository`, ...).
11
+ * Use this for the detail page.
12
+ * - `records.index.json` — slim projection, visible-only. Use
13
+ * this for the list page and any home-page sectioning. Shape
14
+ * is the `IndexRecord` discriminated union from `@grove-dev/core`.
15
+ * - `records.json` — alias of `records.full.json`.
16
16
  *
17
- * The YML files are the human-edited source; this module is a
18
- * typed re-export so pages can import the records without parsing
19
- * JSON inline. Pages cast to the kind they expect based on the
20
- * blueprint in `grove.config.ts`.
17
+ * The YML files are the human-edited source; this module is a
18
+ * typed re-export so pages can import the records without parsing
19
+ * JSON inline.
21
20
  *
22
- * When the JSON files are missing (e.g. before `grove generate`
23
- * runs, or in a fresh scaffold with no records) this module falls
24
- * back to empty arrays so the Astro build still succeeds every
25
- * page must render a graceful "no records yet" state.
21
+ * Blueprint-aware helpers (`indexSlug`, `itemLabel`, `itemsByKind`,
22
+ * `items` default export) read from `data/generated/site-config.json`'s
23
+ * `blueprintConfig` block, so the same module works for all three
24
+ * blueprints `project-directory` (default), `resource-hub`, and
25
+ * `ecosystem-map` — without per-blueprint forks.
26
+ *
27
+ * When the JSON files are missing (e.g. before `grove generate`
28
+ * runs, or in a fresh scaffold with no records) this module falls
29
+ * back to empty arrays so the Astro build still succeeds — every
30
+ * page must render a graceful "no records yet" state.
26
31
  */
27
32
  import { existsSync, readFileSync } from "node:fs";
28
33
  import { fileURLToPath } from "node:url";
@@ -55,6 +60,22 @@ interface IndexPayload {
55
60
  records?: IndexRecord[];
56
61
  }
57
62
 
63
+ interface SiteConfigPayload {
64
+ blueprint?: string;
65
+ blueprintConfig?: {
66
+ id?: string;
67
+ kind?: "project" | "resource" | "entity";
68
+ routeSlug?: string;
69
+ itemSlug?: string;
70
+ /** V1 canonical field name for the record detail slug
71
+ * (replaces the V0 `itemSlug` field). */
72
+ recordSlug?: string;
73
+ labelSingular?: string;
74
+ labelPlural?: string;
75
+ };
76
+ name?: string;
77
+ }
78
+
58
79
  function loadGenerated<T>(filename: string, parser: (raw: string) => T): T {
59
80
  // The Astro build resolves JSON imports from `src/data/*.ts`, but
60
81
  // we want to be defensive about the order of operations: a fresh
@@ -103,17 +124,29 @@ const indexRecordsRaw: IndexRecord[] = loadGenerated(
103
124
  },
104
125
  );
105
126
 
127
+ const siteConfigRaw: SiteConfigPayload = loadGenerated(
128
+ "site-config.json",
129
+ (raw) => {
130
+ if (!raw) return {};
131
+ try {
132
+ return JSON.parse(raw) as SiteConfigPayload;
133
+ } catch {
134
+ return {};
135
+ }
136
+ },
137
+ );
138
+
106
139
  /**
107
- * Full records (all visibility). Use this for the detail page,
108
- * where you need `content`, `bestFor`, `whyListed`, `caveats`,
109
- * the full `github.repository` block, etc.
140
+ * Full records (all visibility). Use this for the detail page,
141
+ * where you need `content`, `bestFor`, `whyListed`, `caveats`,
142
+ * the full `github.repository` block, etc.
110
143
  */
111
144
  export const fullRecords: Resource[] = fullRecordsRaw;
112
145
 
113
146
  /**
114
- * Index-payload records (visible only). Use this for the list
115
- * page and any home-page sectioning, where you only need the
116
- * slim search-index fields.
147
+ * Index-payload records (visible only). Use this for the list
148
+ * page and any home-page sectioning, where you only need the
149
+ * slim search-index fields.
117
150
  */
118
151
  export const records: IndexRecord[] =
119
152
  indexRecordsRaw.length > 0 ? indexRecordsRaw : [];
@@ -140,8 +173,8 @@ export function recordBySlug(slug: string): Resource | undefined {
140
173
  }
141
174
 
142
175
  /**
143
- * Generic slug lookup. The spec name is `findRecord`; this is an
144
- * alias of `recordBySlug` so consumers can use either spelling.
176
+ * Generic slug lookup. The spec name is `findRecord`; this is an
177
+ * alias of `recordBySlug` so consumers can use either spelling.
145
178
  */
146
179
  export function findRecord(slug: string): Resource | undefined {
147
180
  return bySlug.get(slug);
@@ -162,32 +195,86 @@ export function entityBySlug(slug: string): EntityRecord | undefined {
162
195
  return r && r.kind === "entity" ? r : undefined;
163
196
  }
164
197
 
198
+ // ──────────────────────────────────────────────────────────────────────
199
+ // Blueprint-aware generic helpers
200
+ // ──────────────────────────────────────────────────────────────────────
201
+ //
202
+ // Every page (home, list, detail, submit) needs to know the
203
+ // route slug, the kind filter, and the human label. Rather than
204
+ // hardcode "project" / "projects" / "kind: project" at every
205
+ // call-site, we derive them once from `site-config.json`'s
206
+ // `blueprintConfig` block (populated by `grove generate`).
207
+ //
208
+ // `indexSlug()` and `itemLabel()` retain their old signatures
209
+ // (zero-arg) so existing pages keep working — they now read from
210
+ // the JSON instead of a switch statement.
211
+
212
+ const blueprintConfig = siteConfigRaw.blueprintConfig ?? {};
213
+ const blueprintKind = (blueprintConfig.kind ?? "project") as
214
+ | "project"
215
+ | "resource"
216
+ | "entity";
217
+ const blueprintId = (blueprintConfig.id ?? "project-directory") as
218
+ | "project-directory"
219
+ | "resource-hub"
220
+ | "ecosystem-map";
221
+
165
222
  /**
166
- * Page slug for the directory index page, derived from the
167
- * blueprint in `grove.config.ts` (mirrors the helper that used
168
- * to live in `data/site-config.ts`).
223
+ * URL slug for the directory index page (e.g. `/projects/`,
224
+ * `/resources/`, `/entities/`). Override in `grove.config.ts`
225
+ * via `routes.directory` — reflected through to here at generate
226
+ * time.
169
227
  */
170
- export function indexSlug(blueprint: string | undefined): string {
171
- switch (blueprint) {
172
- case "resource-hub":
173
- return "resources";
174
- case "ecosystem-map":
175
- return "entities";
176
- case "project-directory":
177
- default:
178
- return "projects";
179
- }
228
+ export function indexSlug(): string {
229
+ return blueprintConfig.routeSlug ?? "projects";
180
230
  }
181
231
 
182
- /** Human label for items in the UI. */
183
- export function itemLabel(blueprint: string | undefined): string {
184
- switch (blueprint) {
185
- case "resource-hub":
186
- return "resource";
187
- case "ecosystem-map":
188
- return "entity";
189
- case "project-directory":
232
+ /** URL slug for a single record detail page (the dynamic
233
+ * `[recordSlug]` segment). Default "project" for backwards-compat
234
+ * with V0-published configs that exposed the field as `itemSlug`. */
235
+ export function recordSlugConfig(): string {
236
+ return blueprintConfig.recordSlug ?? blueprintConfig.itemSlug ?? "project";
237
+ }
238
+
239
+ /** Singular human label, e.g. "project", "resource", "entity". */
240
+ export function itemLabel(): string {
241
+ return blueprintConfig.labelSingular ?? "project";
242
+ }
243
+
244
+ /** Plural human label, e.g. "projects", "resources", "entities". */
245
+ export function itemLabelPlural(): string {
246
+ return blueprintConfig.labelPlural ?? "projects";
247
+ }
248
+
249
+ /** Active blueprint id. */
250
+ export function blueprintIdFn(): string {
251
+ return blueprintId;
252
+ }
253
+
254
+ /** Active record kind discriminator. */
255
+ export function activeKind(): "project" | "resource" | "entity" {
256
+ return blueprintKind;
257
+ }
258
+
259
+ /**
260
+ * Generic items alias. Defaults to the kind this blueprint
261
+ * produces (project/resource/entity). Use this for all
262
+ * "list"-flavoured pages so a single template works for every
263
+ * blueprint.
264
+ */
265
+ export const items: IndexRecord[] = (() => {
266
+ switch (blueprintKind) {
267
+ case "resource":
268
+ return resources;
269
+ case "entity":
270
+ return entities;
271
+ case "project":
190
272
  default:
191
- return "project";
273
+ return projects;
192
274
  }
193
- }
275
+ })();
276
+
277
+ /** Generic full-record alias. */
278
+ export const fullItems: Resource[] = (() => {
279
+ return fullRecords.filter((r) => r.kind === blueprintKind);
280
+ })();
@@ -0,0 +1,64 @@
1
+ ---
2
+ /**
3
+ * 404 — generic fallback page.
4
+ *
5
+ * Renders the normal shell (header / footer / theme toggle) with a
6
+ * short message and a search form pointing at the list page. Stays
7
+ * on-brand regardless of blueprint.
8
+ */
9
+ import siteConfig from "../../data/generated/site-config.json";
10
+ import { indexSlug } from "../data/records";
11
+ import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
12
+
13
+ const slug = indexSlug();
14
+ const title = `Not found — ${siteConfig.name}`;
15
+ const description = `That page doesn't exist on ${siteConfig.name}.`;
16
+ ---
17
+
18
+ <BaseLayout
19
+ title={title}
20
+ description={description}
21
+ site={siteConfig}
22
+ noindex
23
+ >
24
+ <section class="grove-section">
25
+ <div class="grove-container mx-auto px-5 py-20 sm:px-7 sm:py-28">
26
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
27
+ 404
28
+ </span>
29
+ <h1 class="m-0 mt-2 text-[2.25rem] sm:text-[3rem] font-semibold leading-[1.05] tracking-tight text-ink-900 dark:text-ink-100">
30
+ Page not found
31
+ </h1>
32
+ <p class="m-0 mt-4 max-w-xl text-lg leading-relaxed text-ink-700 dark:text-ink-200">
33
+ The page you're looking for doesn't exist on {siteConfig.name}.
34
+ It may have been moved, renamed, or never existed.
35
+ </p>
36
+
37
+ <form
38
+ class="mt-8 flex max-w-xl flex-col gap-2 sm:flex-row sm:items-center"
39
+ role="search"
40
+ action={`/${slug}`}
41
+ method="get"
42
+ >
43
+ <label class="relative flex-1">
44
+ <span class="sr-only">Search</span>
45
+ <input
46
+ type="search"
47
+ name="q"
48
+ placeholder={`Search ${slug}...`}
49
+ autocomplete="off"
50
+ class="w-full rounded-md border border-ink-200 bg-white py-2.5 px-3 text-sm placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
51
+ />
52
+ </label>
53
+ <button type="submit" class="grove-btn grove-btn-primary">
54
+ Search
55
+ </button>
56
+ </form>
57
+
58
+ <div class="mt-6 flex flex-wrap gap-3">
59
+ <a href="/" class="grove-btn grove-btn-primary">Back to home</a>
60
+ <a href={`/${slug}`} class="grove-btn grove-btn-outline">Browse {slug}</a>
61
+ </div>
62
+ </div>
63
+ </section>
64
+ </BaseLayout>