@grove-dev/astro 0.5.4 → 0.6.1
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.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +0 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +0 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/ui/button.d.ts +8 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +8 -0
- package/dist/ui/button.js.map +1 -1
- package/package.json +2 -2
- package/src/components/DirectoryIndexClient.astro +351 -178
- package/src/components/Hero.astro +15 -11
- package/src/components/Pagination.astro +3 -3
- package/src/components/PoweredBy.astro +60 -0
- package/src/components/ProjectCard.astro +42 -25
- package/src/components/RecordHeader.astro +14 -9
- package/src/components/RefinePanel.astro +11 -2
- package/src/components/TableOfContents.astro +11 -12
- package/src/index.ts +8 -0
- package/src/layouts/BaseLayout.astro +14 -1
- package/src/layouts/Footer.astro +18 -2
- package/src/layouts/Header.astro +1 -1
- package/src/layouts/Seo.astro +64 -38
- package/src/lib/index.ts +0 -1
- package/src/server/collections.ts +96 -67
- package/src/server/index.ts +1 -0
- package/src/server/models.ts +456 -25
- package/src/server/seo.test.ts +123 -0
- package/src/server/seo.ts +141 -0
- package/src/styles.css +23 -6
- package/src/ui/FilterDrawer.astro +25 -5
- package/src/ui/SearchField.astro +58 -2
- package/src/ui/button.test.ts +1 -1
- package/src/ui/button.ts +9 -0
- package/dist/lib/load-collections.d.ts +0 -12
- package/dist/lib/load-collections.d.ts.map +0 -1
- package/dist/lib/load-collections.js +0 -34
- package/dist/lib/load-collections.js.map +0 -1
- package/src/lib/load-collections.ts +0 -33
|
@@ -126,18 +126,22 @@ const primaryCta: CTA | undefined =
|
|
|
126
126
|
|
|
127
127
|
<Container>
|
|
128
128
|
<div class="relative mx-auto flex max-w-[900px] flex-col items-center py-[88px] text-center lg:py-[104px]">
|
|
129
|
-
{/* Eyebrow
|
|
129
|
+
{/* Eyebrow. The `eyebrow` slot replaces the whole line — dot
|
|
130
|
+
included — so a consumer can put richer content here (a
|
|
131
|
+
`PoweredBy` link, a release badge) without forking the Hero. */}
|
|
130
132
|
<span class="inline-flex animate-fade-in items-center gap-2 text-[13px] font-medium text-ink-500 dark:text-ink-400">
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
<slot name="eyebrow">
|
|
134
|
+
<span aria-hidden="true" class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"></span>
|
|
135
|
+
{eyebrow}
|
|
136
|
+
{eyebrowBadge && (
|
|
137
|
+
<span
|
|
138
|
+
class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
|
|
139
|
+
aria-label={eyebrowBadge}
|
|
140
|
+
>
|
|
141
|
+
{eyebrowBadge}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</slot>
|
|
141
145
|
</span>
|
|
142
146
|
|
|
143
147
|
{/* Headline */}
|
|
@@ -7,7 +7,7 @@ interface Props {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
import { paginationPageList } from "@grove-dev/astro";
|
|
10
|
-
import { buttonClass, PAGINATION_EXTRA } from "../ui/button.js";
|
|
10
|
+
import { buttonClass, PAGINATION_DISABLED, PAGINATION_EXTRA } from "../ui/button.js";
|
|
11
11
|
|
|
12
12
|
const { current, total, hrefFor, class: className = "" } = Astro.props;
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ const currentClass = buttonClass("selected", "sm", PAGINATION_EXTRA);
|
|
|
24
24
|
{current > 1 ? (
|
|
25
25
|
<a href={hrefFor(current - 1)} class={linkClass} rel="prev">Previous</a>
|
|
26
26
|
) : (
|
|
27
|
-
<span class={`${linkClass}
|
|
27
|
+
<span class={`${linkClass} ${PAGINATION_DISABLED}`}>Previous</span>
|
|
28
28
|
)}
|
|
29
29
|
|
|
30
30
|
{pages.map((page) =>
|
|
@@ -44,7 +44,7 @@ const currentClass = buttonClass("selected", "sm", PAGINATION_EXTRA);
|
|
|
44
44
|
{current < total ? (
|
|
45
45
|
<a href={hrefFor(current + 1)} class={linkClass} rel="next">Next</a>
|
|
46
46
|
) : (
|
|
47
|
-
<span class={`${linkClass}
|
|
47
|
+
<span class={`${linkClass} ${PAGINATION_DISABLED}`}>Next</span>
|
|
48
48
|
)}
|
|
49
49
|
</nav>
|
|
50
50
|
)}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PoweredBy — "Powered by Grove" attribution.
|
|
4
|
+
*
|
|
5
|
+
* The mark is **inlined**, not loaded through `<img src>`: an SVG
|
|
6
|
+
* served as its own document cannot see page CSS, so `currentColor`
|
|
7
|
+
* never resolves there (the bug that cost the packaged icon set both
|
|
8
|
+
* themes before 0.5.3). Inlined, the mark simply inherits the
|
|
9
|
+
* surrounding text colour and follows the site's theme toggle — no
|
|
10
|
+
* second file, no script.
|
|
11
|
+
*
|
|
12
|
+
* Generic: no `siteConfig` import. `Footer` renders it under the brand
|
|
13
|
+
* block when `footer.poweredBy` is on; consumers can also drop it
|
|
14
|
+
* anywhere themselves — e.g. into `Hero`'s `eyebrow` slot.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Where the link points. Override only for a staging/docs mirror. */
|
|
19
|
+
href?: string;
|
|
20
|
+
/** Mark size in px. Default 16 — sized for 13px text. */
|
|
21
|
+
size?: number;
|
|
22
|
+
/** Extra class on the link. */
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
href = "https://grove.dev.mn",
|
|
28
|
+
size = 16,
|
|
29
|
+
class: className = "",
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<a
|
|
34
|
+
href={href}
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="noopener noreferrer"
|
|
37
|
+
class:list={[
|
|
38
|
+
"inline-flex items-center gap-1.5 transition-colors hover:text-ink-900 dark:hover:text-ink-100",
|
|
39
|
+
className,
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
<span>Powered by</span>
|
|
43
|
+
<svg
|
|
44
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
45
|
+
viewBox="0 0 32 32"
|
|
46
|
+
width={size}
|
|
47
|
+
height={size}
|
|
48
|
+
style={`width:${size}px;height:${size}px`}
|
|
49
|
+
fill="none"
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
class="shrink-0 align-middle"
|
|
52
|
+
>
|
|
53
|
+
<path d="M16 28V14" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
|
54
|
+
<path d="M16 14C16 14 16 8 22 6C22 6 22 12 16 14Z" fill="currentColor" fill-opacity="0.85"></path>
|
|
55
|
+
<path d="M16 14C16 14 16 8 10 6C10 6 10 12 16 14Z" fill="currentColor" fill-opacity="0.6"></path>
|
|
56
|
+
<path d="M16 18C16 18 16 22 20 23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
|
|
57
|
+
<path d="M16 21C16 21 16 24 12 25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
|
|
58
|
+
</svg>
|
|
59
|
+
<span class="font-semibold">Grove</span>
|
|
60
|
+
</a>
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
formatStars,
|
|
46
46
|
nameInitials,
|
|
47
47
|
projectStackIds,
|
|
48
|
+
truncateWords,
|
|
48
49
|
} from "@grove-dev/astro";
|
|
49
50
|
import CardIcon from "./CardIcon.astro";
|
|
50
51
|
import Icon from "./Icon.astro";
|
|
@@ -155,8 +156,13 @@ const avatarUrl =
|
|
|
155
156
|
|
|
156
157
|
const initials = nameInitials(name);
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// Trimmed on a word boundary before the two-line clamp gets to it: a
|
|
160
|
+
// CSS clamp cuts at whatever character the box runs out of room on,
|
|
161
|
+
// which is what produced "…and multi-", "…and retrieval", "…an" across
|
|
162
|
+
// a grid.
|
|
163
|
+
const description = truncateWords(
|
|
164
|
+
descriptionProp ?? (record as { description?: string } | undefined)?.description ?? "",
|
|
165
|
+
);
|
|
160
166
|
|
|
161
167
|
const stars =
|
|
162
168
|
starsProp ?? (isProject ? (record as { github?: { stars?: number } }).github?.stars : undefined);
|
|
@@ -197,15 +203,22 @@ const hasStacks = visibleStacks.length > 0;
|
|
|
197
203
|
|
|
198
204
|
<Root
|
|
199
205
|
{...rootAttrs}
|
|
200
|
-
class={`
|
|
206
|
+
class={`group relative flex h-full min-w-0 flex-col gap-3 rounded-xl border border-ink-200 bg-card p-5 outline-none transition-[border-color,background-color,transform] duration-150 hover:-translate-y-0.5 hover:border-ink-400 hover:bg-ink-50 focus-visible:border-ink-500 focus-visible:ring-2 focus-visible:ring-ink-900/15 dark:border-ink-800 dark:hover:border-ink-600 dark:hover:bg-ink-900 dark:focus-visible:border-ink-500 dark:focus-visible:ring-ink-100/15 ${className}`}
|
|
201
207
|
data-record-slug={slug}
|
|
202
208
|
data-event={event}
|
|
203
209
|
data-event-source={eventSource}
|
|
204
210
|
data-event-item={event ? slug : undefined}
|
|
205
211
|
>
|
|
206
|
-
{/* Header: icon + name + owner
|
|
212
|
+
{/* Header: icon + name + owner.
|
|
213
|
+
|
|
214
|
+
The initials chip is always in the DOM, `hidden` behind the image,
|
|
215
|
+
and unhidden by the image's own `onerror` — the same pattern
|
|
216
|
+
`Icon.astro` uses. Rendering it only when `avatarUrl` is falsy made
|
|
217
|
+
it unreachable for every record with a GitHub owner, so a dead
|
|
218
|
+
logo URL or a renamed org showed the browser's broken-image glyph
|
|
219
|
+
instead of a fallback. */}
|
|
207
220
|
<header class="flex items-start gap-3">
|
|
208
|
-
{avatarUrl
|
|
221
|
+
{avatarUrl && (
|
|
209
222
|
<img
|
|
210
223
|
src={avatarUrl}
|
|
211
224
|
alt=""
|
|
@@ -214,16 +227,18 @@ const hasStacks = visibleStacks.length > 0;
|
|
|
214
227
|
loading={eager ? "eager" : "lazy"}
|
|
215
228
|
fetchpriority={eager ? "high" : undefined}
|
|
216
229
|
decoding="async"
|
|
217
|
-
|
|
230
|
+
onerror="this.hidden=true;this.nextElementSibling.hidden=false"
|
|
231
|
+
class="h-12 w-12 shrink-0 rounded-lg bg-white object-contain p-1"
|
|
218
232
|
/>
|
|
219
|
-
) : (
|
|
220
|
-
<span
|
|
221
|
-
aria-hidden="true"
|
|
222
|
-
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-ink-100 text-base font-semibold uppercase text-ink-700 dark:bg-ink-800 dark:text-ink-200"
|
|
223
|
-
>
|
|
224
|
-
{initials}
|
|
225
|
-
</span>
|
|
226
233
|
)}
|
|
234
|
+
<span
|
|
235
|
+
aria-hidden="true"
|
|
236
|
+
hidden={Boolean(avatarUrl)}
|
|
237
|
+
data-grove-card-fallback-initials
|
|
238
|
+
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-ink-100 text-base font-semibold uppercase text-ink-700 dark:bg-ink-800 dark:text-ink-200"
|
|
239
|
+
>
|
|
240
|
+
{initials}
|
|
241
|
+
</span>
|
|
227
242
|
|
|
228
243
|
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
229
244
|
<h3 class="m-0 truncate text-[17px] font-semibold leading-snug tracking-tight text-ink-900 sm:text-[18px] dark:text-ink-100">
|
|
@@ -270,18 +285,20 @@ const hasStacks = visibleStacks.length > 0;
|
|
|
270
285
|
|
|
271
286
|
{/* Footer: stack logos · stars/updated · arrow */}
|
|
272
287
|
<footer class="mt-auto flex items-center justify-between gap-3 text-[12px] sm:text-[13px]">
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
{stackOverflow
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
288
|
+
{hasStacks ? (
|
|
289
|
+
<div class="flex min-w-0 items-center gap-2 text-ink-500 dark:text-ink-400">
|
|
290
|
+
{visibleStacks.map((stack) => (
|
|
291
|
+
<Icon name={stack} category="stack" size={16} title={stack} />
|
|
292
|
+
))}
|
|
293
|
+
{stackOverflow > 0 && (
|
|
294
|
+
<span class="text-[12px] text-ink-500 dark:text-ink-400">+{stackOverflow}</span>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
) : (
|
|
298
|
+
/* Keeps the footer's stars/updated block right-aligned without
|
|
299
|
+
shipping an empty flex container on every stackless card. */
|
|
300
|
+
<span></span>
|
|
301
|
+
)}
|
|
285
302
|
|
|
286
303
|
<div class="flex min-w-0 shrink-0 items-center gap-3 text-ink-500 dark:text-ink-400">
|
|
287
304
|
{hasStars && (
|
|
@@ -51,7 +51,10 @@ const {
|
|
|
51
51
|
at the row's right edge on wide viewports and wrap below on
|
|
52
52
|
mobile. */}
|
|
53
53
|
<div class="flex items-center gap-4">
|
|
54
|
-
{
|
|
54
|
+
{/* Initials stay in the DOM behind the logo and are revealed by the
|
|
55
|
+
image's own `onerror` — a dead logo URL or a renamed GitHub org
|
|
56
|
+
would otherwise render the broken-image glyph. */}
|
|
57
|
+
{avatarSrc && (
|
|
55
58
|
<img
|
|
56
59
|
src={avatarSrc}
|
|
57
60
|
alt=""
|
|
@@ -59,16 +62,18 @@ const {
|
|
|
59
62
|
height="56"
|
|
60
63
|
loading="lazy"
|
|
61
64
|
decoding="async"
|
|
62
|
-
|
|
65
|
+
onerror="this.hidden=true;this.nextElementSibling.hidden=false"
|
|
66
|
+
class="h-12 w-12 shrink-0 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 sm:h-14 sm:w-14"
|
|
63
67
|
/>
|
|
64
|
-
) : (
|
|
65
|
-
<span
|
|
66
|
-
aria-hidden="true"
|
|
67
|
-
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200 sm:h-14 sm:w-14"
|
|
68
|
-
>
|
|
69
|
-
{initials || "·"}
|
|
70
|
-
</span>
|
|
71
68
|
)}
|
|
69
|
+
<span
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
hidden={Boolean(avatarSrc)}
|
|
72
|
+
data-grove-card-fallback-initials
|
|
73
|
+
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200 sm:h-14 sm:w-14"
|
|
74
|
+
>
|
|
75
|
+
{initials || "·"}
|
|
76
|
+
</span>
|
|
72
77
|
|
|
73
78
|
<div class="min-w-0 flex-1">
|
|
74
79
|
<h1 class="m-0 text-[1.75rem] font-semibold leading-tight tracking-tight text-ink-900 dark:text-ink-100 sm:text-[2rem]">
|
|
@@ -110,13 +110,22 @@ const groups = groupModels.map((group) => ({
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
+
// A directory controller (DirectoryIndexClient) can adopt the URL
|
|
114
|
+
// in place and re-render without a reload; it signals that by
|
|
115
|
+
// cancelling the event. Anywhere else — a taxonomy page, no
|
|
116
|
+
// controller on the page at all — this falls back to navigating,
|
|
117
|
+
// so the panel keeps working on its own.
|
|
118
|
+
function go(url) {
|
|
119
|
+
const event = new CustomEvent("grove:navigate", { detail: { url }, cancelable: true });
|
|
120
|
+
if (document.dispatchEvent(event)) location.assign(url);
|
|
121
|
+
}
|
|
113
122
|
function navigate(group, values) {
|
|
114
123
|
const params = new URLSearchParams(location.search);
|
|
115
124
|
const key = keys[group.dataset.filterGroupKey];
|
|
116
125
|
params.delete(key);
|
|
117
126
|
params.delete("page");
|
|
118
127
|
values.forEach((value) => params.append(key, value));
|
|
119
|
-
|
|
128
|
+
go(params.toString() ? `${pathPrefix}?${params}` : pathPrefix);
|
|
120
129
|
}
|
|
121
130
|
groups.forEach((group) => {
|
|
122
131
|
const trigger = group.querySelector(".grove-filter-trigger");
|
|
@@ -153,7 +162,7 @@ const groups = groupModels.map((group) => ({
|
|
|
153
162
|
const params = new URLSearchParams(location.search);
|
|
154
163
|
params.set("sort", event.target.value);
|
|
155
164
|
params.delete("page");
|
|
156
|
-
|
|
165
|
+
go(params.toString() ? `${pathPrefix}?${params}` : pathPrefix);
|
|
157
166
|
});
|
|
158
167
|
})();
|
|
159
168
|
</script>
|
|
@@ -32,34 +32,33 @@ const hasItems = items.length > 1;
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
{hasItems && (
|
|
35
|
-
<details class="grove-toc
|
|
36
|
-
<summary class="cursor-pointer list-none
|
|
35
|
+
<details class="grove-toc lg:border-0">
|
|
36
|
+
<summary class="cursor-pointer list-none pb-3 text-2xs font-semibold uppercase tracking-wider text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100 lg:cursor-default lg:pointer-events-none lg:pb-2">
|
|
37
37
|
<span class="inline-flex items-center gap-1.5">
|
|
38
38
|
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="grove-toc-chevron transition-transform lg:hidden">
|
|
39
39
|
<path d="M6 4l4 4-4 4V4z" />
|
|
40
40
|
</svg>
|
|
41
41
|
{label}
|
|
42
|
-
<span class="ml-1 rounded-full bg-ink-100 px-1.5 py-0.5 text-[10px] font-medium text-ink-500 dark:bg-ink-800 dark:text-ink-300">
|
|
43
|
-
{items.length}
|
|
44
|
-
</span>
|
|
45
42
|
</span>
|
|
46
43
|
</summary>
|
|
47
44
|
{/* Full list, no inner scroll — the TOC lives in the right rail
|
|
48
|
-
and every entry stays visible. h3 entries indent under
|
|
49
|
-
h2 parents
|
|
50
|
-
|
|
45
|
+
and every entry stays visible. h3 entries indent further under
|
|
46
|
+
their h2 parents; the active state is a left-rail indicator +
|
|
47
|
+
text-color emphasis set by the script + styles.css. */}
|
|
48
|
+
<ol class="grove-toc-list list-none py-1 text-sm lg:py-0">
|
|
51
49
|
{items.map((entry) => (
|
|
52
50
|
<li
|
|
53
51
|
class:list={[
|
|
54
|
-
"grove-toc-item
|
|
55
|
-
entry.depth >= 3 && "
|
|
52
|
+
"grove-toc-item relative",
|
|
53
|
+
entry.depth >= 3 && "grove-toc-item--h3",
|
|
56
54
|
]}
|
|
57
55
|
>
|
|
58
56
|
<a
|
|
59
57
|
href={`#${entry.id}`}
|
|
60
58
|
class:list={[
|
|
61
|
-
"grove-toc-link block
|
|
62
|
-
entry.depth >=
|
|
59
|
+
"grove-toc-link block py-1.5 text-ink-500 no-underline transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100",
|
|
60
|
+
entry.depth >= 2 && "pl-3 lg:pl-4",
|
|
61
|
+
entry.depth >= 3 && "pl-6 text-[0.8125rem] lg:pl-7",
|
|
63
62
|
]}
|
|
64
63
|
data-toc-link={entry.id}
|
|
65
64
|
>
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,14 @@ export default function groveAstro(): AstroIntegration {
|
|
|
109
109
|
"@grove/generated": resolve(consumerRoot, "data/generated"),
|
|
110
110
|
},
|
|
111
111
|
},
|
|
112
|
+
// The OG image pipeline (`@grove-dev/core`'s og-image.ts)
|
|
113
|
+
// dynamically imports satori + resvg's native binding.
|
|
114
|
+
// They only ever run in `prepareDirectory` (Node, build
|
|
115
|
+
// time) — never in page code — so keep the bundler away
|
|
116
|
+
// from the .node binary.
|
|
117
|
+
ssr: {
|
|
118
|
+
external: ["satori", "@resvg/resvg-js"],
|
|
119
|
+
},
|
|
112
120
|
plugins: [
|
|
113
121
|
{
|
|
114
122
|
name: "grove:consumer-global-css-resolver",
|
|
@@ -66,6 +66,10 @@ interface Site {
|
|
|
66
66
|
logo?: string;
|
|
67
67
|
/** Favicon path under `public/` (e.g. `/favicon.svg`). */
|
|
68
68
|
favicon?: string;
|
|
69
|
+
/** BCP-47 language tag (e.g. "en"). Drives `<html lang>` and og:locale. */
|
|
70
|
+
locale?: string;
|
|
71
|
+
/** Twitter/X handle (e.g. "@grove") — emitted as twitter:site. */
|
|
72
|
+
twitter?: string;
|
|
69
73
|
analytics?: {
|
|
70
74
|
googleAnalyticsId?: string;
|
|
71
75
|
};
|
|
@@ -99,6 +103,11 @@ interface Props {
|
|
|
99
103
|
type?: 'website' | 'article' | 'profile';
|
|
100
104
|
noindex?: boolean;
|
|
101
105
|
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
106
|
+
/** Open Graph / Twitter image path (e.g. "/og/records/foo.png").
|
|
107
|
+
* Defaults to the site-wide /og-image.svg inside <Seo />. */
|
|
108
|
+
image?: string;
|
|
109
|
+
/** Alt text for the OG/Twitter image. */
|
|
110
|
+
imageAlt?: string;
|
|
102
111
|
/** Optional Google Analytics 4 Measurement ID (e.g. "G-XXXXXX"). */
|
|
103
112
|
gaId?: string;
|
|
104
113
|
/** Path used for JSON-LD `SearchAction` target. Defaults to "/items". */
|
|
@@ -116,6 +125,8 @@ const {
|
|
|
116
125
|
type,
|
|
117
126
|
noindex,
|
|
118
127
|
jsonLd,
|
|
128
|
+
image,
|
|
129
|
+
imageAlt,
|
|
119
130
|
gaId,
|
|
120
131
|
searchPath,
|
|
121
132
|
submitLabel,
|
|
@@ -187,7 +198,7 @@ const favicon =
|
|
|
187
198
|
---
|
|
188
199
|
|
|
189
200
|
<!doctype html>
|
|
190
|
-
<html lang=
|
|
201
|
+
<html lang={site.locale ?? 'en'} style={themeStyle}>
|
|
191
202
|
<head>
|
|
192
203
|
<meta charset="UTF-8" />
|
|
193
204
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -235,6 +246,8 @@ const favicon =
|
|
|
235
246
|
type={type}
|
|
236
247
|
noindex={noindex}
|
|
237
248
|
jsonLd={jsonLd}
|
|
249
|
+
{...(image ? { image } : {})}
|
|
250
|
+
imageAlt={imageAlt}
|
|
238
251
|
site={site}
|
|
239
252
|
searchPath={directoryPath}
|
|
240
253
|
/>
|
package/src/layouts/Footer.astro
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* so consumers can customize the order and copy. Defaults are
|
|
13
13
|
* provided so the footer still renders if no extra data is supplied.
|
|
14
14
|
*/
|
|
15
|
+
import PoweredBy from "../components/PoweredBy.astro";
|
|
15
16
|
|
|
16
17
|
interface NavItem {
|
|
17
18
|
label: string;
|
|
@@ -34,6 +35,8 @@ interface Site {
|
|
|
34
35
|
columns?: FooterColumn[];
|
|
35
36
|
copyright?: string;
|
|
36
37
|
license?: string;
|
|
38
|
+
/** "Powered by Grove" under the brand block. Defaults to on. */
|
|
39
|
+
poweredBy?: boolean;
|
|
37
40
|
};
|
|
38
41
|
stats?: {
|
|
39
42
|
totalApps?: number;
|
|
@@ -62,6 +65,11 @@ interface Props {
|
|
|
62
65
|
copyrightHolder?: string;
|
|
63
66
|
/** License line under the bar (e.g. "Content under CC BY-SA 4.0."). */
|
|
64
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;
|
|
65
73
|
/** Extra class for the outer footer element. */
|
|
66
74
|
class?: string;
|
|
67
75
|
}
|
|
@@ -75,9 +83,12 @@ const {
|
|
|
75
83
|
itemsLabel = "items",
|
|
76
84
|
copyrightHolder,
|
|
77
85
|
licenseLine,
|
|
86
|
+
poweredBy,
|
|
78
87
|
class: className = "",
|
|
79
88
|
} = Astro.props;
|
|
80
89
|
|
|
90
|
+
const showPoweredBy = poweredBy ?? site.footer?.poweredBy ?? true;
|
|
91
|
+
|
|
81
92
|
const year = new Date().getFullYear();
|
|
82
93
|
const holder = copyrightHolder ?? site.footer?.copyright ?? site.name;
|
|
83
94
|
const gh = site.repoUrl;
|
|
@@ -124,7 +135,7 @@ const resolvedLicenseLine = licenseLine ?? site.footer?.license;
|
|
|
124
135
|
]}
|
|
125
136
|
>
|
|
126
137
|
<div
|
|
127
|
-
class="mx-auto grid w-full max-w-
|
|
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"
|
|
128
139
|
>
|
|
129
140
|
<div class="col-span-2 md:col-span-1">
|
|
130
141
|
<div
|
|
@@ -166,6 +177,11 @@ const resolvedLicenseLine = licenseLine ?? site.footer?.license;
|
|
|
166
177
|
)}
|
|
167
178
|
</p>
|
|
168
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
|
+
)}
|
|
169
185
|
</div>
|
|
170
186
|
|
|
171
187
|
{columns.map((column) => (
|
|
@@ -194,7 +210,7 @@ const resolvedLicenseLine = licenseLine ?? site.footer?.license;
|
|
|
194
210
|
|
|
195
211
|
<div class="border-t border-ink-200 dark:border-ink-800">
|
|
196
212
|
<div
|
|
197
|
-
class="mx-auto flex w-full max-w-
|
|
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"
|
|
198
214
|
>
|
|
199
215
|
<span>© {year} {holder}{resolvedLicenseLine ? `. ${resolvedLicenseLine}` : "."}</span>
|
|
200
216
|
{gh && (
|
package/src/layouts/Header.astro
CHANGED