@dogsbay/docs-layout 0.2.0-beta.98 → 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.98",
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.98",
34
- "@dogsbay/ui": "0.2.0-beta.98"
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
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;