@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/docs-layout",
3
- "version": "0.2.0-beta.96",
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.96",
34
- "@dogsbay/ui": "0.2.0-beta.96"
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",
@@ -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
- const articleJsonLd = (ogType === "article" && tagKeywords.length > 0)
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
- : undefined;
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
- if (localStorage.getItem("theme") === "dark") {
844
- document.documentElement.classList.add("dark");
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-700 dark:text-amber-300",
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
- <span class="opacity-70">{chip.prefixLabel}:</span>
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;