@grove-dev/astro 0.7.0 → 0.8.0

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 (110) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +22 -33
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.d.ts +1 -1
  6. package/dist/lib/display.js +1 -1
  7. package/dist/lib/format.d.ts +1 -1
  8. package/dist/lib/format.js +1 -1
  9. package/dist/lib/icon-kinds.d.ts +1 -1
  10. package/dist/lib/icon-kinds.js +43 -43
  11. package/dist/lib/icon-registry.d.ts +2 -2
  12. package/dist/lib/icon-registry.js +23 -23
  13. package/dist/lib/index.d.ts +9 -9
  14. package/dist/lib/index.d.ts.map +1 -1
  15. package/dist/lib/index.js +9 -9
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/lib/lenses.d.ts +1 -1
  18. package/dist/lib/lenses.js +1 -1
  19. package/dist/lib/packaged-icons.d.ts +1 -1
  20. package/dist/lib/packaged-icons.js +4 -4
  21. package/dist/lib/repo.d.ts +1 -1
  22. package/dist/lib/repo.js +1 -1
  23. package/dist/lib/search.d.ts +1 -1
  24. package/dist/lib/search.js +1 -1
  25. package/dist/lib/taxonomy-counts.d.ts +1 -1
  26. package/dist/lib/taxonomy-counts.js +1 -1
  27. package/package.json +7 -17
  28. package/src/index.ts +23 -38
  29. package/src/lib/display.ts +1 -1
  30. package/src/lib/format.ts +1 -1
  31. package/src/lib/icon-kinds.ts +44 -44
  32. package/src/lib/icon-registry.ts +25 -25
  33. package/src/lib/index.ts +9 -9
  34. package/src/lib/lenses.ts +1 -1
  35. package/src/lib/packaged-icons.ts +8 -8
  36. package/src/lib/repo.ts +1 -1
  37. package/src/lib/search.ts +1 -1
  38. package/src/lib/taxonomy-counts.ts +1 -1
  39. package/src/server/collections.ts +53 -42
  40. package/src/server/contrast.ts +14 -9
  41. package/src/server/directory.ts +22 -17
  42. package/src/server/index.ts +3 -2
  43. package/src/server/models.ts +404 -213
  44. package/src/server/seo.ts +34 -31
  45. package/src/styles.css +235 -91
  46. package/dist/layouts/index.d.ts +0 -25
  47. package/dist/layouts/index.d.ts.map +0 -1
  48. package/dist/layouts/index.js +0 -25
  49. package/dist/layouts/index.js.map +0 -1
  50. package/dist/ui/button.d.ts +0 -53
  51. package/dist/ui/button.d.ts.map +0 -1
  52. package/dist/ui/button.js +0 -90
  53. package/dist/ui/button.js.map +0 -1
  54. package/src/components/CardGrid.astro +0 -41
  55. package/src/components/CardIcon.astro +0 -67
  56. package/src/components/CategoryGrid.astro +0 -114
  57. package/src/components/CollectionCard.astro +0 -56
  58. package/src/components/CollectionIndex.astro +0 -102
  59. package/src/components/CollectionPage.astro +0 -128
  60. package/src/components/CollectionRow.astro +0 -81
  61. package/src/components/CollectionTeaser.astro +0 -47
  62. package/src/components/ContributorsGrid.astro +0 -143
  63. package/src/components/DirectoryIndexClient.astro +0 -510
  64. package/src/components/EditorialSummary.astro +0 -58
  65. package/src/components/FilterGroupMenu.astro +0 -69
  66. package/src/components/FilterOptions.astro +0 -49
  67. package/src/components/FinalCta.astro +0 -43
  68. package/src/components/Hero.astro +0 -243
  69. package/src/components/Icon.astro +0 -191
  70. package/src/components/IndexRow.astro +0 -37
  71. package/src/components/LanguageBreakdown.astro +0 -67
  72. package/src/components/MarkdownBody.astro +0 -37
  73. package/src/components/OriginalCollection.astro +0 -117
  74. package/src/components/Pagination.astro +0 -50
  75. package/src/components/PoweredBy.astro +0 -60
  76. package/src/components/ProjectCard.astro +0 -321
  77. package/src/components/RecordHeader.astro +0 -221
  78. package/src/components/RecordSection.astro +0 -128
  79. package/src/components/RecordSidebar.astro +0 -387
  80. package/src/components/RefinePanel.astro +0 -168
  81. package/src/components/SmartLensTabs.astro +0 -81
  82. package/src/components/StackGrid.astro +0 -151
  83. package/src/components/StackPlatformChips.astro +0 -73
  84. package/src/components/SubmissionClient.astro +0 -254
  85. package/src/components/TableOfContents.astro +0 -140
  86. package/src/components/WhyThisExists.astro +0 -116
  87. package/src/layouts/BaseLayout.astro +0 -415
  88. package/src/layouts/Container.astro +0 -18
  89. package/src/layouts/Footer.astro +0 -231
  90. package/src/layouts/Header.astro +0 -214
  91. package/src/layouts/SectionHeader.astro +0 -43
  92. package/src/layouts/Seo.astro +0 -171
  93. package/src/layouts/ThemeToggle.astro +0 -118
  94. package/src/layouts/astro-shim.d.ts +0 -29
  95. package/src/layouts/index.ts +0 -24
  96. package/src/server/collections.test.ts +0 -212
  97. package/src/server/contrast.test.ts +0 -82
  98. package/src/server/directory.test.ts +0 -177
  99. package/src/server/github-repo.test.ts +0 -88
  100. package/src/server/models-home.test.ts +0 -101
  101. package/src/server/models.test.ts +0 -135
  102. package/src/server/seo.test.ts +0 -123
  103. package/src/ui/Badge.astro +0 -38
  104. package/src/ui/Button.astro +0 -42
  105. package/src/ui/EmptyState.astro +0 -38
  106. package/src/ui/FilterDrawer.astro +0 -124
  107. package/src/ui/PageHeader.astro +0 -39
  108. package/src/ui/SearchField.astro +0 -106
  109. package/src/ui/button.test.ts +0 -54
  110. package/src/ui/button.ts +0 -107
package/src/index.ts CHANGED
@@ -24,49 +24,41 @@
24
24
  * which is the classic "I changed the brand color in global.css
25
25
  * and nothing happened" surprise.
26
26
  */
27
- import { existsSync } from "node:fs";
28
- import { fileURLToPath } from "node:url";
29
- import { dirname, resolve } from "node:path";
30
- import type { AstroIntegration } from "astro";
31
- import { loadConfig, prepareDirectory } from "@grove-dev/core";
32
- import { syncPackagedIcons } from "./lib/packaged-icons.js";
27
+ import { existsSync } from 'node:fs';
28
+ import { dirname, resolve } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { loadConfig, prepareDirectory } from '@grove-dev/core';
31
+ import type { AstroIntegration } from 'astro';
32
+ import { syncPackagedIcons } from './lib/packaged-icons.js';
33
33
 
34
- export * from "@grove-dev/core";
34
+ export * from '@grove-dev/core';
35
35
 
36
36
  // Generic lib helpers — repo URL parsing, formatting, search /
37
37
  // facet / sort / paginate, lens application, score tiers, taxonomy
38
38
  // counts, and pretty-print display maps. All dependency-free and
39
39
  // typed against `@grove-dev/core`.
40
- export * from "./lib/index.js";
40
+ export * from './lib/index.js';
41
41
 
42
42
  const here = dirname(fileURLToPath(import.meta.url));
43
- // `here` is the compiled `dist/` directory. The components and
44
- // layouts live in the source tree (the `dist/` is for the
45
- // integration helper only). Walk up one level to reach `src/`
46
- // before resolving the subpath roots.
47
- const srcRoot = resolve(here, "..", "src");
48
- const componentsDir = resolve(srcRoot, "components");
49
- const layoutsDir = resolve(srcRoot, "layouts");
50
43
 
51
44
  // Virtual module id used to inject the consumer's `src/styles/global.css`.
52
45
  // Resolved by the inline Vite plugin below to the absolute file path
53
46
  // when the file exists, or to an empty module when it doesn't.
54
- const CONSUMER_GLOBAL_CSS_VIRTUAL_ID = "virtual:grove-consumer-global-css";
55
- const CONSUMER_GLOBAL_CSS_EMPTY_ID =
56
- "\0virtual:grove-consumer-global-css:empty";
47
+ const CONSUMER_GLOBAL_CSS_VIRTUAL_ID = 'virtual:grove-consumer-global-css';
48
+ const CONSUMER_GLOBAL_CSS_EMPTY_ID = '\0virtual:grove-consumer-global-css:empty';
57
49
 
58
50
  export default function groveAstro(): AstroIntegration {
59
51
  return {
60
- name: "@grove-dev/astro",
52
+ name: '@grove-dev/astro',
61
53
  hooks: {
62
- "astro:config:setup": async ({ config, updateConfig, injectScript, logger }) => {
54
+ 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {
63
55
  // Alias the components/layouts source directories so
64
56
  // consumer builds can import from
65
57
  // @grove-dev/astro/components/ProjectCard.astro
66
58
  // without us shipping a glob-shaped `exports` map that
67
59
  // Vite/Rollup does not expand reliably.
68
60
  const consumerRoot = fileURLToPath(config.root);
69
- const groveConfigPath = resolve(consumerRoot, "grove.config.ts");
61
+ const groveConfigPath = resolve(consumerRoot, 'grove.config.ts');
70
62
  if (existsSync(groveConfigPath)) {
71
63
  await prepareDirectory(consumerRoot);
72
64
  // Data preparation and the icon sync below are the integration's
@@ -87,26 +79,24 @@ export default function groveAstro(): AstroIntegration {
87
79
  // control is what makes it recoverable.
88
80
  logger.info(
89
81
  `Adopted ${icons.written.length} icon(s) into public/icons/ and recorded them in ` +
90
- ".grove-icons.json. Review with `git diff public/icons` if you had edited any.",
82
+ '.grove-icons.json. Review with `git diff public/icons` if you had edited any.',
91
83
  );
92
84
  }
93
85
  if (icons.skipped.length > 0) {
94
86
  logger.warn(
95
- `Kept ${icons.skipped.length} locally modified icon(s): ${icons.skipped.join(", ")}. ` +
96
- "Run `grove icons sync --force` to restore the packaged versions.",
87
+ `Kept ${icons.skipped.length} locally modified icon(s): ${icons.skipped.join(', ')}. ` +
88
+ 'Run `grove icons sync --force` to restore the packaged versions.',
97
89
  );
98
90
  }
99
91
  }
100
- const globalCssPath = resolve(consumerRoot, "src/styles/global.css");
92
+ const globalCssPath = resolve(consumerRoot, 'src/styles/global.css');
101
93
  const globalCssExists = existsSync(globalCssPath);
102
94
 
103
95
  updateConfig({
104
96
  vite: {
105
97
  resolve: {
106
98
  alias: {
107
- "@grove-dev/astro/components": componentsDir,
108
- "@grove-dev/astro/layouts": layoutsDir,
109
- "@grove/generated": resolve(consumerRoot, "data/generated"),
99
+ '@grove/generated': resolve(consumerRoot, 'data/generated'),
110
100
  },
111
101
  },
112
102
  // The OG image pipeline (`@grove-dev/core`'s og-image.ts)
@@ -115,22 +105,20 @@ export default function groveAstro(): AstroIntegration {
115
105
  // time) — never in page code — so keep the bundler away
116
106
  // from the .node binary.
117
107
  ssr: {
118
- external: ["satori", "@resvg/resvg-js"],
108
+ external: ['satori', '@resvg/resvg-js'],
119
109
  },
120
110
  plugins: [
121
111
  {
122
- name: "grove:consumer-global-css-resolver",
112
+ name: 'grove:consumer-global-css-resolver',
123
113
  resolveId(id) {
124
114
  if (id === CONSUMER_GLOBAL_CSS_VIRTUAL_ID) {
125
- return globalCssExists
126
- ? globalCssPath
127
- : CONSUMER_GLOBAL_CSS_EMPTY_ID;
115
+ return globalCssExists ? globalCssPath : CONSUMER_GLOBAL_CSS_EMPTY_ID;
128
116
  }
129
117
  return null;
130
118
  },
131
119
  load(id) {
132
120
  if (id === CONSUMER_GLOBAL_CSS_EMPTY_ID) {
133
- return "";
121
+ return '';
134
122
  }
135
123
  return null;
136
124
  },
@@ -143,10 +131,7 @@ export default function groveAstro(): AstroIntegration {
143
131
  // regardless of whether the consumer has a global.css yet.
144
132
  // Vite's CSS pipeline (with @tailwindcss/vite) handles the
145
133
  // resulting import as a stylesheet — no JS bundle weight.
146
- injectScript(
147
- "page-ssr",
148
- `import "${CONSUMER_GLOBAL_CSS_VIRTUAL_ID}";`,
149
- );
134
+ injectScript('page-ssr', `import "${CONSUMER_GLOBAL_CSS_VIRTUAL_ID}";`);
150
135
  },
151
136
  },
152
137
  };
@@ -1 +1 @@
1
- export * from "@grove-dev/core";
1
+ export * from '@grove-dev/core';
package/src/lib/format.ts CHANGED
@@ -1 +1 @@
1
- export * from "@grove-dev/core";
1
+ export * from '@grove-dev/core';
@@ -10,50 +10,50 @@
10
10
  * Keyed by `${folder}/${resolvedName}` — the folder matters because
11
11
  * `apple` exists under both `stacks/` and `platforms/`.
12
12
  */
13
- export type IconKind = "color" | "mono";
13
+ export type IconKind = 'color' | 'mono';
14
14
 
15
15
  export const ICON_KINDS: Readonly<Record<string, IconKind>> = {
16
- "platforms/android": "color",
17
- "platforms/app": "mono",
18
- "platforms/chrome": "color",
19
- "platforms/desktop": "mono",
20
- "platforms/embedded": "mono",
21
- "platforms/ios": "mono",
22
- "platforms/linux": "color",
23
- "platforms/macos": "mono",
24
- "platforms/ubuntu": "color",
25
- "platforms/web": "mono",
26
- "platforms/windows": "color",
27
- "stacks/android": "color",
28
- "stacks/apple": "mono",
29
- "stacks/bun": "color",
30
- "stacks/capacitor": "color",
31
- "stacks/clojure": "color",
32
- "stacks/dart": "color",
33
- "stacks/deno": "mono",
34
- "stacks/django": "color",
35
- "stacks/docker": "color",
36
- "stacks/firebase": "color",
37
- "stacks/flutter": "color",
38
- "stacks/go": "color",
39
- "stacks/graphql": "color",
40
- "stacks/ionic": "color",
41
- "stacks/java": "color",
42
- "stacks/javascript": "color",
43
- "stacks/kotlin": "color",
44
- "stacks/llm": "mono",
45
- "stacks/mongodb": "color",
46
- "stacks/nodejs": "color",
47
- "stacks/python": "color",
48
- "stacks/rag": "mono",
49
- "stacks/react": "color",
50
- "stacks/react-native": "color",
51
- "stacks/rust": "mono",
52
- "stacks/solidity": "mono",
53
- "stacks/sveltekit": "color",
54
- "stacks/swift": "color",
55
- "stacks/tauri": "mono",
56
- "stacks/tensorflow": "color",
57
- "stacks/typescript": "color",
58
- "stacks/vue": "color",
16
+ 'platforms/android': 'color',
17
+ 'platforms/app': 'mono',
18
+ 'platforms/chrome': 'color',
19
+ 'platforms/desktop': 'mono',
20
+ 'platforms/embedded': 'mono',
21
+ 'platforms/ios': 'mono',
22
+ 'platforms/linux': 'color',
23
+ 'platforms/macos': 'mono',
24
+ 'platforms/ubuntu': 'color',
25
+ 'platforms/web': 'mono',
26
+ 'platforms/windows': 'color',
27
+ 'stacks/android': 'color',
28
+ 'stacks/apple': 'mono',
29
+ 'stacks/bun': 'color',
30
+ 'stacks/capacitor': 'color',
31
+ 'stacks/clojure': 'color',
32
+ 'stacks/dart': 'color',
33
+ 'stacks/deno': 'mono',
34
+ 'stacks/django': 'color',
35
+ 'stacks/docker': 'color',
36
+ 'stacks/firebase': 'color',
37
+ 'stacks/flutter': 'color',
38
+ 'stacks/go': 'color',
39
+ 'stacks/graphql': 'color',
40
+ 'stacks/ionic': 'color',
41
+ 'stacks/java': 'color',
42
+ 'stacks/javascript': 'color',
43
+ 'stacks/kotlin': 'color',
44
+ 'stacks/llm': 'mono',
45
+ 'stacks/mongodb': 'color',
46
+ 'stacks/nodejs': 'color',
47
+ 'stacks/python': 'color',
48
+ 'stacks/rag': 'mono',
49
+ 'stacks/react': 'color',
50
+ 'stacks/react-native': 'color',
51
+ 'stacks/rust': 'mono',
52
+ 'stacks/solidity': 'mono',
53
+ 'stacks/sveltekit': 'color',
54
+ 'stacks/swift': 'color',
55
+ 'stacks/tauri': 'mono',
56
+ 'stacks/tensorflow': 'color',
57
+ 'stacks/typescript': 'color',
58
+ 'stacks/vue': 'color',
59
59
  };
@@ -7,8 +7,8 @@
7
7
  * (and drifting from) the alias and folder rules.
8
8
  */
9
9
 
10
- export type IconCategory = "stack" | "platform" | "brand";
11
- export type IconFolder = "stacks" | "platforms" | "brands";
10
+ export type IconCategory = 'stack' | 'platform' | 'brand';
11
+ export type IconFolder = 'stacks' | 'platforms' | 'brands';
12
12
 
13
13
  /**
14
14
  * Normalize: lowercase + replace spaces/underscores with dashes.
@@ -21,7 +21,7 @@ export function slugifyIconName(s: string): string {
21
21
  return s
22
22
  .toLowerCase()
23
23
  .trim()
24
- .replace(/[\s_]+/g, "-");
24
+ .replace(/[\s_]+/g, '-');
25
25
  }
26
26
 
27
27
  /**
@@ -31,28 +31,28 @@ export function slugifyIconName(s: string): string {
31
31
  */
32
32
  const defaultAliases: Partial<Record<IconCategory, Record<string, string>>> = {
33
33
  stack: {
34
- ios: "apple",
35
- "native-ios": "apple",
36
- ipados: "apple",
37
- macos: "apple",
38
- "objective-c": "apple",
39
- spritekit: "apple",
40
- swiftui: "apple",
41
- kmp: "kotlin",
42
- "kotlin-multiplatform": "kotlin",
34
+ ios: 'apple',
35
+ 'native-ios': 'apple',
36
+ ipados: 'apple',
37
+ macos: 'apple',
38
+ 'objective-c': 'apple',
39
+ spritekit: 'apple',
40
+ swiftui: 'apple',
41
+ kmp: 'kotlin',
42
+ 'kotlin-multiplatform': 'kotlin',
43
43
  // Names the shipped set covers under a different file.
44
- "node.js": "nodejs",
45
- node: "nodejs",
46
- clojurescript: "clojure",
47
- svelte: "sveltekit",
44
+ 'node.js': 'nodejs',
45
+ node: 'nodejs',
46
+ clojurescript: 'clojure',
47
+ svelte: 'sveltekit',
48
48
  },
49
49
  };
50
50
 
51
51
  /** Stack marks that `category="platform"` should reuse rather than duplicate. */
52
- const platformStackIcons = new Set(["docker"]);
52
+ const platformStackIcons = new Set(['docker']);
53
53
 
54
54
  /** Platforms with no mark at all — always render the initials chip. */
55
- export const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
55
+ export const textOnlyPlatforms = new Set(['self-host', 'self-hosted']);
56
56
 
57
57
  export interface ResolvedIconAsset {
58
58
  /** Subfolder under the icon base path. */
@@ -80,17 +80,17 @@ export function resolveIconAsset(
80
80
  ): ResolvedIconAsset {
81
81
  const resolved = resolveName(name, category, aliases);
82
82
  const folder: IconFolder =
83
- category === "stack" || (category === "platform" && platformStackIcons.has(resolved))
84
- ? "stacks"
85
- : category === "platform"
86
- ? "platforms"
87
- : "brands";
83
+ category === 'stack' || (category === 'platform' && platformStackIcons.has(resolved))
84
+ ? 'stacks'
85
+ : category === 'platform'
86
+ ? 'platforms'
87
+ : 'brands';
88
88
 
89
89
  return {
90
90
  folder,
91
91
  name: resolved,
92
92
  key: `${folder}/${resolved}`,
93
- textOnly: category === "platform" && textOnlyPlatforms.has(resolved),
93
+ textOnly: category === 'platform' && textOnlyPlatforms.has(resolved),
94
94
  };
95
95
  }
96
96
 
@@ -99,7 +99,7 @@ function resolveName(
99
99
  category: IconCategory,
100
100
  aliases?: Record<string, string>,
101
101
  ): string {
102
- if (!raw) return "";
102
+ if (!raw) return '';
103
103
  for (const registry of [aliases, defaultAliases[category]]) {
104
104
  if (!registry) continue;
105
105
  const lower = raw.toLowerCase();
package/src/lib/index.ts CHANGED
@@ -17,12 +17,12 @@
17
17
  * payload and the `Score` / `HealthStatus` unions).
18
18
  */
19
19
 
20
- export * from "./repo.js";
21
- export * from "./format.js";
22
- export * from "./lenses.js";
23
- export * from "./search.js";
24
- export * from "./taxonomy-counts.js";
25
- export * from "./display.js";
26
- export * from "./icon-registry.js";
27
- export * from "./icon-kinds.js";
28
- export * from "./packaged-icons.js";
20
+ export * from './display.js';
21
+ export * from './format.js';
22
+ export * from './icon-kinds.js';
23
+ export * from './icon-registry.js';
24
+ export * from './lenses.js';
25
+ export * from './packaged-icons.js';
26
+ export * from './repo.js';
27
+ export * from './search.js';
28
+ export * from './taxonomy-counts.js';
package/src/lib/lenses.ts CHANGED
@@ -1 +1 @@
1
- export * from "@grove-dev/core";
1
+ export * from '@grove-dev/core';
@@ -6,10 +6,10 @@
6
6
  * carries the matching artwork with it, so a consumer can never end up
7
7
  * with a component asking for files their site does not have.
8
8
  */
9
- import { dirname, resolve } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import type { IconSyncOptions, IconSyncResult } from "@grove-dev/core";
12
- import { syncIconAssets } from "@grove-dev/core";
9
+ import { dirname, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import type { IconSyncOptions, IconSyncResult } from '@grove-dev/core';
12
+ import { syncIconAssets } from '@grove-dev/core';
13
13
 
14
14
  /**
15
15
  * `packages/astro/assets/icons` — two levels up, which holds for both
@@ -17,10 +17,10 @@ import { syncIconAssets } from "@grove-dev/core";
17
17
  */
18
18
  export const packagedIconsDir = resolve(
19
19
  dirname(fileURLToPath(import.meta.url)),
20
- "..",
21
- "..",
22
- "assets",
23
- "icons",
20
+ '..',
21
+ '..',
22
+ 'assets',
23
+ 'icons',
24
24
  );
25
25
 
26
26
  /** Copy the packaged icon set into a site's `public/`. */
package/src/lib/repo.ts CHANGED
@@ -1 +1 @@
1
- export * from "@grove-dev/core";
1
+ export * from '@grove-dev/core';
package/src/lib/search.ts CHANGED
@@ -1 +1 @@
1
- export * from "@grove-dev/core";
1
+ export * from '@grove-dev/core';
@@ -1 +1 @@
1
- export * from "@grove-dev/core";
1
+ export * from '@grove-dev/core';
@@ -1,10 +1,10 @@
1
- import type { Collection, CollectionEntry } from "@grove-dev/core";
2
- import { collectionSchema, findRelated, runCollection } from "@grove-dev/core";
3
- import { absoluteUrl, ogPath, type PageSeo, seoDescription, seoTitle } from "./seo.js";
1
+ import type { Collection, CollectionEntry } from '@grove-dev/core';
2
+ import { collectionSchema, findRelated, runCollection } from '@grove-dev/core';
3
+ import { absoluteUrl, ogPath, type PageSeo, seoDescription, seoTitle } from './seo.js';
4
4
 
5
5
  // Collection YAML loading lives in @grove-dev/core (it also feeds the
6
6
  // sitemap and OG-image pipelines there); re-exported for page code.
7
- export { loadCollections } from "@grove-dev/core";
7
+ export { loadCollections } from '@grove-dev/core';
8
8
 
9
9
  interface RouteHint {
10
10
  routeSlug?: string;
@@ -65,19 +65,18 @@ export function recordsToCollectionEntries(
65
65
  records: RawRecord[],
66
66
  site: RouteHint,
67
67
  ): CollectionEntry[] {
68
- const routeSlug = site.routeSlug ?? site.blueprintConfig?.routeSlug ?? "projects";
68
+ const routeSlug = site.routeSlug ?? site.blueprintConfig?.routeSlug ?? 'projects';
69
69
  const out: CollectionEntry[] = [];
70
70
  for (const r of records) {
71
- if (r.visibility === "hide") continue;
71
+ if (r.visibility === 'hide') continue;
72
72
  if (!r.slug) continue;
73
- const categories = [
74
- ...(r.category ? [r.category] : []),
75
- ...(r.tags ?? []),
76
- ].filter((value, index, self) => self.indexOf(value) === index);
73
+ const categories = [...(r.category ? [r.category] : []), ...(r.tags ?? [])].filter(
74
+ (value, index, self) => self.indexOf(value) === index,
75
+ );
77
76
  out.push({
78
77
  slug: r.slug,
79
78
  title: r.name ?? r.title ?? r.slug,
80
- description: r.description ?? "",
79
+ description: r.description ?? '',
81
80
  url: `/${routeSlug}/${r.slug}/`,
82
81
  repoHref: r.repoUrl ?? r.links?.github,
83
82
  homepageHref: r.links?.website,
@@ -89,11 +88,7 @@ export function recordsToCollectionEntries(
89
88
  // If only `license` (singular, GitHub-synced) is present, mirror
90
89
  // it into the array so the filter still matches it.
91
90
  licenses:
92
- r.licenses && r.licenses.length > 0
93
- ? r.licenses
94
- : r.license
95
- ? [r.license]
96
- : undefined,
91
+ r.licenses && r.licenses.length > 0 ? r.licenses : r.license ? [r.license] : undefined,
97
92
  status: r.visibility,
98
93
  stars: r.stars ?? r.github?.repository?.stargazers_count,
99
94
  forks: r.forks ?? r.github?.repository?.forks_count,
@@ -118,7 +113,7 @@ export interface CollectionPageModel {
118
113
  slug: string;
119
114
  title: string;
120
115
  description: string;
121
- kind: "curated" | "generated";
116
+ kind: 'curated' | 'generated';
122
117
  selectionNote?: string;
123
118
  introduction?: string;
124
119
  };
@@ -140,7 +135,7 @@ export interface CollectionIndexModel {
140
135
  slug: string;
141
136
  title: string;
142
137
  description: string;
143
- kind: "curated" | "generated";
138
+ kind: 'curated' | 'generated';
144
139
  count: number;
145
140
  url: string;
146
141
  }>;
@@ -151,14 +146,19 @@ export interface CollectionIndexModel {
151
146
 
152
147
  export interface CollectionTeaserModel {
153
148
  total: number;
154
- collections: CollectionIndexModel["collections"];
149
+ collections: CollectionIndexModel['collections'];
155
150
  }
156
151
 
157
152
  export function getCollectionPageModel(
158
153
  collection: Collection,
159
154
  entries: CollectionEntry[],
160
155
  allCollections: Collection[],
161
- site?: { name?: string; url?: string; siteUrl?: string; blueprintConfig?: { labelPlural?: string } },
156
+ site?: {
157
+ name?: string;
158
+ url?: string;
159
+ siteUrl?: string;
160
+ blueprintConfig?: { labelPlural?: string };
161
+ },
162
162
  ): CollectionPageModel {
163
163
  const result = runCollection(collection, entries);
164
164
  const related = findRelated(collection, allCollections, 4).map((c) => ({
@@ -166,9 +166,9 @@ export function getCollectionPageModel(
166
166
  title: c.title,
167
167
  url: `/collections/${c.slug}/`,
168
168
  }));
169
- const siteUrl = (site?.siteUrl ?? site?.url ?? "https://example.com").replace(/\/$/, "");
170
- const siteName = site?.name ?? "";
171
- const plural = site?.blueprintConfig?.labelPlural ?? "items";
169
+ const siteUrl = (site?.siteUrl ?? site?.url ?? 'https://example.com').replace(/\/$/, '');
170
+ const siteName = site?.name ?? '';
171
+ const plural = site?.blueprintConfig?.labelPlural ?? 'items';
172
172
  const pageUrl = absoluteUrl(siteUrl, `collections/${collection.slug}/`);
173
173
  // CollectionPage + ItemList + BreadcrumbList. The ItemList is capped
174
174
  // at 50 entries to keep the payload bounded; search engines don't
@@ -178,13 +178,13 @@ export function getCollectionPageModel(
178
178
  name: collection.seo?.title ?? collection.title,
179
179
  description: collection.seo?.description ?? collection.description,
180
180
  items: result.entries.slice(0, 50).map((entry) => ({
181
- url: entry.url.startsWith("http") ? entry.url : absoluteUrl(siteUrl, entry.url),
181
+ url: entry.url.startsWith('http') ? entry.url : absoluteUrl(siteUrl, entry.url),
182
182
  name: entry.title,
183
183
  ...(entry.description ? { description: entry.description } : {}),
184
184
  })),
185
185
  crumbs: [
186
- { url: `${siteUrl}/`, name: "Home" },
187
- { url: absoluteUrl(siteUrl, "collections/"), name: "Collections" },
186
+ { url: `${siteUrl}/`, name: 'Home' },
187
+ { url: absoluteUrl(siteUrl, 'collections/'), name: 'Collections' },
188
188
  { url: pageUrl, name: collection.title },
189
189
  ],
190
190
  });
@@ -195,11 +195,8 @@ export function getCollectionPageModel(
195
195
  title:
196
196
  collection.seo?.title ??
197
197
  seoTitle(`${collection.title} — ${result.entries.length} ${plural}`, siteName),
198
- description: seoDescription(
199
- collection.seo?.description,
200
- collection.description,
201
- ),
202
- image: ogPath("collection", collection.slug),
198
+ description: seoDescription(collection.seo?.description, collection.description),
199
+ image: ogPath('collection', collection.slug),
203
200
  ...(siteName ? { imageAlt: `${collection.title} — ${siteName}` } : {}),
204
201
  jsonLd: jsonLd as unknown as Record<string, unknown>[],
205
202
  noindex: collection.seo?.index === false,
@@ -225,7 +222,12 @@ export function getCollectionPageModel(
225
222
  export function getCollectionIndexModel(
226
223
  collections: Collection[],
227
224
  entries: CollectionEntry[],
228
- site?: { name?: string; url?: string; siteUrl?: string; blueprintConfig?: { labelPlural?: string } },
225
+ site?: {
226
+ name?: string;
227
+ url?: string;
228
+ siteUrl?: string;
229
+ blueprintConfig?: { labelPlural?: string };
230
+ },
229
231
  ): CollectionIndexModel {
230
232
  const rows = collections.map((c) => {
231
233
  const result = runCollection(c, entries);
@@ -240,20 +242,20 @@ export function getCollectionIndexModel(
240
242
  });
241
243
  let seo: PageSeo | undefined;
242
244
  if (site) {
243
- const siteUrl = (site.siteUrl ?? site.url ?? "https://example.com").replace(/\/$/, "");
244
- const siteName = site.name ?? "";
245
- const plural = site.blueprintConfig?.labelPlural ?? "items";
246
- const title = seoTitle("Collections", siteName);
245
+ const siteUrl = (site.siteUrl ?? site.url ?? 'https://example.com').replace(/\/$/, '');
246
+ const siteName = site.name ?? '';
247
+ const plural = site.blueprintConfig?.labelPlural ?? 'items';
248
+ const title = seoTitle('Collections', siteName);
247
249
  const description = seoDescription(
248
250
  undefined,
249
- `${rows.length} curated and generated collections of ${plural} on ${siteName || "this site"} — hand-picked lists kept in sync with the source files.`,
251
+ `${rows.length} curated and generated collections of ${plural} on ${siteName || 'this site'} — hand-picked lists kept in sync with the source files.`,
250
252
  );
251
253
  seo = {
252
254
  title,
253
255
  description,
254
- image: ogPath("default"),
256
+ image: ogPath('default'),
255
257
  jsonLd: collectionSchema({
256
- url: absoluteUrl(siteUrl, "collections/"),
258
+ url: absoluteUrl(siteUrl, 'collections/'),
257
259
  name: title,
258
260
  description,
259
261
  items: rows.map((row) => ({
@@ -262,8 +264,8 @@ export function getCollectionIndexModel(
262
264
  ...(row.description ? { description: row.description } : {}),
263
265
  })),
264
266
  crumbs: [
265
- { url: `${siteUrl}/`, name: "Home" },
266
- { url: absoluteUrl(siteUrl, "collections/"), name: "Collections" },
267
+ { url: `${siteUrl}/`, name: 'Home' },
268
+ { url: absoluteUrl(siteUrl, 'collections/'), name: 'Collections' },
267
269
  ],
268
270
  }) as unknown as Record<string, unknown>[],
269
271
  };
@@ -300,7 +302,16 @@ export function getCollectionTeaserModel(
300
302
  * label without re-loading the collection YAML.
301
303
  */
302
304
  export function findCollectionsFor(
303
- target: { slug?: string; stack?: string; stacks?: string[]; platforms?: string[]; licenses?: string[]; category?: string; visibility?: string; status?: string },
305
+ target: {
306
+ slug?: string;
307
+ stack?: string;
308
+ stacks?: string[];
309
+ platforms?: string[];
310
+ licenses?: string[];
311
+ category?: string;
312
+ visibility?: string;
313
+ status?: string;
314
+ },
304
315
  collections: Collection[],
305
316
  entries: CollectionEntry[],
306
317
  ): { slug: string; title: string; url: string }[] {