@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
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  /**
3
- * Detail page — one record at a time.
3
+ * Detail page — one record at a time.
4
4
  *
5
- * Layout (top to bottom):
6
- * 1. Breadcrumb — back to /projects (or blueprint slug)
5
+ * Layout (top to bottom):
6
+ * 1. Breadcrumb — back to the directory index
7
7
  * 2. AppCard-style header — logo + name + owner + description + CTAs
8
8
  * 3. ScoreBars — 5-dimension + overall score block
9
9
  * 4. BestFor / WhyListed / Caveats — 3-column curation grid
@@ -14,14 +14,24 @@
14
14
  * body) when present
15
15
  * 9. JSON-LD SoftwareSourceCode — for SEO + AI crawlers
16
16
  *
17
- * The page is generic across the 3 record kinds (project / resource
18
- * / entity) — non-project fields simply render nothing.
17
+ * Generic across the 3 record kinds (project / resource / entity) —
18
+ * non-project fields simply render nothing. The URL is dynamic on
19
+ * both `[slug]` (blueprint route, e.g. `projects`/`resources`/
20
+ * `entities`) and `[recordSlug]` (the record slug), so a single page
21
+ * file serves every blueprint and every record.
19
22
  */
20
23
  import siteConfig from "../../../data/generated/site-config.json";
21
- import { fullRecords, projectBySlug, resourceBySlug, entityBySlug, indexSlug } from "../../data/records";
24
+ import {
25
+ fullRecords,
26
+ projectBySlug,
27
+ resourceBySlug,
28
+ entityBySlug,
29
+ indexSlug,
30
+ } from "../../data/records";
22
31
  import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
23
32
  import Icon from "@grove-dev/astro/components/Icon.astro";
24
33
  import ScoreBars from "@grove-dev/astro/components/ScoreBars.astro";
34
+ import CurationGrid from "@grove-dev/astro/components/CurationGrid.astro";
25
35
  import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
26
36
  import { formatRelative, formatStars } from "@grove-dev/astro";
27
37
  import { statusDisplay, prettySlug, labelDisplay } from "@grove-dev/astro";
@@ -29,15 +39,27 @@ import { readFileSync, existsSync } from "node:fs";
29
39
  import { fileURLToPath } from "node:url";
30
40
  import { dirname, resolve } from "node:path";
31
41
  import type { ProjectRecord } from "@grove-dev/core";
42
+ import { marked } from "marked";
32
43
 
33
- export function getStaticPaths() {
44
+ export async function getStaticPaths() {
45
+ // Walk every record and emit a path for it. The directory slug
46
+ // (`projects` / `resources` / `entities`) is taken from the
47
+ // blueprint config so a `resource-hub` blueprint produces
48
+ // `/resources/<slug>` URLs without any per-blueprint fork.
49
+ let dirSlug = "projects";
50
+ try {
51
+ const raw = await import("../../../data/generated/site-config.json");
52
+ dirSlug = raw.default?.blueprintConfig?.routeSlug ?? "projects";
53
+ } catch {
54
+ /* not present */
55
+ }
34
56
  return fullRecords.map((record) => ({
35
- params: { slug: record.slug },
57
+ params: { slug: dirSlug, recordSlug: record.slug },
36
58
  }));
37
59
  }
38
60
 
39
- const slug = indexSlug();
40
- const recordSlug = Astro.params.slug ?? "";
61
+ const slug = Astro.params.slug ?? indexSlug();
62
+ const recordSlug = Astro.params.recordSlug ?? "";
41
63
  const project = projectBySlug(recordSlug);
42
64
  const resource = resourceBySlug(recordSlug);
43
65
  const entity = entityBySlug(recordSlug);
@@ -55,6 +77,7 @@ const name =
55
77
 
56
78
  const title = `${name} — ${siteConfig.name}`;
57
79
  const description = record.description ?? `${name} on ${siteConfig.name}`;
80
+ const itemSingular = siteConfig.blueprintConfig?.labelSingular ?? record.kind;
58
81
 
59
82
  const repoUrl = proj?.repoUrl ?? (record.links as { github?: string } | undefined)?.github ?? "";
60
83
  const homepageUrl = record.links?.website ?? "";
@@ -96,25 +119,50 @@ if (isProject && proj?.content) {
96
119
  const contentPath = candidates.find((p) => existsSync(p));
97
120
  if (contentPath) {
98
121
  try {
99
- // Cheap markdown → HTML using a minimal renderer would be
100
- // nice, but we don't want to pull another dep into the
101
- // template. Render the file as a <pre> for now so the body
102
- // still surfaces on the page. Consumers can replace this
103
- // with a real markdown pipeline (`marked`, `remark`, ...)
104
- // by overriding the `[slug].astro` page.
105
122
  const text = readFileSync(contentPath, "utf8");
106
- contentHtml = text
107
- .replace(/&/g, "&amp;")
108
- .replace(/</g, "&lt;")
109
- .replace(/>/g, "&gt;");
123
+ contentHtml = await marked.parse(text);
110
124
  } catch {
111
125
  contentHtml = null;
112
126
  }
113
127
  }
114
128
  }
115
129
 
116
- // ── JSON-LD (SoftwareSourceCode) for project-kind records ──────
117
- let jsonLd: Record<string, unknown> | null = null;
130
+ // ── Detail-page decision-signal data (5 sections) ────────────
131
+ // Hoist out of the template body so the JSX stays free of
132
+ // complex TypeScript type assertions.
133
+ type MonthlyCommit = number | { month: string; commits: number };
134
+ type ProjectExtras = {
135
+ github?: {
136
+ activity?: { monthlyCommits?: MonthlyCommit[] };
137
+ files?: Record<string, boolean>;
138
+ languages?: Record<string, number>;
139
+ };
140
+ health?: { status?: string; tier?: string };
141
+ };
142
+ const extras = record as ProjectExtras;
143
+ const monthlyCommits = (extras.github?.activity?.monthlyCommits ?? []).map(
144
+ (it): { month: string; commits: number } =>
145
+ typeof it === "number" ? { month: "", commits: it } : { month: it.month ?? "", commits: it.commits ?? 0 },
146
+ );
147
+ const maxMonthlyCommits = Math.max(1, ...monthlyCommits.map((i) => i.commits));
148
+ const contributionFiles = extras.github?.files ?? {};
149
+ const contributionLabels: Record<string, string> = {
150
+ readme: "README",
151
+ contributing: "Contributing",
152
+ security: "Security",
153
+ issueTemplates: "Issue templates",
154
+ pullRequestTemplate: "PR template",
155
+ };
156
+ const contributionSignals = (Object.keys(contributionLabels) as Array<keyof typeof contributionLabels>)
157
+ .map((k) => ({ key: k, label: contributionLabels[k], ok: contributionFiles[k] }))
158
+ .filter((s) => s.ok !== undefined);
159
+ const languages = extras.github?.languages
160
+ ? Object.entries(extras.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
161
+ : [];
162
+ const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, b]) => sum + b, 0));
163
+
164
+ // ── Kind-aware JSON-LD ─────────────────────────────────────────
165
+ let jsonLd: Record<string, unknown>;
118
166
  if (isProject && proj) {
119
167
  jsonLd = {
120
168
  "@context": "https://schema.org",
@@ -129,6 +177,33 @@ if (isProject && proj) {
129
177
  ? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
130
178
  : undefined,
131
179
  };
180
+ } else if (record.kind === "resource") {
181
+ const resourceSchemaType: Record<string, string> = {
182
+ article: "Article",
183
+ book: "Book",
184
+ course: "Course",
185
+ podcast: "PodcastSeries",
186
+ video: "VideoObject",
187
+ };
188
+ jsonLd = {
189
+ "@context": "https://schema.org",
190
+ "@type": resourceSchemaType[record.type] ?? "CreativeWork",
191
+ name,
192
+ description: record.description || undefined,
193
+ url: homepageUrl || undefined,
194
+ author: record.author ? { "@type": "Person", name: record.author } : undefined,
195
+ datePublished: record.publishedAt || undefined,
196
+ };
197
+ } else {
198
+ jsonLd = {
199
+ "@context": "https://schema.org",
200
+ "@type": record.type === "person" ? "Person" : "Organization",
201
+ name,
202
+ description: record.description || undefined,
203
+ url: homepageUrl || undefined,
204
+ foundingDate: record.founded || undefined,
205
+ location: record.location || undefined,
206
+ };
132
207
  }
133
208
  ---
134
209
 
@@ -137,7 +212,7 @@ if (isProject && proj) {
137
212
  description={description}
138
213
  site={siteConfig}
139
214
  type="article"
140
- jsonLd={jsonLd ?? undefined}
215
+ jsonLd={jsonLd}
141
216
  submitHref="/submit"
142
217
  submitLabel={`Submit ${name}`}
143
218
  >
@@ -267,45 +342,13 @@ if (isProject && proj) {
267
342
  )}
268
343
 
269
344
  {/* ── BestFor / WhyListed / Caveats grid ──────────────── */}
270
- {isProject && (bestFor.length > 0 || whyListed.length > 0 || caveats.length > 0) && (
271
- <section class="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3" aria-label="Curation notes">
272
- {bestFor.length > 0 && (
273
- <div class="grove-card p-5">
274
- <h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
275
- Best for
276
- </h3>
277
- <ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
278
- {bestFor.map((b) => (
279
- <li class="text-sm text-ink-700 dark:text-ink-200">— {b}</li>
280
- ))}
281
- </ul>
282
- </div>
283
- )}
284
- {whyListed.length > 0 && (
285
- <div class="grove-card p-5">
286
- <h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
287
- Why listed
288
- </h3>
289
- <ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
290
- {whyListed.map((w) => (
291
- <li class="text-sm text-ink-700 dark:text-ink-200">— {w}</li>
292
- ))}
293
- </ul>
294
- </div>
295
- )}
296
- {caveats.length > 0 && (
297
- <div class="grove-card p-5">
298
- <h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
299
- Caveats
300
- </h3>
301
- <ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
302
- {caveats.map((c) => (
303
- <li class="text-sm text-ink-700 dark:text-ink-200">— {c}</li>
304
- ))}
305
- </ul>
306
- </div>
307
- )}
308
- </section>
345
+ {isProject && (
346
+ <CurationGrid
347
+ bestFor={bestFor}
348
+ whyListed={whyListed}
349
+ caveats={caveats}
350
+ class="mt-8"
351
+ />
309
352
  )}
310
353
 
311
354
  {/* ── GitHub signals dl ────────────────────────────────── */}
@@ -372,6 +415,132 @@ if (isProject && proj) {
372
415
  </section>
373
416
  )}
374
417
 
418
+ {/* ── Decision signals (3-col) ──────────────────────────── */}
419
+ {isProject && github && (
420
+ <section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-3" aria-label="Decision signals">
421
+ {/* Repository health 3-col card */}
422
+ <div class="grove-card p-5">
423
+ <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
424
+ Repository health
425
+ </h3>
426
+ <dl class="mt-4 grid gap-3 text-sm">
427
+ <div class="flex items-center justify-between gap-3">
428
+ <dt class="text-ink-500 dark:text-ink-400">Status</dt>
429
+ <dd class="font-medium text-ink-900 dark:text-ink-100">
430
+ {healthLabel ?? "—"}
431
+ </dd>
432
+ </div>
433
+ <div class="flex items-center justify-between gap-3">
434
+ <dt class="text-ink-500 dark:text-ink-400">Stars</dt>
435
+ <dd class="font-mono text-ink-900 dark:text-ink-100">
436
+ {starsLabel ?? "—"}
437
+ </dd>
438
+ </div>
439
+ <div class="flex items-center justify-between gap-3">
440
+ <dt class="text-ink-500 dark:text-ink-400">Forks</dt>
441
+ <dd class="font-mono text-ink-900 dark:text-ink-100">
442
+ {forks}
443
+ </dd>
444
+ </div>
445
+ <div class="flex items-center justify-between gap-3">
446
+ <dt class="text-ink-500 dark:text-ink-400">Open issues</dt>
447
+ <dd class="font-mono text-ink-900 dark:text-ink-100">
448
+ {github?.open_issues_count ?? "—"}
449
+ </dd>
450
+ </div>
451
+ {licenseSpdx && (
452
+ <div class="flex items-center justify-between gap-3">
453
+ <dt class="text-ink-500 dark:text-ink-400">License</dt>
454
+ <dd class="font-mono text-ink-900 dark:text-ink-100">{licenseSpdx}</dd>
455
+ </div>
456
+ )}
457
+ </dl>
458
+ </div>
459
+
460
+ {/* Activity bar chart */}
461
+ <div class="grove-card p-5">
462
+ <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
463
+ Activity
464
+ </h3>
465
+ {monthlyCommits.length > 0 ? (
466
+ <figure class="mt-4 m-0">
467
+ <div
468
+ class="flex h-28 items-end gap-1.5"
469
+ role="img"
470
+ aria-label="Monthly commits over the last 12 months"
471
+ >
472
+ {monthlyCommits.map((item) => (
473
+ <div class="flex min-w-0 flex-1 flex-col items-center gap-1">
474
+ <div
475
+ class="w-full rounded-sm bg-ink-700 dark:bg-ink-200"
476
+ style={`height: ${Math.max(8, Math.round((item.commits / maxMonthlyCommits) * 96))}px`}
477
+ title={`${item.month || "month"}: ${item.commits} commits`}
478
+ />
479
+ <span class="max-w-full truncate text-[10px] text-ink-500 dark:text-ink-400">
480
+ {item.month ? item.month.slice(5) : item.commits}
481
+ </span>
482
+ </div>
483
+ ))}
484
+ </div>
485
+ <figcaption class="sr-only">
486
+ Monthly commit count over the last 12 months
487
+ </figcaption>
488
+ </figure>
489
+ ) : (
490
+ <p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
491
+ Monthly commit history is not yet populated for this {itemSingular}.
492
+ </p>
493
+ )}
494
+ </div>
495
+
496
+ {/* Contribution readiness chips */}
497
+ <div class="grove-card p-5">
498
+ <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
499
+ Contribution readiness
500
+ </h3>
501
+ {contributionSignals.length > 0 ? (
502
+ <ul class="mt-4 m-0 grid list-none gap-2 p-0 text-sm">
503
+ {contributionSignals.map((signal) => (
504
+ <li class="flex items-center justify-between gap-3">
505
+ <span class="text-ink-700 dark:text-ink-200">{signal.label}</span>
506
+ <span class={signal.ok ? "text-emerald-700 dark:text-emerald-400" : "text-ink-400 dark:text-ink-600"}>
507
+ {signal.ok ? "Found" : "Missing"}
508
+ </span>
509
+ </li>
510
+ ))}
511
+ </ul>
512
+ ) : (
513
+ <p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
514
+ Contribution files have not been synced yet.
515
+ </p>
516
+ )}
517
+ </div>
518
+ </section>
519
+ )}
520
+
521
+ {/* ── Language mix ─────────────────────────────────────── */}
522
+ {isProject && languages.length > 0 && (
523
+ <section class="mt-4 grove-card p-5">
524
+ <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
525
+ Language mix
526
+ </h3>
527
+ <dl class="mt-4 grid gap-2 text-sm">
528
+ {languages.map(([language, bytes]) => {
529
+ const pct = Math.round((bytes / totalLanguageBytes) * 100);
530
+ return (
531
+ <div class="grid grid-cols-[8rem_minmax(0,1fr)_3rem] items-center gap-3">
532
+ <dt class="truncate text-ink-700 dark:text-ink-200">{language}</dt>
533
+ <dd class="h-2 overflow-hidden rounded-sm bg-ink-200 dark:bg-ink-800">
534
+ <div class="h-full bg-ink-700 dark:bg-ink-200" style={`width: ${pct}%`} />
535
+ </dd>
536
+ <dd class="text-right font-mono text-2xs text-ink-500 dark:text-ink-400">{pct}%</dd>
537
+ </div>
538
+ );
539
+ })}
540
+ </dl>
541
+ </section>
542
+ )}
543
+
375
544
  {/* ── Stack + Platform chips ───────────────────────────── */}
376
545
  {isProject && (stacks.length > 0 || platforms.length > 0) && (
377
546
  <section class="mt-8" aria-labelledby="stacks-heading">
@@ -464,15 +633,16 @@ if (isProject && proj) {
464
633
  </section>
465
634
  )}
466
635
 
467
- {/* ── Content markdown (record.content → <pre> for now) ── */}
636
+ {/* ── Content markdown ─────────────────────────────────── */}
468
637
  {contentHtml && (
469
638
  <section class="mt-8" aria-labelledby="content-heading">
470
639
  <h2 id="content-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
471
640
  Notes
472
641
  </h2>
473
- <pre
474
- class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"
475
- ><code>{contentHtml}</code></pre>
642
+ <div
643
+ class="grove-prose mt-4"
644
+ set:html={contentHtml}
645
+ />
476
646
  </section>
477
647
  )}
478
648
  </div>