@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
@@ -1,231 +0,0 @@
1
- ---
2
- /**
3
- * Footer — 4-column grid (brand + Discover + Contribute + Project) plus
4
- * a thin bar at the bottom with copyright + repo link.
5
- *
6
- * Generic: takes a `site` prop with brand + repoUrl + stats, plus
7
- * pre-computed column data. No `siteConfig` import — consumers
8
- * compose props from their own config (or from
9
- * `data/generated/site-config.json` for the showcase).
10
- *
11
- * Each column's links are passed in as a `{ heading, items[] }` shape
12
- * so consumers can customize the order and copy. Defaults are
13
- * provided so the footer still renders if no extra data is supplied.
14
- */
15
- import PoweredBy from "../components/PoweredBy.astro";
16
-
17
- interface NavItem {
18
- label: string;
19
- href: string;
20
- /** When true, opens in a new tab (external). */
21
- external?: boolean;
22
- }
23
-
24
- interface FooterColumn {
25
- heading: string;
26
- items: NavItem[];
27
- }
28
-
29
- interface Site {
30
- name: string;
31
- tagline?: string;
32
- description?: string;
33
- repoUrl?: string;
34
- footer?: {
35
- columns?: FooterColumn[];
36
- copyright?: string;
37
- license?: string;
38
- /** "Powered by Grove" under the brand block. Defaults to on. */
39
- poweredBy?: boolean;
40
- };
41
- stats?: {
42
- totalApps?: number;
43
- totalCategories?: number;
44
- totalStacks?: number;
45
- totalOwners?: number;
46
- };
47
- }
48
-
49
- interface Props {
50
- site: Site;
51
- /** Discover links — first content column. */
52
- discover?: FooterColumn;
53
- /** Contribute links — second content column. */
54
- contribute?: FooterColumn;
55
- /** Project links — third content column. */
56
- project?: FooterColumn;
57
- /** Stats line under brand blurb. */
58
- stats?: { totalApps?: number; totalCategories?: number; totalOwners?: number };
59
- /**
60
- * Plural label for the primary count stat. Default "items" so the
61
- * footer reads naturally regardless of blueprint.
62
- */
63
- itemsLabel?: string;
64
- /** Copyright suffix. Defaults to site.name. */
65
- copyrightHolder?: string;
66
- /** License line under the bar (e.g. "Content under CC BY-SA 4.0."). */
67
- licenseLine?: string;
68
- /**
69
- * "Powered by Grove" under the brand block. Falls back to
70
- * `site.footer.poweredBy`, which defaults to on.
71
- */
72
- poweredBy?: boolean;
73
- /** Extra class for the outer footer element. */
74
- class?: string;
75
- }
76
-
77
- const {
78
- site,
79
- discover = { heading: "Discover", items: [] },
80
- contribute = { heading: "Contribute", items: [] },
81
- project = { heading: "Project", items: [] },
82
- stats,
83
- itemsLabel = "items",
84
- copyrightHolder,
85
- licenseLine,
86
- poweredBy,
87
- class: className = "",
88
- } = Astro.props;
89
-
90
- const showPoweredBy = poweredBy ?? site.footer?.poweredBy ?? true;
91
-
92
- const year = new Date().getFullYear();
93
- const holder = copyrightHolder ?? site.footer?.copyright ?? site.name;
94
- const gh = site.repoUrl;
95
- const showOwners = (stats?.totalOwners ?? 0) > 0;
96
-
97
- const defaultDiscover: NavItem[] = gh
98
- ? [
99
- { label: "Source on GitHub", href: gh, external: true },
100
- { label: "Latest release", href: `${gh}/releases`, external: true },
101
- ]
102
- : [];
103
- const defaultContribute: NavItem[] = gh
104
- ? [
105
- { label: "Submit via GitHub", href: `${gh}/blob/main/CONTRIBUTING.md`, external: true },
106
- { label: "Report an issue", href: `${gh}/issues`, external: true },
107
- { label: "Contributing guide", href: `${gh}/blob/main/CONTRIBUTING.md`, external: true },
108
- ]
109
- : [];
110
- const defaultProject: NavItem[] = gh
111
- ? [
112
- { label: "README", href: `${gh}/blob/main/README.md`, external: true },
113
- { label: "Changelog", href: `${gh}/blob/main/CHANGELOG.md`, external: true },
114
- ]
115
- : [];
116
-
117
- const discoverItems = discover.items.length ? discover.items : defaultDiscover;
118
- const contributeItems = contribute.items.length ? contribute.items : defaultContribute;
119
- const projectItems = project.items.length ? project.items : defaultProject;
120
- const configuredColumns = site.footer?.columns ?? [];
121
- const columns = configuredColumns.length
122
- ? configuredColumns
123
- : [
124
- { heading: discover.heading, items: discoverItems },
125
- { heading: contribute.heading, items: contributeItems },
126
- { heading: project.heading, items: projectItems },
127
- ].filter((column) => column.items.length > 0);
128
- const resolvedLicenseLine = licenseLine ?? site.footer?.license;
129
- ---
130
-
131
- <footer
132
- class:list={[
133
- "border-border bg-background mt-16 border-t text-ink-500 dark:text-ink-400",
134
- className,
135
- ]}
136
- >
137
- <div
138
- class="mx-auto grid w-full max-w-container grid-cols-2 gap-8 px-5 py-12 md:grid-cols-4 sm:px-7"
139
- >
140
- <div class="col-span-2 md:col-span-1">
141
- <div
142
- class="mb-2 inline-flex items-center gap-2 text-[0.95rem] font-semibold text-ink-900 dark:text-ink-100"
143
- >
144
- <span
145
- aria-hidden="true"
146
- class="inline-block h-3.5 w-3.5 rounded bg-ink-900 dark:bg-ink-100"
147
- ></span>
148
- <span>{site.name}</span>
149
- </div>
150
- {site.tagline && (
151
- <p class="m-0 max-w-[36ch] text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
152
- {site.tagline}
153
- </p>
154
- )}
155
- {stats && (
156
- <p class="mt-3 m-0 text-2xs text-ink-500 dark:text-ink-400">
157
- {stats.totalApps !== undefined && (
158
- <>
159
- <span class="font-mono">{stats.totalApps}</span> {itemsLabel}
160
- </>
161
- )}
162
- {stats.totalApps !== undefined && (stats.totalCategories !== undefined || showOwners) && (
163
- <span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
164
- )}
165
- {stats.totalCategories !== undefined && (
166
- <>
167
- <span class="font-mono">{stats.totalCategories}</span> categories
168
- </>
169
- )}
170
- {stats.totalCategories !== undefined && showOwners && (
171
- <span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
172
- )}
173
- {showOwners && (
174
- <>
175
- <span class="font-mono">{stats.totalOwners}</span> project owners
176
- </>
177
- )}
178
- </p>
179
- )}
180
- {showPoweredBy && (
181
- <p class="mt-4 m-0 text-[0.8125rem] text-ink-500 dark:text-ink-400">
182
- <PoweredBy size={16} />
183
- </p>
184
- )}
185
- </div>
186
-
187
- {columns.map((column) => (
188
- <div>
189
- <h2 class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100">
190
- {column.heading}
191
- </h2>
192
- <ul class="m-0 flex list-none flex-col gap-2 p-0">
193
- {column.items.map((item) => (
194
- <li>
195
- <a
196
- href={item.href}
197
- class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
198
- {...(item.external
199
- ? { target: "_blank", rel: "noopener noreferrer" }
200
- : {})}
201
- >
202
- {item.label}
203
- </a>
204
- </li>
205
- ))}
206
- </ul>
207
- </div>
208
- ))}
209
- </div>
210
-
211
- <div class="border-t border-ink-200 dark:border-ink-800">
212
- <div
213
- class="mx-auto flex w-full max-w-container flex-wrap items-center gap-3 px-5 py-4 text-2xs text-ink-500 sm:px-7 dark:text-ink-400"
214
- >
215
- <span>© {year} {holder}{resolvedLicenseLine ? `. ${resolvedLicenseLine}` : "."}</span>
216
- {gh && (
217
- <>
218
- <span class="text-ink-300 dark:text-ink-700">·</span>
219
- <a
220
- href={gh}
221
- target="_blank"
222
- rel="noopener noreferrer"
223
- class="transition-colors hover:text-ink-900 dark:hover:text-ink-100"
224
- >
225
- View source on GitHub
226
- </a>
227
- </>
228
- )}
229
- </div>
230
- </div>
231
- </footer>
@@ -1,214 +0,0 @@
1
- ---
2
- /**
3
- * Header — sticky brand + nav + theme toggle.
4
- *
5
- * Generic: takes a `site` prop with brand + nav. No `siteConfig`
6
- * import — consumers compose the props from their own config.
7
- *
8
- * Optional `submit` button renders a "Submit app" / "Submit project"
9
- * call-to-action on the right side of the nav. Set `submitHref` /
10
- * `submitLabel` to enable, leave them undefined for a clean header.
11
- *
12
- * Optional `repoUrl` adds a "View source" GitHub button. Set
13
- * `repoLabel` to change the visible text.
14
- */
15
-
16
- import ThemeToggle from "./ThemeToggle.astro";
17
- import { buttonClass } from "../ui/button.js";
18
-
19
- interface NavItem {
20
- label: string;
21
- href: string;
22
- }
23
-
24
- interface Site {
25
- name: string;
26
- tagline?: string;
27
- nav: NavItem[];
28
- description?: string;
29
- repoUrl?: string;
30
- url?: string;
31
- /** Logo path under `public/` (e.g. `/logo.svg`). */
32
- logo?: string;
33
- stats?: {
34
- /** Stars on this site's own source repository. */
35
- repositoryStars?: number;
36
- };
37
- blueprintConfig?: {
38
- routeSlug?: string;
39
- };
40
- }
41
-
42
- interface Props {
43
- site: Site;
44
- /** Submit button label, e.g. "Submit app". Omit to hide. */
45
- submitLabel?: string;
46
- /** Submit button href, e.g. "/submit". Omit to hide. */
47
- submitHref?: string;
48
- /** GitHub button label, e.g. "View source". */
49
- repoLabel?: string;
50
- /** Extra class for the outer header element. */
51
- class?: string;
52
- }
53
-
54
- const {
55
- site,
56
- submitLabel,
57
- submitHref,
58
- repoLabel = "Star",
59
- class: className = "",
60
- } = Astro.props;
61
- const directoryPath = `/${site.blueprintConfig?.routeSlug ?? "items"}`;
62
- const defaultNavItems = site.blueprintConfig
63
- ? [
64
- { label: "Recently added", href: `${directoryPath}?sort=recently-added` },
65
- { label: "Trending", href: `${directoryPath}?label=hot` },
66
- { label: "Categories", href: "/categories" },
67
- { label: "Stacks", href: "/stacks" },
68
- { label: "About", href: "/about" },
69
- ]
70
- : [];
71
- const navItems = site.nav?.length ? site.nav : defaultNavItems;
72
- const stars = site.stats?.repositoryStars;
73
- const starsLabel =
74
- typeof stars === "number" && stars >= 1000
75
- ? `${(stars / 1000).toFixed(stars >= 10000 ? 0 : 1)}k`
76
- : typeof stars === "number" ? String(stars) : null;
77
- ---
78
-
79
- <header
80
- class:list={[
81
- "border-border bg-background/85 sticky top-0 z-50 border-b backdrop-blur",
82
- className,
83
- ]}
84
- >
85
- <div
86
- class="mx-auto flex w-full max-w-container items-center gap-3 px-5 py-3 sm:gap-6 sm:px-7"
87
- >
88
- <a
89
- href="/"
90
- class="inline-flex min-w-0 shrink items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
91
- >
92
- {site.logo ? (
93
- <img
94
- src={site.logo}
95
- alt=""
96
- aria-hidden="true"
97
- width="18"
98
- height="18"
99
- class="inline-block h-[18px] w-[18px] shrink-0 object-contain"
100
- />
101
- ) : (
102
- /* No logo configured — a neutral mark is the honest default.
103
- Set `site.logo` in grove.config.ts to replace it. */
104
- <span
105
- aria-hidden="true"
106
- class="inline-block h-[18px] w-[18px] rounded bg-ink-900 dark:bg-ink-100"
107
- ></span>
108
- )}
109
- <span class="truncate">{site.name}</span>
110
- </a>
111
-
112
- <nav class="hidden flex-1 md:block" aria-label="Primary">
113
- <ul class="m-0 flex list-none gap-5 p-0">
114
- {navItems.map((item) => (
115
- <li>
116
- <a
117
- href={item.href}
118
- class="text-[0.875rem] font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
119
- >
120
- {item.label}
121
- </a>
122
- </li>
123
- ))}
124
- </ul>
125
- </nav>
126
-
127
- <div class="ml-auto flex items-center gap-2">
128
- <details class="group relative md:hidden">
129
- <summary class="inline-flex h-9 cursor-pointer list-none items-center justify-center rounded-[var(--radius)] border border-border bg-secondary px-2.5 text-sm font-medium text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/35">
130
- Menu
131
- </summary>
132
- <nav aria-label="Mobile primary" class="absolute right-0 top-[calc(100%+0.5rem)] z-50 w-56 rounded-[var(--radius)] border border-border bg-background p-2 shadow-lg">
133
- <ul class="m-0 flex list-none flex-col gap-1 p-0">
134
- {navItems.map((item) => (
135
- <li><a href={item.href} class="block rounded-md px-3 py-2 text-sm font-medium text-foreground no-underline hover:bg-accent">{item.label}</a></li>
136
- ))}
137
- {submitHref && (
138
- <li class="border-t border-border pt-1"><a href={submitHref} class="block rounded-md px-3 py-2 text-sm font-medium text-foreground no-underline hover:bg-accent">{submitLabel ?? "Submit"}</a></li>
139
- )}
140
- </ul>
141
- </nav>
142
- </details>
143
-
144
- {submitHref && (
145
- <a
146
- href={submitHref}
147
- class={buttonClass("secondary", "sm", "max-sm:!hidden")}
148
- >
149
- <svg
150
- viewBox="0 0 16 16"
151
- width="13"
152
- height="13"
153
- aria-hidden="true"
154
- fill="currentColor"
155
- >
156
- <path d="M7.75 2a.75.75 0 0 1 .75.75v3.5h3.5a.75.75 0 0 1 0 1.5h-3.5v3.5a.75.75 0 0 1-1.5 0v-3.5h-3.5a.75.75 0 0 1 0-1.5h3.5v-3.5A.75.75 0 0 1 7.75 2Z"></path>
157
- </svg>
158
- <span>{submitLabel ?? "Submit"}</span>
159
- </a>
160
- )}
161
-
162
- {site.repoUrl && (
163
- <a
164
- href={site.repoUrl}
165
- target="_blank"
166
- rel="noopener noreferrer"
167
- aria-label={`${repoLabel} on GitHub${starsLabel ? ` — ${stars} stars` : ""}`}
168
- title={`${repoLabel} on GitHub`}
169
- class={buttonClass("secondary", "bare", "h-8 overflow-hidden p-0 text-sm")}
170
- >
171
- <span class="flex items-center gap-1.5 px-2.5 py-1.5 text-ink-900 dark:text-ink-100">
172
- <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="currentColor">
173
- <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"></path>
174
- </svg>
175
- <span class="hidden lg:inline">{repoLabel}</span>
176
- </span>
177
- {starsLabel && <span class="hidden items-center gap-1 border-s border-border bg-secondary px-2 py-1.5 text-secondary-foreground sm:flex">
178
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
179
- <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path>
180
- </svg>
181
- <span class="font-mono">{starsLabel}</span>
182
- </span>}
183
- </a>
184
- )}
185
-
186
- <ThemeToggle />
187
- </div>
188
- </div>
189
- </header>
190
-
191
- <script>
192
- // Smooth-scroll for in-page anchor links (Header nav + Hero CTA).
193
- // Works on any <a href="#…"> that points to an existing element on
194
- // the current page. External links (e.g. /about, /projects?…) are
195
- // skipped because they don't start with "#". Modifier-clicks
196
- // (Cmd/Ctrl/Shift/middle) let the browser handle the anchor
197
- // normally so users can open the same section in a new tab.
198
- document.addEventListener("click", (e) => {
199
- if (e.defaultPrevented) return;
200
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
201
- if (e.button !== 0) return;
202
- const link = (e.target as HTMLElement).closest<HTMLAnchorElement>(
203
- 'a[href^="#"]',
204
- );
205
- if (!link) return;
206
- const id = link.getAttribute("href")!;
207
- if (id.length <= 1) return;
208
- const target = document.querySelector(id);
209
- if (!target) return;
210
- e.preventDefault();
211
- target.scrollIntoView({ behavior: "smooth", block: "start" });
212
- history.replaceState(null, "", id);
213
- });
214
- </script>
@@ -1,43 +0,0 @@
1
- ---
2
- interface Props {
3
- eyebrow?: string;
4
- title: string;
5
- description?: string;
6
- align?: "left" | "center";
7
- /** Pass 1 when this header IS the page title (one h1 per route). */
8
- level?: 1 | 2 | 3;
9
- }
10
-
11
- const { eyebrow, title, description, align = "left", level = 2 } = Astro.props;
12
- const Heading = `h${level}` as "h1" | "h2" | "h3";
13
- ---
14
-
15
- <header
16
- class={`mb-6 flex flex-col gap-2 ${
17
- align === "center" ? "items-center text-center" : ""
18
- }`}
19
- >
20
- {
21
- eyebrow && (
22
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
23
- {eyebrow}
24
- </span>
25
- )
26
- }
27
- <Heading
28
- class="text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
29
- >
30
- {title}
31
- </Heading>
32
- {
33
- description && (
34
- <p
35
- class={`text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 ${
36
- align === "center" ? "max-w-2xl" : "max-w-[56ch]"
37
- }`}
38
- >
39
- {description}
40
- </p>
41
- )
42
- }
43
- </header>
@@ -1,171 +0,0 @@
1
- ---
2
- // SPDX-License-Identifier: MIT
3
-
4
- /**
5
- * Per-page SEO: <title>, description, canonical, Open Graph, Twitter Card,
6
- * and JSON-LD structured data. Designed to be used inside <head> by the
7
- * BaseLayout. Pages pass the rich props they care about; everything else
8
- * has a sensible default derived from the site config and the Astro URL.
9
- *
10
- * Renders:
11
- * - <title>, meta description, canonical link, robots
12
- * - Open Graph (og:*) for Facebook, LinkedIn, Discord, Slack, etc.
13
- * - Twitter Card (twitter:*) for X / Twitter
14
- * - JSON-LD WebSite+Organization identity graph on every page
15
- * (built by @grove-dev/core's `siteSchema`, with a SearchAction
16
- * pointing at the directory's browse page)
17
- * - Per-page JSON-LD nodes when `jsonLd` is provided (SoftwareSourceCode
18
- * on record pages, CollectionPage/ItemList/BreadcrumbList on
19
- * collection and taxonomy pages, ...)
20
- *
21
- * Noindex is opt-in via the `noindex` prop. By default everything is
22
- * indexable; noindex targets today are /submit, /404, and /empty.
23
- */
24
- import { siteSchema, validateJsonLd, type JsonLdNode } from '@grove-dev/core';
25
-
26
- interface Props {
27
- title: string;
28
- description: string;
29
- site: {
30
- name: string;
31
- description?: string;
32
- repoUrl?: string;
33
- /** Header logo path under `public/` — used as Organization logo when set. */
34
- logo?: string;
35
- /** BCP-47 language tag (e.g. "en"). Drives og:locale + JSON-LD inLanguage. */
36
- locale?: string;
37
- /** Twitter/X handle (e.g. "@grove") — emitted as twitter:site. */
38
- twitter?: string;
39
- };
40
- searchPath?: string;
41
- /** Open Graph / Twitter image. Default: /og-image.svg. Should be absolute or root-relative. */
42
- image?: string;
43
- /** Alt text for the OG/Twitter image. Default: "<site name> social preview". */
44
- imageAlt?: string;
45
- /** og:type. Default: "website". Use "article" for blog posts / changelog. */
46
- type?: 'website' | 'article' | 'profile';
47
- /** Set true to add noindex,nofollow. */
48
- noindex?: boolean;
49
- /** Extra JSON-LD payload (object) — typically SoftwareSourceCode on item pages. */
50
- jsonLd?: Record<string, unknown> | Record<string, unknown>[];
51
- }
52
-
53
- const {
54
- title,
55
- description,
56
- site: siteConfig,
57
- searchPath = '/items',
58
- image = '/og-image.svg',
59
- imageAlt,
60
- type = 'website',
61
- noindex = false,
62
- jsonLd,
63
- } = Astro.props;
64
-
65
- // `Astro.site` is required for canonical / OG / Twitter / JSON-LD URLs to
66
- // resolve to the consumer's domain. Failing the build (rather than silently
67
- // emitting `https://example.com/...`) is the right call: every canonical,
68
- // OG, and JSON-LD URL would otherwise leak to a domain the consumer doesn't
69
- // own, which search engines either ignore or treat as canonical to the
70
- // wrong host.
71
- if (!Astro.site) {
72
- throw new Error(
73
- "Seo: `site` is not set on the Astro config. Add `site: 'https://your-domain'` to astro.config.mjs so canonical / Open Graph / Twitter / JSON-LD URLs resolve correctly.",
74
- );
75
- }
76
- const siteUrl = Astro.site;
77
- const canonical = new URL(Astro.url.pathname, siteUrl).toString();
78
- const ogImage = new URL(image, siteUrl).toString();
79
- const ogImageAlt = imageAlt ?? `${siteConfig.name} social preview`;
80
- const locale = siteConfig.locale ?? 'en';
81
-
82
- // Truncate description for OG/Twitter where longer is worse (160–200 chars).
83
- function truncate(s: string, max: number): string {
84
- if (s.length <= max) return s;
85
- return s.slice(0, max - 1).trimEnd() + '…';
86
- }
87
- const shortDescription = truncate(description, 200);
88
-
89
- // WebSite+Organization identity graph — site-wide signals that appear on
90
- // every page (Google uses them for site links and the knowledge panel).
91
- // Built by core's `siteSchema` so the @id graph shape is shared with the
92
- // rest of the JSON-LD registry; the SearchAction is appended here because
93
- // the browse path is an adapter concern, not a core one.
94
- const bareSiteUrl = siteUrl.toString().replace(/\/$/, '');
95
- const [siteLd] = siteSchema({
96
- url: bareSiteUrl,
97
- name: siteConfig.name,
98
- description: siteConfig.description ?? description,
99
- inLanguage: locale,
100
- orgName: siteConfig.name,
101
- orgUrl: bareSiteUrl,
102
- ...(siteConfig.logo ? { orgLogo: new URL(siteConfig.logo, siteUrl).toString() } : {}),
103
- ...(siteConfig.repoUrl ? { sameAs: [siteConfig.repoUrl] } : {}),
104
- });
105
- siteLd.potentialAction = {
106
- '@type': 'SearchAction',
107
- target: `${bareSiteUrl}${searchPath}?q={search_term_string}`,
108
- 'query-input': 'required name=search_term_string',
109
- };
110
-
111
- const perPageNodes = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : null;
112
-
113
- // Dev-only well-formedness check — surfaces malformed nodes (relative
114
- // URLs, missing @context, duplicate @ids) in the terminal during
115
- // `astro dev` without ever failing or slowing a production build.
116
- if (import.meta.env.DEV && perPageNodes) {
117
- const issues = validateJsonLd(perPageNodes as JsonLdNode[]);
118
- for (const issue of issues) {
119
- console.warn(`[grove seo] ${Astro.url.pathname}: ${issue.message}`);
120
- }
121
- }
122
-
123
- // Pre-stringify the JSON-LD payloads in the frontmatter so the
124
- // template body just interpolates the resulting strings. For SSR
125
- // this saves the JSON.stringify call on every render; for static
126
- // builds the cost is unchanged but the template reads cleaner.
127
- const siteLdJson = JSON.stringify(siteLd);
128
- const perPageLdJson = perPageNodes ? JSON.stringify(perPageNodes) : null;
129
- ---
130
-
131
- <!-- Primary meta -->
132
- <title>{title}</title>
133
- <meta name="description" content={description} />
134
- <link rel="canonical" href={canonical} />
135
- {noindex ? (
136
- <meta name="robots" content="noindex,nofollow" />
137
- ) : (
138
- <meta name="robots" content="index,follow,max-image-preview:large" />
139
- )}
140
-
141
- <!-- Open Graph -->
142
- <meta property="og:type" content={type} />
143
- <meta property="og:url" content={canonical} />
144
- <meta property="og:title" content={title} />
145
- <meta property="og:description" content={shortDescription} />
146
- <meta property="og:image" content={ogImage} />
147
- <meta property="og:image:width" content="1200" />
148
- <meta property="og:image:height" content="630" />
149
- <meta property="og:image:alt" content={ogImageAlt} />
150
- <meta property="og:site_name" content={siteConfig.name} />
151
- <meta property="og:locale" content={locale} />
152
-
153
- <!-- Twitter Card -->
154
- <meta name="twitter:card" content="summary_large_image" />
155
- {siteConfig.twitter && <meta name="twitter:site" content={siteConfig.twitter} />}
156
- <meta name="twitter:title" content={title} />
157
- <meta name="twitter:description" content={shortDescription} />
158
- <meta name="twitter:image" content={ogImage} />
159
- <meta name="twitter:image:alt" content={ogImageAlt} />
160
-
161
- <!-- JSON-LD: site-wide identity graph -->
162
- <script is:inline type="application/ld+json" set:html={siteLdJson} />
163
-
164
- <!-- JSON-LD: per-page (SoftwareSourceCode, CollectionPage, BreadcrumbList, ...) -->
165
- {perPageLdJson && (
166
- <script
167
- is:inline
168
- type="application/ld+json"
169
- set:html={perPageLdJson}
170
- />
171
- )}