Minor release adding three closely-related capabilities driven by a new
/ai/* content surface at stackql.io: authors write normal Docusaurus
frontmatter and the plugin emits the right schema with no in-page <script>
blocks or <meta> workarounds.
Breaking changes: none. All v1.4.x mechanisms (script tags, meta tags,
hardcoded /docs/* -> TechArticle) keep working unchanged. The frontmatter
path is purely additive.
Added:
-
themeConfig.structuredData.techArticleRoutePrefixesoption for configuring which route prefixes emitTechArticle. Defaults to['/docs/'](unchanged behavior for 1.4.x users). Each prefix must start and end with/. A landing route like/docsor/ai(no trailing slash) does not match the corresponding/docs/or/ai/prefix and so stays a plainWebPage, preserving the 1.4.x carve-out. -
Frontmatter-driven FAQ / HowTo / SoftwareApplication emission. Consumers can declare
faq,howTo, andsoftwareApplicationin page frontmatter as an alternative to the in-MDX<script type="application/json">block:--- title: What is StackQL? faq: - question: Is StackQL a database? answer: No. StackQL is a query runtime ... ---
When both the frontmatter path and the script-tag path are present on the same page for the same schema, the frontmatter wins and a verbose log line names the duplicate.
-
Frontmatter-driven
proficiencyLevel,dependencies, andspeakablefields. The<meta name="aeo:...">-tag paths stay supported; frontmatter wins on conflict.speakable: falsein frontmatter opts the page out;speakable: { cssSelector }orspeakable: { xpath }overrides the default selectors for that page only.
Internal:
- Plugin now uses Docusaurus's
allContentLoadedlifecycle hook to capture per-route frontmatter (keyed by normalized permalink), in addition to its existingpostBuildHTML scan. Tolerates multi-version docs, multiple content plugin instances, and missing branches in theallContenttree (custom JSX pages, redirect stubs, plugin-generated routes). No consumer impact. contentLoadeddoes not receiveallContentin Docusaurus 3.x; onlyallContentLoadeddoes. Using the right hook from the start.softwareApplication: trueis now accepted (as well as an object) for bare opt-in with no extra fields. Previously thesoftwareApplicationvalidator rejected non-object values, which would have been awkward in frontmatter wheretrueis the obvious "yes, emit this" idiom.
Bugfix patch on top of 1.4.0. Three issues surfaced when a real consumer (stackql.io) wired up 1.4.0; all are fixed here. No new features, no config-shape changes, no dependency bumps.
Breaking changes: none.
Fixed:
trailingSlash: falsesites silently skipped flat-routed pages with sibling subdirectories. WithtrailingSlash: false, Docusaurus emits e.g.build/docs.htmlalongside abuild/docs/directory housing the nested routes (/docs/foo,/docs/bar, ...). The previous file-path resolver preferred the directory form, resolved/docstobuild/docs/index.html(which does not exist undertrailingSlash: false), and silently skipped the route. On stackql.io this dropped 11 landing pages including/docsand/blog. The resolver now prefers the flatoutDir/route.htmlform when it exists, falls back tooutDir/route/index.html, and special-cases the root route. Existence-based (notsiteConfig.trailingSlash-based) so it stays correct under per-page frontmatter overrides in Docusaurus 3.TypeError: Cannot read properties of null (reading 'content')when a page lacks a<meta name="description">(e.g. a redirect-stub homepage). The description meta is now read defensively and falls back tositeConfig.tagline(then to""). The same node-then- fallback pattern is applied to<title>, which falls back tositeConfig.title. Pages without metadata still emit the fullWebPage/BreadcrumbList/WebSite/Organizationcore graph rather than being skipped.- README config example included
dunson theOrganizationblock.dunsis defined onLocalBusiness(and its subtypes), not onOrganization, so the schema.org Validator flags it (Google's Rich Results Test is permissive and does not).dunsis removed from the example and the README now explains how to narrow@typetoLocalBusinessfor sites that need it.taxIDstays - it is valid onOrganization.
AEO (Answer Engine Optimization) release. Adds opt-in support for the schema
types that AI search surfaces (Google AI Overviews, Perplexity, ChatGPT search,
Claude web tools) actually consume, plus connected @graph linking between the
primary page entity and any secondary entities.
Breaking changes: none. All additions are opt-in via frontmatter / injected
script blocks or themeConfig.structuredData keys that did not previously
exist. Existing sites get one behavior change only: a speakable property is
added to the WebPage node by default. Opt out per-page with
<meta name="aeo:speakable" content="false"> or globally with
themeConfig.structuredData.speakable: false.
Added:
FAQPageemission. Page injects a<script type="application/json" data-aeo-faq>block containing[{question, answer}, ...]. Plugin emits aFAQPagenode withQuestion/Answerchildren, each with stable@idvalues.HowToemission. Page injects<script type="application/json" data-aeo-howto>with{name, totalTime?, estimatedCost?, description?, steps: [{name, text, url?, image?}]}.TechArticlefor/docs/*routes (in place of genericArticle), with optionalproficiencyLevelanddependenciessourced from<meta name="aeo:proficiencyLevel">and<meta name="aeo:dependencies">./blog/*keeps emittingArticleexactly as before.SoftwareApplicationper-page (opt-in). Page injects<script type="application/json" data-aeo-software>with at minimum a name plus any ofapplicationCategory,applicationSubCategory,operatingSystem,featureList,softwareVersion,downloadUrl,offers.SpeakableSpecificationon everyWebPagenode by default. Override viathemeConfig.structuredData.speakable.cssSelector(array) orxpath(array). Default CSS selectors:["h1", "article p:first-of-type", "[data-speakable]"].- Graph linking. When a page emits a secondary entity, the primary entity
(
Article/TechArticle, orWebPagefor non-article pages) gets amainEntitypointer to the secondary@id. Priority:HowTo->FAQPage->SoftwareApplication. - Build-time validation. Malformed
faq,howTo, orsoftwareApplicationpayloads throw an error naming the route, the field, and what was expected. Previously the plugin only validated thatthemeConfig.structuredDataexisted.
Internal:
- Hoisted route-independent work (skip-route check, default
inLanguage/datePublishedresolution, speakable spec construction) out of the per-route.map()callback. - Defensive guard around missing
structuredData.authors[name]entries - previously the plugin threwCannot read property 'authorId' of undefinedon a blog post whose author URL was not in the authors map. Now it skips theauthor/Personnodes for that post and continues. Working author configs are unaffected.