From 9d4290bf96b53bc8c9033780588ed58ced835f81 Mon Sep 17 00:00:00 2001 From: Liam Randall Date: Sat, 6 Jun 2026 15:27:43 -0400 Subject: [PATCH 1/3] fixing minor template parse errors for transcript pages Signed-off-by: Liam Randall --- src/pages/_index/index.tsx | 5 ++++- src/theme/wasmcloud/community/video-seo.tsx | 23 +++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pages/_index/index.tsx b/src/pages/_index/index.tsx index f0f21d63f..93a394b79 100644 --- a/src/pages/_index/index.tsx +++ b/src/pages/_index/index.tsx @@ -44,7 +44,10 @@ const softwareApplicationSchema = { 'wasmCloud is an open source CNCF project that enables teams to build, manage, and scale polyglot Wasm applications across any cloud, Kubernetes, or edge.', url: 'https://wasmcloud.com', downloadUrl: 'https://wasmcloud.com/docs/installation/', - softwareHelp: 'https://wasmcloud.com/docs/', + softwareHelp: { + '@type': 'CreativeWork', + url: 'https://wasmcloud.com/docs/', + }, license: 'https://www.apache.org/licenses/LICENSE-2.0', isAccessibleForFree: true, author: { '@id': 'https://wasmcloud.com/#organization' }, diff --git a/src/theme/wasmcloud/community/video-seo.tsx b/src/theme/wasmcloud/community/video-seo.tsx index c1bfbbddd..dd2c3ccff 100644 --- a/src/theme/wasmcloud/community/video-seo.tsx +++ b/src/theme/wasmcloud/community/video-seo.tsx @@ -162,9 +162,10 @@ function meetingUrlForTranscript(siteUrl: string, permalink: string): string { * result as of June 2025). * * Transcript page (M8 — Article): - * - Article with transcribes link back to the VideoObject's @id, plus - * mentions: [Person] for speakers and about/mentions Thing entities - * from M12's dictionary. + * - Article with `associatedMedia` link to the VideoObject's @id (the + * transcript's source recording), `isPartOf` a CreativeWorkSeries for + * the wasmCloud Community Meeting collection, plus mentions: [Person] + * for speakers and about/mentions Thing entities from M12's dictionary. */ export default function VideoSEO({ metadata, @@ -330,14 +331,18 @@ export default function VideoSEO({ inLanguage: VIDEO_LANGUAGE, url: canonicalUrl, mainEntityOfPage: canonicalUrl, - // Link back to the canonical video entity. Google understands this - // as "this text transcribes that video" and treats the meeting page - // as the canonical AV asset. - transcribes: { '@id': videoObjectId }, + // Link back to the canonical video entity via `associatedMedia` — + // a valid Article property whose range is MediaObject (VideoObject + // qualifies). Schema.org does not define a `transcribes` property, + // so we don't emit it; the prose context + the video reference + // here carry the relationship. + associatedMedia: { '@id': videoObjectId }, // Series-level grouping (every weekly meeting is part of the - // wasmCloud Community Meeting series) + // wasmCloud Community Meeting series). Use `CreativeWorkSeries` + // — schema.org has no bare `Series` type; `CreativeWorkSeries` + // is the correct CreativeWork-subtype expected by `isPartOf`. isPartOf: { - '@type': 'Series', + '@type': 'CreativeWorkSeries', name: 'wasmCloud Community Meeting', url: `${siteUrl}/community/`, }, From 69d07b0283efc45bad38e5ec6ef093b9c3af44ea Mon Sep 17 00:00:00 2001 From: Liam Randall Date: Sat, 6 Jun 2026 15:38:52 -0400 Subject: [PATCH 2/3] tweaking for google rich results tests Signed-off-by: Liam Randall --- src/theme/wasmcloud/blog/blog-post-schema.tsx | 7 ++++++- src/theme/wasmcloud/community/post-page/index.tsx | 10 ++++++++-- src/theme/wasmcloud/community/video-seo.tsx | 4 ++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/theme/wasmcloud/blog/blog-post-schema.tsx b/src/theme/wasmcloud/blog/blog-post-schema.tsx index 048716190..a4ce38693 100644 --- a/src/theme/wasmcloud/blog/blog-post-schema.tsx +++ b/src/theme/wasmcloud/blog/blog-post-schema.tsx @@ -212,7 +212,12 @@ export default function BlogPostSchema(): JSX.Element | null { publisher: PUBLISHER_REF, ...(datePublished && { datePublished }), ...(dateModified && { dateModified }), - ...(authors && { author: authors }), + // `author` is technically optional on Article-family schemas, but + // Google's Rich Results Test flags its absence on every Article it + // sees. Community transcript pages don't carry personal `authors:` + // in frontmatter — for those we fall back to the wasmCloud project + // org so the field is always present and the rich-result is eligible. + author: authors ?? PUBLISHER_REF, ...(image && { image }), ...(keywords && { keywords }), ...(wordCount !== undefined && { wordCount }), diff --git a/src/theme/wasmcloud/community/post-page/index.tsx b/src/theme/wasmcloud/community/post-page/index.tsx index 1aa95212c..f2c30ee5e 100644 --- a/src/theme/wasmcloud/community/post-page/index.tsx +++ b/src/theme/wasmcloud/community/post-page/index.tsx @@ -4,7 +4,6 @@ import Head from '@docusaurus/Head'; import { HtmlClassNameProvider, ThemeClassNames } from '@docusaurus/theme-common'; import { BlogPostProvider, useBlogPost } from '@docusaurus/plugin-content-blog/client'; import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata'; -import BlogPostPageStructuredData from '@theme/BlogPostPage/StructuredData'; import TOC from '@theme/TOC'; import type { Props } from '@theme/BlogPostPage'; import Unlisted from '@theme/ContentVisibility/Unlisted'; @@ -14,6 +13,13 @@ import CommunityPostItem from '../post-item'; import Link from '@docusaurus/Link'; import VideoSEO from '../video-seo'; import MeetingSpeakers from '../meeting-speakers'; +// Replaces Docusaurus's default `` with our +// richer BlogPostSchema so community pages emit the same Article-family +// payload (with author/publisher defaulted to the wasmCloud org, an image +// derived from frontmatter, and entity-graph mentions) as the blog. Without +// this, the default emits `author: []` on transcript pages and Google's +// Rich Results Test reports "Missing field 'author'". +import BlogPostSchema from '../../blog/blog-post-schema'; // Community meetings & transcripts older than 2025 are no longer current. Tell // search engines not to index them, but keep `follow` so they still pass link @@ -87,7 +93,7 @@ export default function CommunityPostPage(props: Props): JSX.Element { className={clsx(ThemeClassNames.wrapper.blogPages, ThemeClassNames.page.blogPostPage)} > - + diff --git a/src/theme/wasmcloud/community/video-seo.tsx b/src/theme/wasmcloud/community/video-seo.tsx index dd2c3ccff..39ecbba03 100644 --- a/src/theme/wasmcloud/community/video-seo.tsx +++ b/src/theme/wasmcloud/community/video-seo.tsx @@ -328,6 +328,10 @@ export default function VideoSEO({ // personally authored, the project entity is the appropriate author. author: VIDEO_PUBLISHER, publisher: VIDEO_PUBLISHER, + // Article rich-result eligibility wants an `image`. The YouTube + // maxresdefault thumbnail is the canonical preview for this + // transcript (it shares its source video with the meeting page). + image: thumbnailUrl, inLanguage: VIDEO_LANGUAGE, url: canonicalUrl, mainEntityOfPage: canonicalUrl, From 9f35d91ae504f830c7696146ad3ecb04e3e044a9 Mon Sep 17 00:00:00 2001 From: Liam Randall Date: Sat, 6 Jun 2026 18:02:26 -0400 Subject: [PATCH 3/3] updating template generator Signed-off-by: Liam Randall --- community/2026-05-27-community-meeting.mdx | 12 ++ package.json | 3 + scripts/generate-transcript-inheritance.mjs | 106 ++++++++++++++++++ scripts/validate-structured-data.mjs | 13 ++- src/data/transcript-inheritance.json | 21 ++++ src/theme/wasmcloud/blog/blog-post-schema.tsx | 30 ++++- src/theme/wasmcloud/community/video-seo.tsx | 33 +++++- 7 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 scripts/generate-transcript-inheritance.mjs create mode 100644 src/data/transcript-inheritance.json diff --git a/community/2026-05-27-community-meeting.mdx b/community/2026-05-27-community-meeting.mdx index 16329020a..4cbfbcd5d 100644 --- a/community/2026-05-27-community-meeting.mdx +++ b/community/2026-05-27-community-meeting.mdx @@ -32,6 +32,18 @@ image: https://i.ytimg.com/vi/QyVyD37cvrw/maxresdefault.jpg duration: 3252 showTitle: true slug: 2026-05-27-community-meeting +about: wasmCloud +mentions: + - ComponentModel + - WASIPreview3 + - WASI + - JCO + - ComponentizeJS + - StarlingMonkey + - Wasmtime + - BytecodeAlliance + - Java + - JavaScript speakers: - bailey-hayes - liam-randall diff --git a/package.json b/package.json index cd0e50322..4f566a36f 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,11 @@ "license": "Apache-2.0", "scripts": { "docusaurus": "docusaurus", + "prestart": "node scripts/generate-transcript-inheritance.mjs", "start": "docusaurus start --config docusaurus.config.ts --no-open", + "prebuild": "node scripts/generate-transcript-inheritance.mjs", "build": "docusaurus build", + "generate:transcript-inheritance": "node scripts/generate-transcript-inheritance.mjs", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", diff --git a/scripts/generate-transcript-inheritance.mjs b/scripts/generate-transcript-inheritance.mjs new file mode 100644 index 000000000..680e83447 --- /dev/null +++ b/scripts/generate-transcript-inheritance.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * Build-time generator for `src/data/transcript-inheritance.json`. + * + * Community meetings have TWO pages each: a landing/watch page + * (`-community-meeting.mdx`) and a transcript page + * (`-community-meeting-transcript.mdx`). The landing page's + * frontmatter carries `about:` + `mentions:` entity references from the + * M12 dictionary; the transcript page covers the same content but its + * frontmatter typically does not repeat those refs. Without inheritance, + * the transcript's Article JSON-LD lacks the entity graph and trips the + * M10 validator's `no about or mentions` warning. + * + * This script reads every landing page's frontmatter and emits a single + * JSON map keyed by the transcript permalink (derived deterministically + * from the landing slug). `video-seo.tsx` imports the map at module + * load and applies the inherited refs when rendering a transcript page + * whose own frontmatter doesn't carry them. + * + * Idempotent. Safe to run before every build (it's wired into + * package.json's `prebuild` script). + */ +import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import yaml from 'js-yaml'; + +const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const COMMUNITY_DIR = join(REPO_ROOT, 'community'); +const OUT_PATH = join(REPO_ROOT, 'src', 'data', 'transcript-inheritance.json'); + +const LANDING_MDX_RE = /^(\d{4}-\d{2}-\d{2})-community-meeting\.mdx$/; +const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/; + +/** + * Extract about/mentions from a landing page's frontmatter. Returns an + * empty result when neither field is present so the caller can decide + * whether to emit anything. + */ +async function readLandingRefs(path) { + const raw = await readFile(path, 'utf8'); + const m = FRONTMATTER_RE.exec(raw); + if (!m) return null; + let fm; + try { + fm = yaml.load(m[1]); + } catch (e) { + console.warn(`[transcript-inheritance] yaml parse failed for ${path}: ${e.message}`); + return null; + } + if (!fm || typeof fm !== 'object') return null; + const out = {}; + if (typeof fm.about === 'string' && fm.about) out.about = fm.about; + if (Array.isArray(fm.mentions)) { + const cleaned = fm.mentions.filter((m) => typeof m === 'string' && m.length > 0); + if (cleaned.length > 0) out.mentions = cleaned; + } + return Object.keys(out).length > 0 ? out : null; +} + +async function main() { + const entries = await readdir(COMMUNITY_DIR); + const map = {}; + let scanned = 0; + let withRefs = 0; + + for (const entry of entries.sort()) { + const match = LANDING_MDX_RE.exec(entry); + if (!match) continue; + scanned += 1; + const date = match[1]; + const refs = await readLandingRefs(join(COMMUNITY_DIR, entry)); + if (!refs) continue; + withRefs += 1; + // Key by the transcript page's canonical permalink. The transcript + // slug is the landing slug with `-transcript` appended (set by + // `slug:` in the transcript MDX frontmatter, mirrored by the + // `transcriptUrlForMeeting` helper in video-seo.tsx). + const transcriptPermalink = `/community/${date}-community-meeting-transcript/`; + map[transcriptPermalink] = refs; + } + + await mkdir(dirname(OUT_PATH), { recursive: true }); + await writeFile( + OUT_PATH, + JSON.stringify( + { + $comment: + 'Auto-generated by scripts/generate-transcript-inheritance.mjs (prebuild step). Edits will be overwritten. Maps transcript permalink → { about, mentions } from the parent landing page\'s frontmatter so video-seo.tsx can inherit entity refs.', + generated_at: new Date().toISOString(), + entries: map, + }, + null, + 2, + ) + '\n', + ); + + console.log( + `[transcript-inheritance] scanned ${scanned} landing pages, wrote ${withRefs} entries → ${OUT_PATH.replace(REPO_ROOT + '/', '')}`, + ); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/validate-structured-data.mjs b/scripts/validate-structured-data.mjs index f440f0e5e..741c1f0b2 100644 --- a/scripts/validate-structured-data.mjs +++ b/scripts/validate-structured-data.mjs @@ -190,8 +190,19 @@ function validatePayload(payload, ctx) { return { errors, warnings }; } +// Pages that opt out of search indexing with `` +// are intentionally not eligible for rich results. Soft warnings about authoring +// gaps (e.g. missing M12 about/mentions) provide zero SEO value on these pages, +// so we suppress them. Hard schema-validity errors still fire — invalid JSON-LD +// is invalid regardless of indexability. +// Tolerate other attributes before `name=` (e.g. react-helmet emits +// `data-rh=true name=robots content=...`) and quoted/unquoted attribute +// values. The key signal is `name=robots` + `content` containing `noindex`. +const NOINDEX_META_RE = /]*\bname=["']?robots["']?[^>]*\bcontent=["'][^"']*noindex/i; + async function validateFile(htmlPath) { const html = await readFile(htmlPath, 'utf8'); + const isNoindex = NOINDEX_META_RE.test(html); const errors = []; const warnings = []; let count = 0; @@ -209,7 +220,7 @@ async function validateFile(htmlPath) { } const r = validatePayload(parsed, `${htmlPath} script #${count}`); errors.push(...r.errors); - warnings.push(...r.warnings); + if (!isNoindex) warnings.push(...r.warnings); } return { count, errors, warnings }; } diff --git a/src/data/transcript-inheritance.json b/src/data/transcript-inheritance.json new file mode 100644 index 000000000..64368f488 --- /dev/null +++ b/src/data/transcript-inheritance.json @@ -0,0 +1,21 @@ +{ + "$comment": "Auto-generated by scripts/generate-transcript-inheritance.mjs (prebuild step). Edits will be overwritten. Maps transcript permalink → { about, mentions } from the parent landing page's frontmatter so video-seo.tsx can inherit entity refs.", + "generated_at": "2026-06-06T22:00:12.383Z", + "entries": { + "/community/2026-05-27-community-meeting-transcript/": { + "about": "wasmCloud", + "mentions": [ + "ComponentModel", + "WASIPreview3", + "WASI", + "JCO", + "ComponentizeJS", + "StarlingMonkey", + "Wasmtime", + "BytecodeAlliance", + "Java", + "JavaScript" + ] + } + } +} diff --git a/src/theme/wasmcloud/blog/blog-post-schema.tsx b/src/theme/wasmcloud/blog/blog-post-schema.tsx index a4ce38693..7faa18a74 100644 --- a/src/theme/wasmcloud/blog/blog-post-schema.tsx +++ b/src/theme/wasmcloud/blog/blog-post-schema.tsx @@ -6,6 +6,14 @@ import { buildEntityNodes, buildEntityRefs, } from '@theme/wasmcloud/structured-data/entities'; +// Community transcript pages inherit about/mentions from their parent +// landing page via this map (see video-seo.tsx + the prebuild generator). +// On non-community pages and on landing pages the map is unused. +import transcriptInheritance from '@site/src/data/transcript-inheritance.json'; + +type InheritedRefs = { about?: string; mentions?: string[] }; +const TRANSCRIPT_INHERITANCE: Record = + (transcriptInheritance as { entries?: Record }).entries ?? {}; /** * Per M2 of the structured-data spike: emit the full Article-family schema @@ -187,8 +195,26 @@ export default function BlogPostSchema(): JSX.Element | null { metadata, siteUrl, ); - const entityRefs = buildEntityRefs(frontMatter as Record); - const entityNodes = buildEntityNodes(frontMatter as Record); + // Community transcript pages don't carry their own about/mentions + // (those refs live on the parent landing page). Inherit from the + // prebuild-generated map so the transcript's BlogPosting/Article + // node carries the same entity graph as its meeting page. + const permalinkWithSlash = permalink.endsWith('/') ? permalink : permalink + '/'; + const inherited = + TRANSCRIPT_INHERITANCE[permalink] || TRANSCRIPT_INHERITANCE[permalinkWithSlash]; + const fmForRefs: Record = inherited + ? { + ...(frontMatter as Record), + ...(((frontMatter as { about?: unknown }).about === undefined && + inherited.about) ? { about: inherited.about } : {}), + ...(!Array.isArray((frontMatter as { mentions?: unknown }).mentions) && + inherited.mentions + ? { mentions: inherited.mentions } + : {}), + } + : (frontMatter as Record); + const entityRefs = buildEntityRefs(fmForRefs); + const entityNodes = buildEntityNodes(fmForRefs); // M2 risk #12: Speakable is restricted to NewsArticle. Honor the // `speakable: true` frontmatter only when the post is typed NewsArticle. diff --git a/src/theme/wasmcloud/community/video-seo.tsx b/src/theme/wasmcloud/community/video-seo.tsx index 39ecbba03..41d8224a8 100644 --- a/src/theme/wasmcloud/community/video-seo.tsx +++ b/src/theme/wasmcloud/community/video-seo.tsx @@ -3,10 +3,19 @@ import Head from '@docusaurus/Head'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import type { BlogPostMetadata } from '@docusaurus/plugin-content-blog'; import speakersData from '@site/src/data/speakers.json'; +// Auto-generated at prebuild time by scripts/generate-transcript-inheritance.mjs. +// Maps each transcript permalink to its parent landing page's about/mentions +// frontmatter so transcript Article JSON-LD inherits the same entity graph +// without the author having to duplicate the refs across both files. +import transcriptInheritance from '@site/src/data/transcript-inheritance.json'; import JsonLd from '@theme/wasmcloud/json-ld'; import { buildEntityRefs } from '@theme/wasmcloud/structured-data/entities'; import { isTranscriptPermalink } from './utils'; +type InheritedRefs = { about?: string; mentions?: string[] }; +const TRANSCRIPT_INHERITANCE: Record = + (transcriptInheritance as { entries?: Record }).entries ?? {}; + type Chapter = { seconds: number; label: string }; type SpeakerPerson = { @@ -195,7 +204,29 @@ export default function VideoSEO({ const keywords = getKeywords(frontMatter); const speakers = buildSpeakers(frontMatter); - const entityRefs = buildEntityRefs(frontMatter); + // Transcript pages typically don't repeat `about:` / `mentions:` in their + // own frontmatter — those refs live on the parent landing page. When this + // is a transcript page and its own frontmatter omits one of the refs, fall + // back to the inheritance map (generated at prebuild from landing-page + // frontmatter; see scripts/generate-transcript-inheritance.mjs). Avoids the + // M10 validator warning `no about or mentions (M12 entity refs)` on + // transcript Article nodes. + const permalinkWithSlash = permalink.endsWith('/') ? permalink : permalink + '/'; + const inheritedRefs: InheritedRefs | undefined = isTranscript + ? TRANSCRIPT_INHERITANCE[permalink] || TRANSCRIPT_INHERITANCE[permalinkWithSlash] + : undefined; + const frontMatterForRefs = inheritedRefs + ? { + ...frontMatter, + ...(frontMatter.about === undefined && inheritedRefs.about + ? { about: inheritedRefs.about } + : {}), + ...(!Array.isArray(frontMatter.mentions) && inheritedRefs.mentions + ? { mentions: inheritedRefs.mentions } + : {}), + } + : frontMatter; + const entityRefs = buildEntityRefs(frontMatterForRefs); // Stable IDs the schemas use to cross-reference each other: // VideoObject @id → canonical meeting URL + #video