Project guide for Claude Code working in this repository. Tells you what this site is, how the AEO platform is wired together, where the dependencies live, and the conventions to follow.
stackql.io is the marketing and documentation site for StackQL, built on Docusaurus 3.10. Three audiences:
- Humans - default site nav. This is a docs-only site: the docs tree is the site root (
/installing-stackql,/providers,/getting-started/*,/command-line-usage/*,/mcp/*,/language-spec/*,/developers/*,/quick-starts/*) and docs/index.md is the homepage. Blog at/blog/<section>/*(three sections, see "The blog" below). Two top-level React pages remain:/privacyand/contact-us(a real page: community channels, email, phone and address). There are no meta-refresh stub pages: every alias URL is a Netlify 301 (see "Redirects are Netlify 301s, never pages" below). - AI agents and answer engines - a parallel content surface at
/ai/*(canonical definitions, comparisons, how-tos, FAQs, troubleshooting, etc.) reachable by deep link, viallms.txtand from one "AI Reference" link in the footer, but not from the header nav. The footer link exists so the tree has a crawl path: with no internal link at all Google left its 44 sitemap URLs as "Discovered - currently not indexed". - LLM crawlers -
llms.txtandllms-full.txtat site root; a plain-markdown twin (/foo.md) for every doc and blog page, advertised from each page by<link rel="alternate" type="text/markdown">.
The two surfaces serve the same URLs to all visitors (no UA-based cloaking). The /ai/* pages just don't appear in the header nav.
Every alias or retired URL on this site is a [[redirects]] rule in netlify.toml, not a built page. Do not add a src/pages stub or a plugin route that renders <meta http-equiv="refresh">: a meta-refresh page is a 200 that the sitemap plugin lists and that Google reports as "Page with redirect" (Search Console flagged every one of them in October 2026), and it carries conflicting signals (robots: index,follow, a canonical, and a refresh). An edge 301 is neither built nor listed. The former stubs (/install, /downloads, /stackqldocs, /tutorials, /cookbooks, /stackql-deploy -> stackql-deploy.io, /features -> /, /providers/<slug>, /registry/<name>) are all rules now; the navbar and footer link to stackql-deploy.io directly. /contact-us was a stub to GitHub Discussions and is a real page now (src/pages/contact-us.js, data in src/data/contact-us/index.js): agents landing on it read contact details as text rather than an empty page.
Two blocks of rules are generated and must not be edited by hand:
BEGIN/END generated provider redirects- scripts/generate-provider-redirects.js, from the provider catalog. Rerun after changing src/configs/providers.json.BEGIN/END generated blog redirects- scripts/generate-blog-redirects.js, from blog front matter slugs. See "The blog".
Netlify serves a matching static file in preference to a rule unless the rule has force = true, so a rule only works for a path that is not also built. If something must link to an alias path from inside the site (it should not need to), use pathname:// so onBrokenLinks: 'throw' does not reject it.
- Docusaurus 3.10.1, classic preset
- React 18 (pinned - several deps require it)
- MUI 5/6 (already in deps - Material UI and emotion)
- Node 22 toolchain, yarn is the package manager (yarn.lock is the only lockfile - package-lock.json is gitignored)
- Deployed via Netlify, build =
npm run build, publish =build/ - Search via Algolia DocSearch (
ALGOLIA_APP_ID,ALGOLIA_API_KEY,ALGOLIA_INDEX_NAMEenv vars required for prod builds)
The classic preset's docs instance has routeBasePath: '/' (Docusaurus docs-only mode). docs/index.md carries slug: / and is the homepage; there is no src/pages/index.js. The marketing homepage was collapsed into the docs site in August 2025, and until October 2026 the root was a meta-refresh stub to /docs, which is why Google indexed /docs as the home and showed no sitelinks.
- Inbound
/docs/*links are Netlify 301s to the same path without the prefix (/docs->/,/docs.md->/index.md,/docs/*->/:splat). Those rules sit below the/docs/query-library/*proxy and the specific legacy/docs/...rules in netlify.toml. Netlify applies the first matching rule, so keep that order. - The query library stays proxied at
/docs/query-library/*: it is a separate site whosebaseUrlis a cross-repo contract (see "The query library" below). It is the only thing left under/docs. - Internal links use root paths (
/installing-stackql, never/docs/installing-stackql).onBrokenLinks: 'throw'catches relative mistakes; absolutestackql.io/docs/...links need a grep. - Root-level routes that are not docs:
/blog/*,/ai/*,/privacy,/contact-us,/search, and the Netlify 301 aliases (/install,/downloads,/stackqldocs,/stackql-deploy,/features,/tutorials,/cookbooks,/providers/<slug>,/registry/*). A new doc whose slug collides with one of these would clash with it (a built file shadows a Netlify rule), so check before adding root-level docs. - Getting Started and Command Line Usage are sidebar categories with generated index pages (
/getting-started,/command-line-usage), so every major section has a landing page Google can surface as a sitelink: installation, providers, getting started, command line usage, MCP.
Two custom plugins published to npm and one secondary content-docs instance handle the AEO pipeline. Both plugins are in sibling repos and developed alongside this site.
Source: ../docusaurus-plugin-structured-data/ (sibling to this repo)
Published: npm registry, current version pinned in package.json
Lifecycle: postBuild + allContentLoaded
Emits JSON-LD <script type="application/ld+json"> blocks into the <head> of every emitted HTML page. The shape:
WebPage+BreadcrumbList+WebSite+Organizationon every pageArticle+ImageObject+Personon blog posts (/blog/<section>/<slug>). Breadcrumbs follow the blog instance base path (Home > Blog > Section > Post) andArticle.articleSectionis['Blog', '<Section>']- needs plugin >= 1.6.0TechArticleon every doc page of the default andaidocs instances (techArticleDocsInstances: ['default', 'ai'], plugin >= 1.6.0), except the instance roots/and/ai, which stayWebPage.techArticleRoutePrefixes: ['/ai/']is kept so older plugin versions still mark/ai/*.FAQPage,HowTo,SoftwareApplicationopt-in via frontmatter (faq:,howTo:,softwareApplication:). The homepage setssoftwareApplication: true, which emits aSoftwareApplicationfrom the site defaults inthemeConfig.structuredData.softwareApplication(name,DeveloperApplication, OS, MIT license URL, download URL, freeOffer).SoftwareSourceCode(plugin >= 1.7.0) in every page's graph fromthemeConfig.structuredData.softwareSourceCode(repository, Go, MIT, platforms), referenced fromWebSite.about.- Dates (plugin >= 1.7.0):
dateModifiedis the doc's git last-update time (showLastUpdateTime: trueon the docs plugin) or the blog post's date; the build time only for pages with no date of their own (generated category indexes, React pages).datePublishedis the post date for blog posts andwebpage.datePublished(2021-07-01) for the rest. SpeakableSpecificationon every WebPage with default selectors- Connected
@graphviamainEntitylinking (TechArticle.mainEntity -> FAQPage when both present)
Config lives at themeConfig.structuredData in docusaurus.config.js. Key settings:
techArticleDocsInstancesandtechArticleRoutePrefixes- which pages get TechArticle (see above)excludedRoutes: []- nothing is excluded (exact routes or globs are accepted);/providersused to be a custom React grid and is a doc page todayauthors:- blog author identity graphorganization:- StackQL Studios identity, contact point (contactType: 'customer support', email, telephone,url: /contact-us; the plugin warns at build time ifcontactTypeis missing), address (postcode 3000, the same as src/data/contact-us/index.js), and a square 512px logo (Google wants a logo mark, not a cover image)softwareSourceCode:andsoftwareApplication:- the product blocks described above. The plugin takes no plugin options; everything is underthemeConfig.structuredData, and an option in thepluginsarray fails the build.breadcrumbLabelMap:- friendly names for breadcrumb segments. Keys are single segments ('quick-starts') or full paths ('/blog/providers'), and a full path wins. Ancestor segments only become linked crumbs when a page exists at that path; otherwise they fold into the leaf name.
If a page has no <meta name="description">, the plugin falls back to siteConfig.tagline. The homepage explicitly sets a description meta in docusaurus.config.js themeConfig.metadata to avoid the fallback on the most-cited page.
Source: ../docusaurus-plugin-aeo/ (sibling to this repo)
Published: npm registry, current version pinned in package.json
Lifecycle: postBuild + allContentLoaded + theme component injection
Four features:
.mdcompanion files - for every doc and blog page, emits a sibling.mdat the same path (e.g./foo->/foo.md,/->/index.md). Since plugin 0.5.0 the file is plain markdown rendered from the MDX source (imports, exports, expressions and presentational JSX removed;<Tabs>become bold labels; download-link components become links; everything else unwrapped and dedented), with# title,> descriptionandSource: <page URL>at the top. Only emits when there is a source markdown file (React pages and auto-generated index routes are correctly skipped - they have no source to mirror). Every page that has a companion carries<link rel="alternate" type="text/markdown" href="...">in its head, inserted inpostBuild. Note that docs/providers.md is entirely JSX rendered from the catalog, so its companion is title, description and Source only.llms.txt+llms-full.txt- at site root.llms.txtis the corpus index (Markdown bullet list with title and description per page, absolute links) and ends with an## Optionalsection linkingllms-full.txt.llms-full.txtis the concatenated companions of the AI Reference and Documentation instances only (llmsTxt.fullTxt.include), separated by\n\n---\n\n; agents read a bounded prefix of it, so the blog posts stay inllms.txtbut out of the full text.- "Ask AI" dropdown - swizzled into the breadcrumb row of every doc page and the header of every blog post, and rendered only on pages that have a companion (generated category index pages have none). The prompt links the companion by the same rule the file is written (
{companionUrl}). MUI outlined Button + Menu. Three providers: Claude, ChatGPT, Perplexity (Gemini was removed - it does not accept URL-encoded prompts). Brand icons are hand-rolled inline SVGs insrc/theme/AskAiButton/brand-icons/to avoid React-version conflicts with icon libraries. /ai/*helpers - exported from@stackql/docusaurus-plugin-aeo/helpers. Used to integrate the/ai/*content surface with the structured-data plugin.
Config lives at the plugin options object in the plugins array in docusaurus.config.js:
llmsTxt.instanceSections- section titles + ordering for thellms.txtindex (AI Reference -> Documentation -> one "Blog -" section per blog instance, generated from blogSections)askAi.providerOrder- dropdown ordering (defaults to claude, chatgpt, perplexity)askAi.promptTemplate- the prefilled prompt sent to the AI surface. Default is self-contained ("Read {companionUrl} and help me understand it. Summarize the key points, then ask me one clarifying question to dig deeper."). The user can edit it before submitting.llmsTxt.fullTxt.include- the content instances concatenated intollms-full.txt(AI Reference and Documentation).
A second @docusaurus/plugin-content-docs instance with id: 'ai', routeBasePath: '/ai', path: 'ai-content', and sidebarPath: false. Configured in docusaurus.config.js.
Directory structure under ai-content/:
ai-content/
├── index.md # /ai landing
├── canonical-definitions/ # "What is X?" pages
├── comparisons/ # "StackQL vs Y" pages
├── how-tos/ # task guides
├── concepts/ # design rationale + best practices
├── faqs/ # topic-grouped Q&A
├── architecture/ # internals
├── troubleshooting/ # error -> resolution
├── industry-positioning/ # where StackQL fits
├── tutorials/ # end-to-end walkthroughs
└── providers/ # auto-generated per-provider reference (placeholder)
Each section has an index.md landing. Individual reference pages go directly inside each section dir.
sidebarPath: false is what keeps these out of the human nav. They're reachable via direct URL, the sitemap, and llms.txt.
The preset blog is disabled (blog: false). Three @docusaurus/plugin-content-blog instances are generated from the blogSections array near the top of docusaurus.config.js (GitHub issue #288). A tag-based split was ruled out because the blog sidebar is built per instance and cannot be filtered by tag.
| Instance id | Content dir | Routes | Nav label |
|---|---|---|---|
product |
blog/product/ |
/blog/product/* |
Product Announcements |
providers |
blog/providers/ |
/blog/providers/* |
Provider Announcements |
tutorials |
blog/tutorials/ |
/blog/tutorials/* |
Tutorials |
- Each instance has its own list page, sidebar (
blogSidebarCount: 'ALL'), tags, pagination and feeds (/blog/<id>/rss.xml,atom.xml,feed.json). blog/authors.yml is shared by all three viaauthorsMapPath: '../authors.yml'. - New posts go straight into the section directory. The directory is the type - there is no marker tag. Slugs are set in front matter as before and must be unique across all three sections (the redirect generator checks this).
/blogis a landing page built by the local plugin plugins/blog-landing/index.js. It reads the three instances inallContentLoaded(the only hook that sees other plugins' content) and adds a route rendering src/components/BlogLanding/index.jsx with the newest five posts per section.- Every pre-split post URL (
/blog/<slug>and its.mdcompanion) has a 301 to its new home in netlify.toml. The per-post block between theBEGIN/END generated blog redirectsmarkers is owned by scripts/generate-blog-redirects.js, which derives rules from front matter slugs. Rerun it only if a pre-split post's slug or section changes; posts written after the split never had an old URL and need no rule, so add each new post's slug toPOST_SPLIT_SLUGSin the script. Old/blog/tags/*,/blog/page/*,/blog/archiveand/blog/authorsgo to/blog; the old feed URLs go to the product announcements feeds. - A post file must live inside a section directory. A file at
blog/root belongs to no instance, is silently not built, and gets no redirect (one tutorial sat there for two weeks after the split and its old URL 404'd). - The navbar "More" dropdown and the footer list the three sections plus Quick Starts; both are derived from
blogSections. The two announcement entries carry a bullhorn (navLabel) in the header only. The/bloglanding page is deliberately not linked from the header or footer; it is reachable by URL and from the sitemap. - "Tutorials" in the nav means the blog section. The docs walkthroughs formerly at
/docs/tutorials/*are "Quick Starts" at/quick-starts/*(directorydocs/quick-starts/, sidebar category in sidebars.js). Old URLs are 301'd in netlify.toml, including/tutorials->/blog/tutorialsand/cookbooks->/quick-starts(both were meta-refresh React stubs, now deleted). - Sitemap:
lastmod: 'date'per URL from the source file's last git commit (changefreqandpriorityare off; crawlers ignore them).ignorePatternscover/blog/*/tags/**,/blog/*/page/**,/blog/*/archiveand/blog/*/authors(list pages, not content). Those same pages (about 300 tag pages and 20 paginated list pages across the three instances) are also markednoindex, followby the wrappers in src/theme/BlogTagsPostsPage, src/theme/BlogTagsListPage and src/theme/BlogListPage (page 2 onwards only; page 1 is the section landing), via src/theme/NoIndex. They are linked from every post, so Google crawls them regardless of the sitemap; the noindex stops them piling up under "Crawled - currently not indexed". - The
breadcrumbLabelMapentries for the section ids are generated fromblogSections, so JSON-LD breadcrumbs read "Product Announcements" rather than "product". - The shared nav used by the provider microsites lives in
../docusaurus-config(vendored by those sites at build time). Its Blog/Tutorials links must be kept in step with the main site nav.
netlify.toml sets MIME types and cache headers for the AEO files. Critical rules:
*.md->text/markdown; charset=utf-8(top-level and nested)/llms.txtand/llms-full.txt->text/plain; charset=utf-8- All three get
X-Robots-Tag: index, followandmax-age=300cache /providers.json(the published provider catalog, see "Provider catalog" below) ->application/json; charset=utf-8, the same 5-minute cache andAccess-Control-Allow-Origin: *
Without these rules Netlify serves .md as application/octet-stream (browsers download instead of display) and crawlers may skip it.
Redirect rules live in the same file and their order matters (first match wins): the query library proxy, then specific legacy /docs/... rules, then the /docs/* -> /:splat catch-all, then the hand-written top-level aliases (/tutorials, /cookbooks, /install, /downloads, /stackqldocs), then the generated provider block, then the remaining hand-written rules, then the generated per-post blog block. See "Redirects are Netlify 301s, never pages" above.
The StackQL query library lives in its own repo/site (query-library.stackql.io)
and is served under the canonical path https://stackql.io/docs/query-library/*
via a Netlify 200 proxy rewrite in netlify.toml. This repo
holds no library content, build tooling or CI - just the rewrite rule and a
navbar link to /docs/query-library/ (an href, not to, so it is a full
page load rather than a client-side route). Response headers for the proxied
path (content types, CORS, cache) come from the origin site, not this repo's
netlify.toml. The path 404s under yarn serve/yarn start - expected,
Netlify-only behaviour. Do not add files under static/docs/query-library/:
Netlify serves matching static files in preference to redirect rules, so any
file there would shadow the proxied site.
src/configs/providers.json is the single source of truth for everything provider-related on this site: config, not code. It is an array of categories, each with providers of { name, href, icon, deployedBy, repo, branch, invertOnDark?, registryAliases? }. deployedBy is the microsite's hosting platform, netlify or github (GitHub Pages); repo is the GitHub repository of the provider and its microsite as owner/name, and branch the branch the microsite is deployed from (today always the repository's default branch). None of the three is rendered on this site; they are published in /providers.json so cross-repo automation can tell where each microsite lives and how to redeploy it. The code that reads it is src/lib/providers.js, which validates it, derives each provider's slug and path and exposes PROVIDER_CATEGORIES, PROVIDERS, providerRoutes() and registryRoutes(). Nothing else in the repo holds provider lists; the former src/configs/providers-data.json, providers.ts and the unused ProviderCards component were removed. The catalog drives:
- the tiles and table of contents on docs/providers.md, which imports from
src/lib/providers. Tiles and TOC entries link straight to the microsite (href), in the same tab (customProps.target: '_self', honoured by the swizzled src/theme/DocCard), with no redirect hop. - the navbar "Providers" menu: a two-level dropdown (category -> provider, every entry, catalog order) rendered by the custom navbar item type
custom-providersDropdownin src/theme/NavbarItem/ProvidersDropdownNavbarItem/index.js and configured as the "Providers" entry in docusaurus.config.js. Category rows link to/providers#<category id>, provider rows straight to the microsite (hrefwithhtmlinstead oflabelso there is no external-link icon,target="_self",rel="noopener"so the microsite keeps the referrer). Desktop is a hover/focus flyout per category; the mobile sidebar gets nested collapsibles. - the published catalog at
/providers.json, written into the build by plugins/provider-catalog/index.js (postBuild only, so not undernpm run start). It is a cross-repo contract: the shared chrome in../docusaurus-config, vendored at build time by every provider microsite and by the query library, fetches it when those sites build and generates their own two-level Providers menu from it. The shape (version 1) is documented in the plugin. Adding a field is fine; renaming or removing one, or moving the file, breaks those builds, so bump the version and change the consumer first. Netlify serves it with CORS and a 5-minute cache. - two families of Netlify 301 rules to
https://<slug>-provider.stackql.io/, theBEGIN/END generated provider redirectsblock in netlify.toml written by scripts/generate-provider-redirects.js. They used to be Docusaurus routes rendering a meta-refresh page (plugins/provider-redirects,src/components/ProviderRedirect), which Search Console reported as "Page with redirect" for every entry; both were removed in October 2026./providers/<slug>is explicit: exactly one rule per catalog entry, no exceptions. Nothing inside the site links here any more (tiles and navbar go straight to the microsite); it is kept for inbound links and the old sitemap./registry/<name>is the inbound surface for external links: one rule per catalog entry plus each entry'sregistryAliases, so a provider family exposes one canonical inbound link (/registry/databricks-> the Databricks Account microsite)./providersitself is the catalog doc page (docs/providers.md) now that docs live at the root; the bare/registry301s to it.
The slug is derived from href, which must be exactly https://<slug>-provider.stackql.io/; the module throws at config load on a missing field, a malformed href or a duplicate slug or alias. To add a provider, add one entry to the JSON and run node scripts/generate-provider-redirects.js. Do not create pages under src/pages/providers or src/pages/registry: a built file there would shadow the Netlify rule for that path. The retired /providers/databricks URL is a hand-written 301 straight to the Databricks Account microsite (a single hop, not via /registry/databricks).
The provider microsites and the query library get their nav from ../docusaurus-config, which builds its own Providers menu from this site's /providers.json at build time (see the bullet above), so there is no featured list to keep in step any more. A new catalog entry shows up on those sites on their next build after this site deploys.
src/components/Gist/index.jsx - local replacement for the unmaintained react-gist package (was blocking React 18 upgrade). Drop-in compatible: same <Gist id="..." /> API. Used by two blog posts.
src/theme/DocItem/Footer/index.js - copy of the theme-classic doc footer with one addition: a doc with hide_last_update: true in its front matter drops the "Last updated" row while keeping tags and the edit link. Only the homepage uses it, so search results do not show a modification date on a landing page. showLastUpdateTime stays on globally. Keep this file in step with theme-classic when Docusaurus is upgraded.
src/theme/NavbarItem/ComponentTypes.js - wraps the theme-classic navbar item registry and adds the site's custom types. One so far: custom-providersDropdown (see "Provider catalog" above). Its desktop and mobile markup copy theme-classic's DropdownNavbarItem; keep them in step when Docusaurus is upgraded.
src/theme/DocCard/index.js - copy of the theme-classic doc card with one addition: a sidebar item's customProps can replace the default emoji with iconComponent (a React node), icon (an image path under static/, with invertOnDark to invert it in dark mode) or emoji. Link items and category items both honour it. The tiles on docs/providers.md and the four Quick Starts provider categories in sidebars.js use icon; the Quick Starts entries look the icon up in the provider catalog by slug, so the cards follow src/configs/providers.json.
Every doc has its own description:; the old boilerplate ("Query and Deploy Cloud Infrastructure and Resources using SQL") was shared by 89 pages and is gone. Google uses the description as the snippet under a sitelink and treats repetition as a reason to withhold sitelinks, so a new doc needs a one-sentence description written from its content, not a copied one. The homepage title is deliberately descriptive ("SQL for cloud infrastructure, SaaS APIs and AI agents") with sidebar_label: Welcome to StackQL keeping the sidebar entry short.
The structured-data plugin reads several frontmatter fields directly. Use these on /ai/* pages especially.
---
title: What is StackQL?
description: One-sentence definition - this becomes the JSON-LD WebPage.description, the llms.txt entry's description, and the OG description.
keywords: [stackql, sql, cloud, api]
proficiencyLevel: Beginner # Beginner | Intermediate | Expert - sets TechArticle.proficiencyLevel
dependencies: stackql >= 0.6 # optional string - sets TechArticle.dependencies
faq:
- question: Is StackQL a database?
answer: No. StackQL is a query runtime...
- question: Does StackQL replace Terraform?
answer: Not directly. Terraform's primary job is...
---When faq: is present, the plugin emits a FAQPage JSON-LD node and links it to the TechArticle via mainEntity. The .md companion preserves the frontmatter verbatim, so an LLM ingesting the markdown gets the FAQ pairs as content too.
howTo: and softwareApplication: work the same way. See the structured-data plugin README for the full shapes.
- Build with the AEO env vars set:
ALGOLIA_APP_ID=dummy ALGOLIA_API_KEY=dummy ALGOLIA_INDEX_NAME=dummy npm run build(local builds only - production gets real values). - Check that the build emits expected
.mdcompanions:find build -name "*.md" -type f | wc -lshould be ~300. - Check that
build/llms.txtandbuild/llms-full.txtexist and are non-empty. - Check that
build/providers.jsonexists and lists every catalog category: the provider microsites and the query library build their Providers menu from it. - For
/ai/*pages, verify the JSON-LD by inspecting the rendered HTML forTechArticle+FAQPagetypes (script we wrote in earlier sessions can be reproduced if needed).
- Pick the right directory by answer intent (definition, comparison, how-to, etc.).
- Write the page in the same voice as ai-content/canonical-definitions/what-is-stackql.md - direct, declarative, technical-encyclopedia tone (think Wikipedia, not a vendor blog).
- Always include
description:in frontmatter - this drives JSON-LD, llms.txt, and OG metadata. - Use
faq:frontmatter rather than the<script type="application/json" data-aeo-faq>MDX pattern. Both work; frontmatter is cleaner. - Cross-link to related
/ai/*pages even if they don't exist yet - mark them as "not yet written" so Docusaurus'sonBrokenLinks: 'throw'doesn't fail the build. As pages are written, convert the plain-text references to real links.
- The structured-data plugin doesn't care if you change content - it re-runs on every build. But if you add
faq:,howTo:,proficiencyLevel:, ordependencies:to frontmatter, those will surface in the JSON-LD automatically.
The two AEO plugins are developed in sibling repos. To bump:
yarn add @stackql/docusaurus-plugin-structured-data@<version>
yarn add @stackql/docusaurus-plugin-aeo@<version>
If the build breaks after a bump, the plugin's CHANGELOG.md is the first place to look. Both plugins maintain detailed changelogs noting breaking changes and config migrations.
npm run start does NOT run postBuild, so:
- No
.mdcompanions are emitted - No
llms.txtis emitted - JSON-LD from the structured-data plugin is NOT injected (it runs in postBuild)
But the Ask AI button (theme component) DOES render in dev. To verify the full AEO pipeline locally, run npm run build && npm run serve instead.
After plugin changes, dev cache must be cleared: rm -rf .docusaurus && npm run start. Hard refresh the browser too (Ctrl+F5).
react-gist@1.2.4 declares react: <=17 as a peer dep. Replaced with the local src/components/Gist/index.jsx component to drop the conflict. Do not reintroduce react-gist.
When the live site changes, AI fetch tools (Claude.ai's web_fetch, ChatGPT's browse, Perplexity) may serve cached responses for some hours. If a recently-deployed .md URL appears as 404 in an LLM response, check the URL directly with curl first - if curl returns 200, the bot is using a stale cache. Wait a few hours and retest.
These routes are React pages, the blog landing plugin route, blog list pages, or generated category index pages (/getting-started, /command-line-usage, /quick-starts/*) with no source markdown. The AEO plugin correctly does not emit .md companions for them. They appear in the human nav but not in llms.txt or anywhere requiring a .md twin. This is by design - do not "fix" by trying to force .md emission. The navbar and footer link to the canonical pages (/, /installing-stackql, /providers), so crawlers see real site structure; keep it that way when adding chrome links. /install, /downloads, /stackqldocs, /stackql-deploy, /features, /tutorials, /cookbooks, /providers/<slug> and /registry/<name> are Netlify 301s, so they 404 under yarn serve. /privacy is an MDX page and does get a companion; /contact-us is a React page and does not.
The Ask AI button is hidden below 997px viewport width (the Docusaurus mobile breakpoint) to keep the breadcrumb row uncluttered. To verify the button on a desktop test, the browser window must be wider than 997px.
# Local dev server (no AEO postBuild, no JSON-LD)
npm run start
# Full production build (AEO + JSON-LD + .md companions)
ALGOLIA_APP_ID=dummy ALGOLIA_API_KEY=dummy ALGOLIA_INDEX_NAME=dummy npm run build
# Serve the production build locally to verify
npm run serve
# Clear dev cache after plugin changes
rm -rf .docusaurus build
# Inspect emitted JSON-LD on a page
grep -A1 'application/ld+json' build/command-line-usage/exec.html | head -20
grep -A1 'application/ld+json' build/blog/product/stackql-mcp-server-now-available.html | head -20
# Count .md companions
find build -name "*.md" -type f | wc -l
# Check llms.txt structure
grep -E '^## ' build/llms.txt
# Regenerate the per-post blog redirect block in netlify.toml
node scripts/generate-blog-redirects.js
# Regenerate the provider redirect block in netlify.toml (after editing providers.json)
node scripts/generate-provider-redirects.js
# Search Console hygiene: no redirect stubs or noindex pages in the sitemap
grep -c '<loc>' build/sitemap.xml
grep -E 'stackql.io/(providers/|registry|install$|downloads$|stackqldocs$)' build/sitemap.xml # expect nothing
grep -l 'content="noindex, follow"' build/blog/*/tags/*.html | wc -l # expect every tag page../docusaurus-plugin-structured-data- JSON-LD emission plugin source. Bug fixes for stackql.io-specific issues land here first, then ship to npm.../docusaurus-plugin-aeo- AEO plugin source. Same pattern.../docusaurus-config- the shared chrome vendored at build time by the provider microsites and the query library. It fetches this site's/providers.jsonto build its Providers menu, and itstheme/NavbarItemis a copy of src/theme/NavbarItem/ProvidersDropdownNavbarItem/index.js adapted to that data source; change the two together.../../stackql-registry- StackQL provider registry. Houses the StackqlDeployDropdown component whose styling the Ask AI button was modeled on.