@dogsbay/docs-layout 0.2.0-beta.96 → 0.2.0-beta.99
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 +3 -3
- package/src/DocsLayout.astro +84 -6
- package/src/TagList.astro +17 -2
- package/src/json-ld.ts +3 -1
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.99",
|
|
4
4
|
"description": "Standard documentation layout components for Dogsbay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"./json-ld": "./src/json-ld.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@dogsbay/primitives": "0.2.0-beta.
|
|
34
|
-
"@dogsbay/ui": "0.2.0-beta.
|
|
33
|
+
"@dogsbay/primitives": "0.2.0-beta.99",
|
|
34
|
+
"@dogsbay/ui": "0.2.0-beta.99"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"happy-dom": "^20.10.6",
|
package/src/DocsLayout.astro
CHANGED
|
@@ -47,7 +47,7 @@ import VersionSwitcher from "./VersionSwitcher.astro";
|
|
|
47
47
|
import LocaleSwitcher from "./LocaleSwitcher.astro";
|
|
48
48
|
import { filterNavByAxis } from "./nav-filter.js";
|
|
49
49
|
import type { LlmProviderName } from "./llm-actions.js";
|
|
50
|
-
import { jsonLdTypeFor, normalizeCustomJsonLd, buildArticleJsonLd } from "./json-ld.js";
|
|
50
|
+
import { jsonLdTypeFor, normalizeCustomJsonLd, buildArticleJsonLd, buildWebSiteJsonLd } from "./json-ld.js";
|
|
51
51
|
import { resolveTagKeywords } from "./tag-list-data.js";
|
|
52
52
|
|
|
53
53
|
interface NavItem {
|
|
@@ -623,6 +623,23 @@ const computedCanonical = canonicalUrl
|
|
|
623
623
|
* raw value if it will not parse, so a typo shows up as itself rather
|
|
624
624
|
* than as "Invalid Date".
|
|
625
625
|
*/
|
|
626
|
+
/**
|
|
627
|
+
* A value as a machine-readable ISO 8601 datetime, or "" when it is
|
|
628
|
+
* not a date at all.
|
|
629
|
+
*
|
|
630
|
+
* Unlike `_formatPostDate`, an unparseable value must NOT pass
|
|
631
|
+
* through: this feeds `article:published_time` / `modified_time`,
|
|
632
|
+
* where a malformed datetime is worse than an absent one — a consumer
|
|
633
|
+
* either ignores the whole article object or shows garbage.
|
|
634
|
+
* `lastUpdated` is documented as also accepting a version/tag string
|
|
635
|
+
* ("v2.1"), which must never reach these tags.
|
|
636
|
+
*/
|
|
637
|
+
function isoDate(value: string | undefined): string {
|
|
638
|
+
if (!value) return "";
|
|
639
|
+
const d = new Date(value);
|
|
640
|
+
return Number.isNaN(d.getTime()) ? "" : d.toISOString();
|
|
641
|
+
}
|
|
642
|
+
|
|
626
643
|
function _formatPostDate(iso: string): string {
|
|
627
644
|
const d = new Date(iso);
|
|
628
645
|
if (Number.isNaN(d.getTime())) return iso;
|
|
@@ -682,7 +699,14 @@ const tagKeywords = resolveTagKeywords(tags, tagLabels);
|
|
|
682
699
|
// Course; everything else → Article. Matches the conventions
|
|
683
700
|
// search engines key off for educational / tutorial / reference
|
|
684
701
|
// SERP rendering. See `json-ld.ts` for the full mapping table.
|
|
685
|
-
|
|
702
|
+
// Structured data is emitted for EVERY page, not only tagged articles.
|
|
703
|
+
// This used to also require `tagKeywords.length > 0`, so an untagged
|
|
704
|
+
// page — a homepage, a landing page, a section index — emitted none at
|
|
705
|
+
// all. An external agent audit reported "No JSON-LD structured data
|
|
706
|
+
// found on homepage" and it was right: the page most likely to be an
|
|
707
|
+
// agent's entry point was the one describing itself the least.
|
|
708
|
+
// Keywords are enrichment, not a precondition for identity.
|
|
709
|
+
const articleJsonLd = ogType === "article"
|
|
686
710
|
? buildArticleJsonLd({
|
|
687
711
|
type: jsonLdTypeFor(pageType),
|
|
688
712
|
title,
|
|
@@ -693,7 +717,12 @@ const articleJsonLd = (ogType === "article" && tagKeywords.length > 0)
|
|
|
693
717
|
url: computedCanonical,
|
|
694
718
|
headings,
|
|
695
719
|
})
|
|
696
|
-
:
|
|
720
|
+
: buildWebSiteJsonLd({
|
|
721
|
+
siteName,
|
|
722
|
+
title,
|
|
723
|
+
description: metaDescription,
|
|
724
|
+
url: computedCanonical,
|
|
725
|
+
});
|
|
697
726
|
|
|
698
727
|
// `customJsonLd` accepts either a single object or an array.
|
|
699
728
|
// Normalize to an array so we can iterate uniformly. Empty array
|
|
@@ -806,6 +835,26 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
806
835
|
{ogType === "article" && tagKeywords.map((keyword) => (
|
|
807
836
|
<meta property="article:tag" content={keyword} />
|
|
808
837
|
))}
|
|
838
|
+
{/* Article dates. `og:type="article"` was declared with no times
|
|
839
|
+
at all, which is an incomplete article object — crawlers and
|
|
840
|
+
social cards had no publish date to show even though the page
|
|
841
|
+
renders one visibly.
|
|
842
|
+
|
|
843
|
+
`article:modified_time` falls back to `lastUpdated`, the prop
|
|
844
|
+
the docs chrome already uses for "Last updated", so a docs page
|
|
845
|
+
gets a modified time without the blog-only `updatedDate` being
|
|
846
|
+
set. Only emitted when the value parses as a date: a
|
|
847
|
+
frontmatter `lastUpdated: v2.1` is a version string, and a
|
|
848
|
+
malformed datetime is worse than an absent one. */}
|
|
849
|
+
{ogType === "article" && isoDate(publishedDate) && (
|
|
850
|
+
<meta property="article:published_time" content={isoDate(publishedDate)} />
|
|
851
|
+
)}
|
|
852
|
+
{ogType === "article" && isoDate(updatedDate ?? lastUpdated) && (
|
|
853
|
+
<meta
|
|
854
|
+
property="article:modified_time"
|
|
855
|
+
content={isoDate(updatedDate ?? lastUpdated)}
|
|
856
|
+
/>
|
|
857
|
+
)}
|
|
809
858
|
|
|
810
859
|
{/* JSON-LD primary block — Article / HowTo / TechArticle /
|
|
811
860
|
Course depending on `pageType`. Emits keywords as a
|
|
@@ -839,10 +888,39 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
839
888
|
></script>
|
|
840
889
|
)}
|
|
841
890
|
|
|
891
|
+
{/*
|
|
892
|
+
Theme bootstrap. Inline and BLOCKING on purpose — it must run
|
|
893
|
+
before first paint or the page flashes light before going dark.
|
|
894
|
+
|
|
895
|
+
An explicit choice wins in BOTH directions. Someone on a
|
|
896
|
+
dark-mode OS who deliberately picked light must stay light, so
|
|
897
|
+
"light" is checked rather than treated as "not dark". Only the
|
|
898
|
+
absence of a stored value falls through to the OS preference.
|
|
899
|
+
|
|
900
|
+
That fallback is the point of this block. Previously the only
|
|
901
|
+
condition was `=== "dark"`, so a visitor whose OS is in dark mode
|
|
902
|
+
got a light page until they found the toggle — and nothing else
|
|
903
|
+
in the CSS consulted prefers-color-scheme either. It also made
|
|
904
|
+
dark mode untestable by any tool that emulates the media query
|
|
905
|
+
(DevTools' Rendering panel, Playwright's colorScheme), because
|
|
906
|
+
the page did not read it.
|
|
907
|
+
|
|
908
|
+
localStorage access throws in some privacy modes; a throw here
|
|
909
|
+
would skip the class entirely, so it is guarded and degrades to
|
|
910
|
+
the OS preference.
|
|
911
|
+
*/}
|
|
842
912
|
<script is:inline>
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
913
|
+
(function () {
|
|
914
|
+
var stored = null;
|
|
915
|
+
try {
|
|
916
|
+
stored = localStorage.getItem("theme");
|
|
917
|
+
} catch (e) {}
|
|
918
|
+
var dark =
|
|
919
|
+
stored === "dark" ||
|
|
920
|
+
(stored !== "light" &&
|
|
921
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
922
|
+
if (dark) document.documentElement.classList.add("dark");
|
|
923
|
+
})();
|
|
846
924
|
</script>
|
|
847
925
|
<slot name="head" />
|
|
848
926
|
</head>
|
package/src/TagList.astro
CHANGED
|
@@ -72,8 +72,13 @@ const chips = buildChips(tags, { indexPath, prefixes, labels });
|
|
|
72
72
|
*/
|
|
73
73
|
const PALETTE: Record<TagPaletteName, string> = {
|
|
74
74
|
blue: "border-blue-500/40 bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
75
|
+
// amber-800, not -700, in light mode: amber-700 on amber-500/15 is
|
|
76
|
+
// 4.48:1 — under 4.5:1 by a hair, and failing regardless of the
|
|
77
|
+
// opacity bug below. -800 gives 6.32:1. Every other colour clears it
|
|
78
|
+
// at -700 (4.76–8.60:1). Dark mode is unaffected: text-*-300 on the
|
|
79
|
+
// same tint over a dark page is 9–12:1 across the palette.
|
|
75
80
|
amber:
|
|
76
|
-
"border-amber-500/40 bg-amber-500/15 text-amber-
|
|
81
|
+
"border-amber-500/40 bg-amber-500/15 text-amber-800 dark:text-amber-300",
|
|
77
82
|
emerald:
|
|
78
83
|
"border-emerald-500/40 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
|
79
84
|
violet:
|
|
@@ -108,7 +113,17 @@ const DEFAULT_CLASS =
|
|
|
108
113
|
>
|
|
109
114
|
{chip.prefixLabel ? (
|
|
110
115
|
<Fragment>
|
|
111
|
-
|
|
116
|
+
{/*
|
|
117
|
+
No opacity here. `opacity-70` on the prefix blended the
|
|
118
|
+
text 30% toward the chip background and put EVERY colour
|
|
119
|
+
in the palette below 4.5:1 — measured 2.75–3.95:1, worst
|
|
120
|
+
amber, best slate. It is 12px text, so 4.5:1 applies.
|
|
121
|
+
|
|
122
|
+
The prefix/leaf distinction is carried by font-weight
|
|
123
|
+
instead: the leaf below is font-semibold against the
|
|
124
|
+
anchor's font-medium. Weight costs no contrast.
|
|
125
|
+
*/}
|
|
126
|
+
<span>{chip.prefixLabel}:</span>
|
|
112
127
|
<span class="font-semibold">{chip.leafLabel}</span>
|
|
113
128
|
</Fragment>
|
|
114
129
|
) : (
|
package/src/json-ld.ts
CHANGED
|
@@ -100,8 +100,10 @@ export function buildArticleJsonLd(
|
|
|
100
100
|
"@context": "https://schema.org",
|
|
101
101
|
"@type": type,
|
|
102
102
|
name: title,
|
|
103
|
-
keywords: keywords.join(", "),
|
|
104
103
|
};
|
|
104
|
+
// Only when there are any. An empty `keywords: ""` asserts the page
|
|
105
|
+
// has no topics, which is a different claim from staying silent.
|
|
106
|
+
if (keywords.length > 0) block.keywords = keywords.join(", ");
|
|
105
107
|
|
|
106
108
|
if (type === "Article" || type === "TechArticle") {
|
|
107
109
|
block.headline = title;
|