@dogsbay/docs-layout 0.2.0-beta.10 → 0.2.0-beta.100
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/package.json +7 -5
- package/src/BlogIndex.astro +179 -0
- package/src/DocsFooter.astro +27 -3
- package/src/DocsLayout.astro +541 -40
- package/src/DocsNavClient.astro +107 -0
- package/src/DocsToc.astro +1 -1
- package/src/SearchDialog.astro +301 -33
- package/src/TagList.astro +17 -2
- package/src/VersionSwitcher.astro +6 -0
- package/src/docs-nav-client.ts +419 -0
- package/src/json-ld.ts +112 -0
- package/src/link-icons.ts +54 -0
- package/src/markdown-negotiation.ts +38 -2
- package/src/nav-filter.ts +42 -129
- package/src/search-facets.ts +511 -9
- package/src/switcher.ts +83 -2
- package/src/toc-placement.ts +71 -0
- package/src/version-redirect.ts +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/docs-layout",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.100",
|
|
4
4
|
"description": "Standard documentation layout components for Dogsbay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./TagList.astro": "./src/TagList.astro",
|
|
14
14
|
"./StatusBadge.astro": "./src/StatusBadge.astro",
|
|
15
15
|
"./TypeBadge.astro": "./src/TypeBadge.astro",
|
|
16
|
+
"./BlogIndex.astro": "./src/BlogIndex.astro",
|
|
16
17
|
"./TaxonomyIndex.astro": "./src/TaxonomyIndex.astro",
|
|
17
18
|
"./TaxonomyTerm.astro": "./src/TaxonomyTerm.astro",
|
|
18
19
|
"./SearchDialog.astro": "./src/SearchDialog.astro",
|
|
@@ -29,14 +30,15 @@
|
|
|
29
30
|
"./json-ld": "./src/json-ld.ts"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@dogsbay/
|
|
33
|
-
"@dogsbay/
|
|
33
|
+
"@dogsbay/primitives": "0.2.0-beta.100",
|
|
34
|
+
"@dogsbay/ui": "0.2.0-beta.100"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"
|
|
37
|
+
"happy-dom": "^20.10.6",
|
|
38
|
+
"vitest": "^4.1.10"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
|
-
"astro": "^5.0.0 || ^6.0.0"
|
|
41
|
+
"astro": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
40
42
|
},
|
|
41
43
|
"files": [
|
|
42
44
|
"src",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* BlogIndex — reverse-chronological list of posts.
|
|
4
|
+
*
|
|
5
|
+
* Reads the slice its route file hands it from `src/data/blog.json`
|
|
6
|
+
* (emitted by `format-astro/src/blog.ts` during `dogsbay site build`)
|
|
7
|
+
* and renders one card per post plus page navigation.
|
|
8
|
+
*
|
|
9
|
+
* All layout lives here rather than in the generated route files, so a
|
|
10
|
+
* design fix propagates on the next build instead of being baked into
|
|
11
|
+
* every emitted page — the same reason `TaxonomyIndex` exists.
|
|
12
|
+
*
|
|
13
|
+
* Uses the shared theme tokens only. The blog is the same site as the
|
|
14
|
+
* docs, so there is no blog palette, no blog typography scale, and no
|
|
15
|
+
* second set of card styles to keep in sync.
|
|
16
|
+
*/
|
|
17
|
+
import Card from "@dogsbay/ui/card/Card.astro";
|
|
18
|
+
|
|
19
|
+
interface BlogPostRef {
|
|
20
|
+
slug: string;
|
|
21
|
+
title: string;
|
|
22
|
+
url: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
date?: string;
|
|
25
|
+
author?: string[];
|
|
26
|
+
tags?: string[];
|
|
27
|
+
heroImage?: string;
|
|
28
|
+
readingMinutes: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
/** The posts for THIS page of the index, newest first. */
|
|
33
|
+
posts: BlogPostRef[];
|
|
34
|
+
/** URL-form index path, already basePath-prefixed. */
|
|
35
|
+
indexPath: string;
|
|
36
|
+
pageNo: number;
|
|
37
|
+
totalPages: number;
|
|
38
|
+
/** Page heading. Defaults to "Blog", or "Blog — page N" beyond page 1. */
|
|
39
|
+
heading?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
posts,
|
|
44
|
+
indexPath,
|
|
45
|
+
pageNo,
|
|
46
|
+
totalPages,
|
|
47
|
+
heading = pageNo === 1 ? "Blog" : `Blog — page ${pageNo}`,
|
|
48
|
+
} = Astro.props;
|
|
49
|
+
|
|
50
|
+
/** Build time, so every reader sees the same string. See DocsLayout. */
|
|
51
|
+
function formatDate(iso: string): string {
|
|
52
|
+
const d = new Date(iso);
|
|
53
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
54
|
+
// timeZone: "UTC" is load-bearing. parseMeta normalizes `created:
|
|
55
|
+
// 2026-01-01` to "2026-01-01T00:00:00.000Z", so a build host in any
|
|
56
|
+
// negative-offset zone (the default on many CI runners) would render
|
|
57
|
+
// "31 December 2025". DocsFooter already carries this fix; these
|
|
58
|
+
// copies dropped it.
|
|
59
|
+
return d.toLocaleDateString("en-GB", {
|
|
60
|
+
year: "numeric",
|
|
61
|
+
month: "long",
|
|
62
|
+
day: "numeric",
|
|
63
|
+
timeZone: "UTC",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const base = indexPath === "/" ? "" : indexPath.replace(/\/+$/, "");
|
|
68
|
+
const hrefForPage = (n: number): string => (n === 1 ? `${base}/` : `${base}/page/${n}`);
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
{/*
|
|
72
|
+
The index needs its own <h1>. Post titles are <h2>, so without one the
|
|
73
|
+
page has an h2 with no h1 above it — a heading-order violation axe-core
|
|
74
|
+
flags, and a missing page heading for anyone navigating by headings.
|
|
75
|
+
The sticky header's site name is chrome, not a page heading.
|
|
76
|
+
*/}
|
|
77
|
+
<h1 class="text-3xl font-bold tracking-tight mb-8">{heading}</h1>
|
|
78
|
+
|
|
79
|
+
{posts.length === 0 && (
|
|
80
|
+
<p class="text-muted-foreground">No posts yet.</p>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/*
|
|
84
|
+
Three across at lg, two at sm, one on mobile. The card is one link, not
|
|
85
|
+
a card containing links: nesting interactive elements inside a clickable
|
|
86
|
+
card is the classic keyboard-and-screen-reader trap, so tags and other
|
|
87
|
+
links stay off the card and live on the post page instead.
|
|
88
|
+
|
|
89
|
+
`items-stretch` + `h-full` keeps a short post's card the same height as
|
|
90
|
+
a long one's — ragged card bottoms are the thing that makes a grid look
|
|
91
|
+
broken rather than sparse.
|
|
92
|
+
*/}
|
|
93
|
+
<ul
|
|
94
|
+
class="not-prose grid list-none grid-cols-1 items-stretch gap-6 p-0 sm:grid-cols-2 lg:grid-cols-3"
|
|
95
|
+
data-blog-index
|
|
96
|
+
>
|
|
97
|
+
{posts.map((post) => (
|
|
98
|
+
<li class="m-0 p-0">
|
|
99
|
+
<a
|
|
100
|
+
href={post.url}
|
|
101
|
+
class="group block h-full rounded-lg no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
102
|
+
>
|
|
103
|
+
<Card class="flex h-full flex-col gap-3 p-5 transition-colors group-hover:border-ring">
|
|
104
|
+
{post.heroImage && (
|
|
105
|
+
<img
|
|
106
|
+
src={post.heroImage}
|
|
107
|
+
alt=""
|
|
108
|
+
class="aspect-[16/9] w-full rounded-md border border-border object-cover"
|
|
109
|
+
loading="lazy"
|
|
110
|
+
decoding="async"
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<h2 class="text-lg font-semibold leading-snug tracking-tight text-foreground group-hover:underline">
|
|
115
|
+
{post.title}
|
|
116
|
+
</h2>
|
|
117
|
+
|
|
118
|
+
{/*
|
|
119
|
+
data-pagefind-ignore: the same values are indexed as structured
|
|
120
|
+
filters on the post itself. Without it every card's date leads
|
|
121
|
+
its search excerpt.
|
|
122
|
+
*/}
|
|
123
|
+
<div
|
|
124
|
+
class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground"
|
|
125
|
+
data-pagefind-ignore
|
|
126
|
+
>
|
|
127
|
+
{post.author && post.author.length > 0 && (
|
|
128
|
+
<span class="font-medium text-foreground">{post.author.join(", ")}</span>
|
|
129
|
+
)}
|
|
130
|
+
{post.author && post.author.length > 0 && post.date && (
|
|
131
|
+
<span aria-hidden="true">·</span>
|
|
132
|
+
)}
|
|
133
|
+
{post.date && <time datetime={post.date}>{formatDate(post.date)}</time>}
|
|
134
|
+
{(post.author?.length || post.date) && <span aria-hidden="true">·</span>}
|
|
135
|
+
<span>{post.readingMinutes} min read</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{post.description && (
|
|
139
|
+
<p class="m-0 text-sm leading-relaxed text-muted-foreground">
|
|
140
|
+
{post.description}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</Card>
|
|
144
|
+
</a>
|
|
145
|
+
</li>
|
|
146
|
+
))}
|
|
147
|
+
</ul>
|
|
148
|
+
|
|
149
|
+
{totalPages > 1 && (
|
|
150
|
+
<nav class="not-prose mt-12 flex items-center justify-between" aria-label="Blog pages">
|
|
151
|
+
{pageNo > 1 ? (
|
|
152
|
+
<a
|
|
153
|
+
href={hrefForPage(pageNo - 1)}
|
|
154
|
+
class="text-sm text-foreground no-underline hover:underline"
|
|
155
|
+
rel="prev"
|
|
156
|
+
>
|
|
157
|
+
← Newer posts
|
|
158
|
+
</a>
|
|
159
|
+
) : (
|
|
160
|
+
<span></span>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<span class="text-sm text-muted-foreground">
|
|
164
|
+
Page {pageNo} of {totalPages}
|
|
165
|
+
</span>
|
|
166
|
+
|
|
167
|
+
{pageNo < totalPages ? (
|
|
168
|
+
<a
|
|
169
|
+
href={hrefForPage(pageNo + 1)}
|
|
170
|
+
class="text-sm text-foreground no-underline hover:underline"
|
|
171
|
+
rel="next"
|
|
172
|
+
>
|
|
173
|
+
Older posts →
|
|
174
|
+
</a>
|
|
175
|
+
) : (
|
|
176
|
+
<span></span>
|
|
177
|
+
)}
|
|
178
|
+
</nav>
|
|
179
|
+
)}
|
package/src/DocsFooter.astro
CHANGED
|
@@ -14,6 +14,14 @@ interface Props {
|
|
|
14
14
|
lastUpdated?: string;
|
|
15
15
|
prev?: PaginationLink;
|
|
16
16
|
next?: PaginationLink;
|
|
17
|
+
/**
|
|
18
|
+
* Captions above the prev/next titles. Default "Previous" / "Next",
|
|
19
|
+
* which is right for docs, where adjacency is position in the sidebar.
|
|
20
|
+
* A blog's adjacency is TIME, so it passes "Newer" / "Older" — the
|
|
21
|
+
* link means something different there and the label should say so.
|
|
22
|
+
*/
|
|
23
|
+
prevLabel?: string;
|
|
24
|
+
nextLabel?: string;
|
|
17
25
|
copyright?: string;
|
|
18
26
|
/**
|
|
19
27
|
* When true, render a low-key footer link to `/llms.txt` so
|
|
@@ -32,11 +40,27 @@ const {
|
|
|
32
40
|
lastUpdated,
|
|
33
41
|
prev,
|
|
34
42
|
next,
|
|
43
|
+
prevLabel = "Previous",
|
|
44
|
+
nextLabel = "Next",
|
|
35
45
|
copyright,
|
|
36
46
|
llmsLink = false,
|
|
37
47
|
llmsLinkHref = "/llms.txt",
|
|
38
48
|
class: className,
|
|
39
49
|
} = Astro.props;
|
|
50
|
+
|
|
51
|
+
// `lastUpdated` is a canonical ISO string (e.g. "2026-06-16T00:00:00.000Z").
|
|
52
|
+
// Render it as a clean, locale-stable date; UTC avoids an off-by-one when the
|
|
53
|
+
// build host isn't on UTC. Non-date strings (e.g. a version/tag) pass through.
|
|
54
|
+
function formatDate(value: string): string {
|
|
55
|
+
const d = new Date(value);
|
|
56
|
+
if (Number.isNaN(d.getTime())) return value;
|
|
57
|
+
return d.toLocaleDateString("en-US", {
|
|
58
|
+
year: "numeric",
|
|
59
|
+
month: "long",
|
|
60
|
+
day: "numeric",
|
|
61
|
+
timeZone: "UTC",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
40
64
|
---
|
|
41
65
|
|
|
42
66
|
<footer class:list={["mt-12 border-t pt-6", className]}>
|
|
@@ -49,7 +73,7 @@ const {
|
|
|
49
73
|
Edit this page
|
|
50
74
|
</a>
|
|
51
75
|
)}
|
|
52
|
-
{lastUpdated && <span>Last updated: {lastUpdated}</span>}
|
|
76
|
+
{lastUpdated && <span>Last updated: {formatDate(lastUpdated)}</span>}
|
|
53
77
|
</div>
|
|
54
78
|
)}
|
|
55
79
|
|
|
@@ -60,7 +84,7 @@ const {
|
|
|
60
84
|
<a href={prev.href} class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors hover:bg-accent" rel="prev">
|
|
61
85
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
62
86
|
<div class="text-left">
|
|
63
|
-
<div class="text-xs text-muted-foreground">
|
|
87
|
+
<div class="text-xs text-muted-foreground">{prevLabel}</div>
|
|
64
88
|
<div>{prev.label}</div>
|
|
65
89
|
</div>
|
|
66
90
|
</a>
|
|
@@ -68,7 +92,7 @@ const {
|
|
|
68
92
|
{next ? (
|
|
69
93
|
<a href={next.href} class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors hover:bg-accent" rel="next">
|
|
70
94
|
<div class="text-right">
|
|
71
|
-
<div class="text-xs text-muted-foreground">
|
|
95
|
+
<div class="text-xs text-muted-foreground">{nextLabel}</div>
|
|
72
96
|
<div>{next.label}</div>
|
|
73
97
|
</div>
|
|
74
98
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|