@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 +10 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/package.json +53 -0
- package/src/features/aiRoutes.js +83 -0
- package/src/features/companions.js +234 -0
- package/src/features/llmsTxt.js +175 -0
- package/src/helpers.js +61 -0
- package/src/index.js +200 -0
- package/src/theme/AskAiButton/icons.js +55 -0
- package/src/theme/AskAiButton/index.jsx +116 -0
- package/src/theme/AskAiButton/styles.module.css +74 -0
- package/src/theme/BlogPostItem/Footer/index.jsx +12 -0
- package/src/theme/DocItem/Footer/index.jsx +12 -0
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 };
|