From 012be8ba3a6c5dcdbe070a7aa7ac2a07c6abe10e Mon Sep 17 00:00:00 2001 From: Sam Irons Date: Mon, 18 May 2026 10:34:12 +1000 Subject: [PATCH 1/4] Add JSON-LD TechArticle structured data for installation and getting-started sections Generates a Schema.org TechArticle JSON-LD block in for all pages under /docs/installation/* and /docs/getting-started/*. Scoped by URL path so child pages are covered without requiring frontmatter changes. Maps existing frontmatter fields (title, description, pubDate, modDate) to standard Schema.org properties. Co-Authored-By: Claude Sonnet 4.6 --- src/themes/octopus/components/HtmlHead.astro | 2 + src/themes/octopus/components/JsonLd.astro | 69 ++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/themes/octopus/components/JsonLd.astro diff --git a/src/themes/octopus/components/HtmlHead.astro b/src/themes/octopus/components/HtmlHead.astro index 2dbf2d1585..993cb82cad 100644 --- a/src/themes/octopus/components/HtmlHead.astro +++ b/src/themes/octopus/components/HtmlHead.astro @@ -3,6 +3,7 @@ import { Accelerator } from 'astro-accelerator-utils'; import type { Frontmatter } from 'astro-accelerator-utils/types/Frontmatter'; import { SITE, OPEN_GRAPH, HEADER_SCRIPTS } from '@config'; import { getEligibleSlugs } from '@util/mdxContent'; +import JsonLd from '@components/JsonLd.astro'; const accelerator = new Accelerator(SITE); const stats = new accelerator.statistics('octopus/components/HtmlHead.astro'); @@ -123,5 +124,6 @@ stats.stop(); {pageMeta.map((m) => )} {authorMeta.map((m) => )} + diff --git a/src/themes/octopus/components/JsonLd.astro b/src/themes/octopus/components/JsonLd.astro new file mode 100644 index 0000000000..bf62df08b7 --- /dev/null +++ b/src/themes/octopus/components/JsonLd.astro @@ -0,0 +1,69 @@ +--- +import type { Frontmatter } from 'astro-accelerator-utils/types/Frontmatter'; +import { SITE } from '@config'; + +type Props = { + frontmatter: Frontmatter; + canonicalURL: string; +}; + +const { frontmatter, canonicalURL } = Astro.props; + +const pathname = Astro.url.pathname.replace(/\/$/, ''); +const eligible = + pathname.startsWith('/docs/installation') || + pathname.startsWith('/docs/getting-started'); + +function toIsoDate(val: unknown): string | undefined { + if (!val) return undefined; + if (val instanceof Date) return val.toISOString().slice(0, 10); + const s = String(val).trim().split('T')[0]; + return s.length > 0 ? s : undefined; +} + +const headline = + typeof frontmatter.title === 'string' + ? frontmatter.title.replace(/[\[\]*`]/g, '').trim().slice(0, 110) + : ''; + +const description = + typeof frontmatter.description === 'string' && + frontmatter.description.trim().length > 0 + ? frontmatter.description.trim() + : undefined; + +const datePublished = toIsoDate(frontmatter.pubDate); +const dateModified = toIsoDate(frontmatter.modDate) ?? datePublished; +const lang = + typeof frontmatter.lang === 'string' ? frontmatter.lang : SITE.default.lang; + +const jsonLd = eligible + ? { + '@context': 'https://schema.org', + '@type': 'TechArticle', + headline, + ...(description && { description }), + url: canonicalURL, + ...(datePublished && { datePublished }), + ...(dateModified && { dateModified }), + inLanguage: lang, + publisher: { + '@type': 'Organization', + name: SITE.owner, + url: SITE.url, + }, + isPartOf: { + '@type': 'WebSite', + name: SITE.title, + url: SITE.url + SITE.subfolder, + }, + } + : null; + +// Escape sequences to prevent premature tag close. +const jsonLdString = jsonLd + ? JSON.stringify(jsonLd, null, 2).replace(/<\//g, '<\\/') + : null; +--- + +{jsonLdString && sequences to prevent premature tag close. -const jsonLdString = jsonLd - ? JSON.stringify(jsonLd, null, 2).replace(/<\//g, '<\\/') - : null; + const nodes: GraphNode[] = [article, buildWebSite(), buildOrganization()]; + return toGraphPayload(nodes); +} + +const payload = buildPayload(); --- -{jsonLdString && ,