@grove-dev/astro 0.7.0 → 0.9.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.
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -1
- package/dist/lib/display.js +1 -1
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/icon-kinds.d.ts +1 -1
- package/dist/lib/icon-kinds.js +43 -43
- package/dist/lib/icon-registry.d.ts +2 -2
- package/dist/lib/icon-registry.js +23 -23
- package/dist/lib/index.d.ts +9 -9
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +9 -9
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -1
- package/dist/lib/lenses.js +1 -1
- package/dist/lib/packaged-icons.d.ts +1 -1
- package/dist/lib/packaged-icons.js +4 -4
- package/dist/lib/repo.d.ts +1 -1
- package/dist/lib/repo.js +1 -1
- package/dist/lib/search.d.ts +1 -1
- package/dist/lib/search.js +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -1
- package/dist/lib/taxonomy-counts.js +1 -1
- package/package.json +7 -17
- package/src/index.ts +23 -38
- package/src/lib/display.ts +1 -1
- package/src/lib/format.ts +1 -1
- package/src/lib/icon-kinds.ts +44 -44
- package/src/lib/icon-registry.ts +25 -25
- package/src/lib/index.ts +9 -9
- package/src/lib/lenses.ts +1 -1
- package/src/lib/packaged-icons.ts +8 -8
- package/src/lib/repo.ts +1 -1
- package/src/lib/search.ts +1 -1
- package/src/lib/taxonomy-counts.ts +1 -1
- package/src/server/collections.ts +53 -42
- package/src/server/contrast.ts +14 -9
- package/src/server/directory.ts +22 -17
- package/src/server/index.ts +3 -2
- package/src/server/models.ts +425 -218
- package/src/server/seo.ts +34 -31
- package/src/styles.css +235 -91
- package/dist/layouts/index.d.ts +0 -25
- package/dist/layouts/index.d.ts.map +0 -1
- package/dist/layouts/index.js +0 -25
- package/dist/layouts/index.js.map +0 -1
- package/dist/ui/button.d.ts +0 -53
- package/dist/ui/button.d.ts.map +0 -1
- package/dist/ui/button.js +0 -90
- package/dist/ui/button.js.map +0 -1
- package/src/components/CardGrid.astro +0 -41
- package/src/components/CardIcon.astro +0 -67
- package/src/components/CategoryGrid.astro +0 -114
- package/src/components/CollectionCard.astro +0 -56
- package/src/components/CollectionIndex.astro +0 -102
- package/src/components/CollectionPage.astro +0 -128
- package/src/components/CollectionRow.astro +0 -81
- package/src/components/CollectionTeaser.astro +0 -47
- package/src/components/ContributorsGrid.astro +0 -143
- package/src/components/DirectoryIndexClient.astro +0 -510
- package/src/components/EditorialSummary.astro +0 -58
- package/src/components/FilterGroupMenu.astro +0 -69
- package/src/components/FilterOptions.astro +0 -49
- package/src/components/FinalCta.astro +0 -43
- package/src/components/Hero.astro +0 -243
- package/src/components/Icon.astro +0 -191
- package/src/components/IndexRow.astro +0 -37
- package/src/components/LanguageBreakdown.astro +0 -67
- package/src/components/MarkdownBody.astro +0 -37
- package/src/components/OriginalCollection.astro +0 -117
- package/src/components/Pagination.astro +0 -50
- package/src/components/PoweredBy.astro +0 -60
- package/src/components/ProjectCard.astro +0 -321
- package/src/components/RecordHeader.astro +0 -221
- package/src/components/RecordSection.astro +0 -128
- package/src/components/RecordSidebar.astro +0 -387
- package/src/components/RefinePanel.astro +0 -168
- package/src/components/SmartLensTabs.astro +0 -81
- package/src/components/StackGrid.astro +0 -151
- package/src/components/StackPlatformChips.astro +0 -73
- package/src/components/SubmissionClient.astro +0 -254
- package/src/components/TableOfContents.astro +0 -140
- package/src/components/WhyThisExists.astro +0 -116
- package/src/layouts/BaseLayout.astro +0 -415
- package/src/layouts/Container.astro +0 -18
- package/src/layouts/Footer.astro +0 -231
- package/src/layouts/Header.astro +0 -214
- package/src/layouts/SectionHeader.astro +0 -43
- package/src/layouts/Seo.astro +0 -171
- package/src/layouts/ThemeToggle.astro +0 -118
- package/src/layouts/astro-shim.d.ts +0 -29
- package/src/layouts/index.ts +0 -24
- package/src/server/collections.test.ts +0 -212
- package/src/server/contrast.test.ts +0 -82
- package/src/server/directory.test.ts +0 -177
- package/src/server/github-repo.test.ts +0 -88
- package/src/server/models-home.test.ts +0 -101
- package/src/server/models.test.ts +0 -135
- package/src/server/seo.test.ts +0 -123
- package/src/ui/Badge.astro +0 -38
- package/src/ui/Button.astro +0 -42
- package/src/ui/EmptyState.astro +0 -38
- package/src/ui/FilterDrawer.astro +0 -124
- package/src/ui/PageHeader.astro +0 -39
- package/src/ui/SearchField.astro +0 -106
- package/src/ui/button.test.ts +0 -54
- package/src/ui/button.ts +0 -107
package/src/layouts/Footer.astro
DELETED
|
@@ -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>
|
package/src/layouts/Header.astro
DELETED
|
@@ -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>
|
package/src/layouts/Seo.astro
DELETED
|
@@ -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
|
-
)}
|