@stackql/docusaurus-plugin-aeo 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - Feature 1: `.md` companion files for every doc/blog/page route (`raw` and `plain` modes).
8
+ - Feature 2: `llms.txt` and `llms-full.txt` generation at site root, following the llmstxt.org spec.
9
+ - Feature 3: Swizzle-friendly "Ask AI" dropdown (Claude, ChatGPT, Perplexity, Gemini) injected into doc and blog footers.
10
+ - Feature 4: `/ai/*` routing convention + helper exports for downstream sites.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StackQL Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,311 @@
1
+ # @stackql/docusaurus-plugin-aeo
2
+
3
+ AEO (Answer Engine Optimization) helpers for Docusaurus 3.x sites: emit raw `.md` companion files for every page, generate `llms.txt` and `llms-full.txt` at the site root, drop an "Ask AI" dropdown into doc and blog footers, and document a `/ai/*` routing convention for machine-readable companion content. This is a sibling to `@stackql/docusaurus-plugin-structured-data` (which emits JSON-LD); the two plugins compose without overlap.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @stackql/docusaurus-plugin-aeo
9
+ ```
10
+
11
+ ```bash
12
+ yarn add @stackql/docusaurus-plugin-aeo
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ Minimal `docusaurus.config.js`:
18
+
19
+ ```js
20
+ module.exports = {
21
+ // ...
22
+ plugins: [
23
+ '@stackql/docusaurus-plugin-aeo',
24
+ ],
25
+ };
26
+ ```
27
+
28
+ All four features are on by default. To configure, pass options:
29
+
30
+ ```js
31
+ module.exports = {
32
+ plugins: [
33
+ [
34
+ '@stackql/docusaurus-plugin-aeo',
35
+ {
36
+ companions: { format: 'raw' },
37
+ llmsTxt: { header: 'StackQL is a SQL interface to cloud and SaaS APIs.' },
38
+ askAi: { providerOrder: ['claude', 'perplexity', 'chatgpt', 'gemini'] },
39
+ verbose: true,
40
+ },
41
+ ],
42
+ ],
43
+ };
44
+ ```
45
+
46
+ ## Feature 1: `.md` companion files
47
+
48
+ For every emitted HTML route from the docs and blog plugins, this plugin writes a sibling `.md` file. A page at `/docs/intro` gets a companion at `/docs/intro/index.md` (or `/docs/intro.md` if `siteConfig.trailingSlash` is `false`). The file contains the raw markdown source.
49
+
50
+ Two modes:
51
+
52
+ - `companions.format: 'raw'` (default): the MDX source is emitted as-is. MDX `<Component />` tags pass through. LLMs handle them fine, and the file is a faithful representation of the page.
53
+ - `companions.format: 'plain'`: a best-effort regex pass strips `import` / `export` lines and JSX tags, then prepends a `# Title\n\n> description` block from the page frontmatter. Not a full remark pipeline - use `'raw'` unless an MDX-heavy page is causing concrete problems.
54
+
55
+ Fetch example:
56
+
57
+ ```bash
58
+ curl https://your-site.example/docs/intro/index.md
59
+ ```
60
+
61
+ **MIME type note.** Setting `Content-Type: text/markdown` from a static-site plugin is not possible. The file simply gets a `.md` extension and the host's MIME table handles it. Vercel, Netlify, Cloudflare Pages, GitHub Pages, and S3+CloudFront all serve `.md` as `text/markdown` or `text/plain` out of the box. If you serve from Nginx, ensure `text/markdown md;` is in your `mime.types`.
62
+
63
+ Non-content routes (custom React pages, redirects, the 404 page, search) are skipped silently. Enable `verbose: true` to log skips.
64
+
65
+ ## Feature 2: `llms.txt` and `llms-full.txt`
66
+
67
+ After feature 1 emits all companions, the plugin writes two files to the build root:
68
+
69
+ - `llms.txt` - sectioned index of every doc/blog page, in the format described at <https://llmstxt.org>:
70
+
71
+ ```text
72
+ # StackQL
73
+
74
+ > A SQL interface to cloud and SaaS APIs.
75
+
76
+ ## Documentation
77
+
78
+ - [Getting started](/docs/intro/index.md): Install StackQL and run your first query.
79
+ - [Providers](/docs/providers/index.md): Catalog of supported cloud APIs.
80
+
81
+ ## Blog
82
+
83
+ - [Querying Snowflake with StackQL](/blog/snowflake/index.md): ...
84
+ ```
85
+
86
+ - `llms-full.txt` - every companion concatenated with a `\n---\n\n` separator, each block prefixed with the page's title and URL so an LLM can cite individual sections.
87
+
88
+ Options:
89
+
90
+ | Option | Default | Description |
91
+ | --- | --- | --- |
92
+ | `llmsTxt.enabled` | `true` | Master switch. |
93
+ | `llmsTxt.exclude` | `["/search", "/404", "/blog/tags/**", "/blog/page/**", "/blog/archive", "/blog/authors/**"]` | Route glob patterns to skip. |
94
+ | `llmsTxt.include` | `null` | If set, only routes matching at least one pattern are included. Use for opt-in mode. |
95
+ | `llmsTxt.header` | `null` | String prepended to `llms.txt` above the section list. Good for a project intro paragraph or links to key external resources. |
96
+ | `llmsTxt.sections` | `{ docs: 'Documentation', blog: 'Blog', pages: 'Pages' }` | Section title overrides. |
97
+ | `llmsTxt.fullTxt` | `true` | Emit `llms-full.txt`. |
98
+
99
+ Glob patterns are matched against route permalinks. `**` matches across path segments; `*` matches within a single segment.
100
+
101
+ ## Feature 3: "Ask AI" button
102
+
103
+ A swizzle-friendly dropdown is injected above the existing `DocItem/Footer` and `BlogPostItem/Footer`. Each item opens the corresponding AI surface in a new tab with a prefilled prompt that references the current page's `.md` companion.
104
+
105
+ Providers and URL patterns:
106
+
107
+ | Provider | URL |
108
+ | --- | --- |
109
+ | Claude | `https://claude.ai/new?q={prompt}` |
110
+ | ChatGPT | `https://chatgpt.com/?q={prompt}` |
111
+ | Perplexity | `https://www.perplexity.ai/search?q={prompt}` |
112
+ | Gemini | `https://gemini.google.com/app?q={prompt}` |
113
+
114
+ Default prompt:
115
+
116
+ ```text
117
+ Read https://your-site.example/path/to/page.md and help me with the following question about it:
118
+ ```
119
+
120
+ The trailing space is intentional - the cursor lands ready for the user to type.
121
+
122
+ Options:
123
+
124
+ | Option | Default | Description |
125
+ | --- | --- | --- |
126
+ | `askAi.enabled` | `true` | When `false`, the theme components are not registered. |
127
+ | `askAi.providerOrder` | `['claude', 'chatgpt', 'perplexity', 'gemini']` | Order of items in the dropdown. Drop entries to hide them. |
128
+ | `askAi.promptTemplate` | `'Read {pageUrl}.md and help me with the following question about it: '` | Prompt sent to each provider. `{pageUrl}` is the page's canonical URL (no trailing slash). If feature 1 is disabled, the consumer should remove the `.md` from the template. |
129
+ | `askAi.placement` | `'doc-footer'` | `'doc-footer'` injects above doc and blog footers. `'none'` does not register any theme components - swizzle manually if you want to place the button elsewhere. |
130
+
131
+ Styling uses CSS modules and reads from Docusaurus's theme tokens (`--ifm-color-primary`, etc.) so dark/light mode work automatically.
132
+
133
+ Swizzle to move the button:
134
+
135
+ ```bash
136
+ npm run swizzle @stackql/docusaurus-plugin-aeo AskAiButton -- --wrap
137
+ ```
138
+
139
+ The component lives at `@theme/AskAiButton`. Import and place it wherever you want.
140
+
141
+ ## Feature 4: `/ai/*` routing convention
142
+
143
+ This plugin documents but does not auto-configure a second docs instance for machine-targeted content. The pattern:
144
+
145
+ ```js
146
+ // docusaurus.config.js
147
+ const { sitemapExclude, AI_ROUTE_PATTERNS } = require('@stackql/docusaurus-plugin-aeo/helpers');
148
+
149
+ module.exports = {
150
+ plugins: [
151
+ '@stackql/docusaurus-plugin-aeo',
152
+ [
153
+ '@docusaurus/plugin-content-docs',
154
+ {
155
+ id: 'ai',
156
+ routeBasePath: '/ai',
157
+ path: 'ai-content',
158
+ sidebarPath: false,
159
+ },
160
+ ],
161
+ [
162
+ '@docusaurus/plugin-sitemap',
163
+ {
164
+ ...sitemapExclude, // skip /ai/* in sitemap.xml
165
+ // your other sitemap options
166
+ },
167
+ ],
168
+ // ...other plugins
169
+ ],
170
+ themeConfig: {
171
+ structuredData: {
172
+ // @stackql/docusaurus-plugin-structured-data's `excludedRoutes` is an
173
+ // exact-match array, NOT a glob list. List the /ai/* routes you want
174
+ // skipped explicitly, or build the list at config time from a known
175
+ // route enumeration. See "Helpers" below.
176
+ excludedRoutes: [
177
+ '/ai',
178
+ // '/ai/faqs/install', '/ai/howto/connect', ...
179
+ ],
180
+ // ...rest of structuredData config
181
+ },
182
+ },
183
+ };
184
+ ```
185
+
186
+ Suggested directory layout under `ai-content/`:
187
+
188
+ ```text
189
+ ai-content/
190
+ ├── faqs/
191
+ │ └── what-is-stackql.md # frontmatter: faq: { ... }
192
+ ├── howto/
193
+ │ └── connect-to-aws.md # frontmatter: howTo: { ... }
194
+ └── apps/
195
+ └── stackql-cli.md # frontmatter: softwareApplication: { ... }
196
+ ```
197
+
198
+ The companion files emitted by feature 1 will live at `/ai/faqs/<slug>/index.md` (etc.), and they will appear in `llms.txt` and `llms-full.txt` just like any other doc, which is the point.
199
+
200
+ ### Optional validation
201
+
202
+ Set `aiRoutes.validate: true` to fail the build (with warnings, not errors) when files under `ai/faqs/`, `ai/howto/`, or `ai/apps/` are missing the corresponding frontmatter payload (`faq`, `howTo`, `softwareApplication`). Off by default.
203
+
204
+ ### Helpers
205
+
206
+ ```js
207
+ const {
208
+ AI_ROUTE_PATTERNS, // ['/ai', '/ai/**'] - glob patterns
209
+ sitemapExclude, // { ignorePatterns: AI_ROUTE_PATTERNS }
210
+ isAiRoute, // (permalink) => boolean
211
+ buildStructuredDataAiExcludes, // (routePaths[]) => string[]
212
+ } = require('@stackql/docusaurus-plugin-aeo/helpers');
213
+ ```
214
+
215
+ **Heads-up on the structured-data plugin.** `@stackql/docusaurus-plugin-structured-data`'s `themeConfig.structuredData.excludedRoutes` compares routes with strict equality, so glob patterns like `/ai/**` do not work there. Use one of these approaches:
216
+
217
+ - List the exact `/ai/*` permalinks you want skipped by hand (works if your `/ai/*` tree is small and stable).
218
+ - Compute the list at config time from an enumeration you control:
219
+
220
+ ```js
221
+ const { buildStructuredDataAiExcludes } = require('@stackql/docusaurus-plugin-aeo/helpers');
222
+ const aiRoutes = require('./ai-content/_routes.json'); // your own enumeration
223
+ // ...
224
+ themeConfig: {
225
+ structuredData: {
226
+ excludedRoutes: buildStructuredDataAiExcludes(aiRoutes),
227
+ // ...
228
+ },
229
+ }
230
+ ```
231
+
232
+ The `sitemapExclude` helper, by contrast, does work with globs because `@docusaurus/plugin-sitemap` uses `ignorePatterns` (micromatch under the hood).
233
+
234
+ ## Full options reference
235
+
236
+ ```js
237
+ {
238
+ // Feature 1
239
+ companions: {
240
+ enabled: true, // emit .md siblings
241
+ format: 'raw', // 'raw' | 'plain'
242
+ exclude: [], // route glob patterns to skip
243
+ },
244
+
245
+ // Feature 2
246
+ llmsTxt: {
247
+ enabled: true,
248
+ exclude: [
249
+ '/search',
250
+ '/404',
251
+ '/blog/tags/**',
252
+ '/blog/page/**',
253
+ '/blog/archive',
254
+ '/blog/authors/**',
255
+ ],
256
+ include: null, // null = all not-excluded
257
+ header: null, // optional string prepended to llms.txt
258
+ sections: {
259
+ docs: 'Documentation',
260
+ blog: 'Blog',
261
+ pages: 'Pages',
262
+ },
263
+ fullTxt: true, // emit llms-full.txt
264
+ },
265
+
266
+ // Feature 3
267
+ askAi: {
268
+ enabled: true,
269
+ providerOrder: ['claude', 'chatgpt', 'perplexity', 'gemini'],
270
+ promptTemplate: 'Read {pageUrl}.md and help me with the following question about it: ',
271
+ placement: 'doc-footer', // 'doc-footer' | 'none'
272
+ },
273
+
274
+ // Feature 4
275
+ aiRoutes: {
276
+ validate: false,
277
+ },
278
+
279
+ // Cross-cutting
280
+ verbose: false,
281
+ }
282
+ ```
283
+
284
+ Options are validated by a small handwritten validator at plugin construction. Invalid options throw a clear error before the build starts.
285
+
286
+ ## Composing with `@stackql/docusaurus-plugin-structured-data`
287
+
288
+ The two plugins are complementary and intended to be used together:
289
+
290
+ - `@stackql/docusaurus-plugin-structured-data` emits JSON-LD into page `<head>`. <https://github.com/stackql/docusaurus-plugin-structured-data>
291
+ - `@stackql/docusaurus-plugin-aeo` does everything else AEO-adjacent: raw `.md` companions, `llms.txt`, the Ask AI button, the `/ai/*` convention.
292
+
293
+ There is no overlap and no shared state. Add both:
294
+
295
+ ```js
296
+ module.exports = {
297
+ plugins: [
298
+ '@stackql/docusaurus-plugin-aeo',
299
+ [
300
+ '@stackql/docusaurus-plugin-structured-data',
301
+ {
302
+ /* structured-data options */
303
+ },
304
+ ],
305
+ ],
306
+ };
307
+ ```
308
+
309
+ ## License
310
+
311
+ MIT - Jeffrey Aven @ StackQL Studios.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@stackql/docusaurus-plugin-aeo",
3
+ "version": "0.1.0",
4
+ "description": "AEO (Answer Engine Optimization) helpers for Docusaurus: .md companion files, llms.txt / llms-full.txt, an Ask AI dropdown, and /ai/* route conventions.",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./helpers": "./src/helpers.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "CHANGELOG.md",
14
+ "LICENSE"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1"
21
+ },
22
+ "peerDependencies": {
23
+ "@docusaurus/core": "^3.0.0",
24
+ "react": "^18.0.0 || ^19.0.0",
25
+ "react-dom": "^18.0.0 || ^19.0.0"
26
+ },
27
+ "dependencies": {
28
+ "gray-matter": "^4.0.3"
29
+ },
30
+ "overrides": {
31
+ "serialize-javascript": "^7.0.5",
32
+ "uuid": "^11.1.1"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/stackql-labs/docusaurus-plugin-aeo"
37
+ },
38
+ "keywords": [
39
+ "docusaurus",
40
+ "aeo",
41
+ "answer-engine-optimization",
42
+ "llms.txt",
43
+ "ai",
44
+ "seo",
45
+ "plugin"
46
+ ],
47
+ "author": "Jeffrey Aven @ StackQL Studios",
48
+ "license": "MIT",
49
+ "bugs": {
50
+ "url": "https://github.com/stackql-labs/docusaurus-plugin-aeo/issues"
51
+ },
52
+ "homepage": "https://github.com/stackql-labs/docusaurus-plugin-aeo#readme"
53
+ }
@@ -0,0 +1,83 @@
1
+ // Feature 4: validate that pages under /ai/* carry frontmatter payloads
2
+ // consistent with their filename pattern. Off by default.
3
+ //
4
+ // Convention enforced when enabled:
5
+ // ai/faqs/<slug>.md -> must declare a `faq` frontmatter object
6
+ // ai/howto/<slug>.md -> must declare a `howTo` frontmatter object
7
+ // ai/howtos/<slug>.md -> same as above
8
+ // ai/apps/<slug>.md -> must declare a `softwareApplication`
9
+ // frontmatter object
10
+ //
11
+ // Anything else under /ai/* is permitted without a payload (consumer can
12
+ // extend this with additional checks if needed).
13
+
14
+ const KIND_FOR_DIR = {
15
+ faqs: 'faq',
16
+ faq: 'faq',
17
+ howto: 'howTo',
18
+ howtos: 'howTo',
19
+ apps: 'softwareApplication',
20
+ app: 'softwareApplication',
21
+ applications: 'softwareApplication',
22
+ };
23
+
24
+ function classify(permalink) {
25
+ // permalink like "/ai/faqs/foo"
26
+ if (!permalink.startsWith('/ai/')) return null;
27
+ const segments = permalink.split('/').filter(Boolean); // ["ai", "faqs", "foo"]
28
+ if (segments.length < 2) return null;
29
+ const dir = segments[1].toLowerCase();
30
+ return KIND_FOR_DIR[dir] || null;
31
+ }
32
+
33
+ function collectDocs(content) {
34
+ const out = [];
35
+ if (!content || !Array.isArray(content.loadedVersions)) return out;
36
+ for (const v of content.loadedVersions) {
37
+ if (!Array.isArray(v.docs)) continue;
38
+ for (const d of v.docs) {
39
+ out.push(d);
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function validateAiRoutes({ loadedContentByPlugin, verbose }) {
46
+ const problems = [];
47
+ for (const [, entry] of loadedContentByPlugin) {
48
+ if (entry.pluginName !== 'docusaurus-plugin-content-docs') continue;
49
+ const docs = collectDocs(entry.content);
50
+ for (const doc of docs) {
51
+ const permalink = doc.permalink;
52
+ if (!permalink) continue;
53
+ const expectedKey = classify(permalink);
54
+ if (!expectedKey) continue;
55
+ const fm = doc.frontMatter || {};
56
+ const value = fm[expectedKey];
57
+ if (
58
+ value === undefined ||
59
+ value === null ||
60
+ (typeof value === 'object' && Object.keys(value).length === 0)
61
+ ) {
62
+ problems.push(
63
+ `[plugin-aeo] aiRoutes: ${permalink} is missing required frontmatter "${expectedKey}" (source: ${doc.source || doc.id})`,
64
+ );
65
+ }
66
+ }
67
+ }
68
+
69
+ if (problems.length > 0) {
70
+ const msg = problems.join('\n');
71
+ if (verbose) {
72
+ console.warn(msg);
73
+ } else {
74
+ console.warn(
75
+ `[plugin-aeo] aiRoutes: ${problems.length} validation issue(s). Enable verbose: true for details.`,
76
+ );
77
+ }
78
+ } else if (verbose) {
79
+ console.log('[plugin-aeo] aiRoutes: validation OK');
80
+ }
81
+ }
82
+
83
+ module.exports = { validateAiRoutes, classify };