@stackql/docusaurus-plugin-aeo 0.1.0 → 0.1.2

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 CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ Fix: cross-plugin loaded content was not captured because contentLoaded does not receive allContent in Docusaurus 3.x. Use allContentLoaded hook. Without this fix, feature 1 (.md companions) emitted zero files and feature 2 (llms.txt / llms-full.txt) was empty.
6
+
7
+ ## 0.1.1
8
+
9
+ Bugfix release. v0.1.0 failed to build on a real Docusaurus 3.10 consumer with three distinct crashes; all three are fixed here.
10
+
11
+ ### Fixed
12
+
13
+ - **Plugin construction crash: `plugin.options.id` is `undefined`.** v0.1.0 exported a `validateOptions` function that bypassed Docusaurus's option normalization, so the standard `id: 'default'` default was never applied. `@docusaurus/core` then crashed in `lib/server/plugins/actions.js` at `createPluginActionsUtils`:
14
+
15
+ ```text
16
+ TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string.
17
+ Received undefined
18
+ at Object.join (node:path:460:7)
19
+ at createPluginActionsUtils (.../@docusaurus/core/lib/server/plugins/actions.js:27:36)
20
+ ```
21
+
22
+ Fix: removed the `validateOptions` export so Docusaurus's built-in plugin schema runs. The plugin's handwritten construction-time validator (`normalizeOptions` + the internal `validateOptions` in `src/index.js`) still validates plugin-specific options.
23
+
24
+ - **SSR stack overflow on every doc and blog page.** The footer wrappers at `src/theme/DocItem/Footer/index.jsx` and `src/theme/BlogPostItem/Footer/index.jsx` imported `@theme-original/DocItem/Footer` and `@theme-original/BlogPostItem/Footer`. When a *plugin* (not a *theme*) contributes the wrapper and is the only contributor in the wrapper layer, `@theme-original/X` resolves back to the wrapper itself, recursing during SSG:
25
+
26
+ ```text
27
+ Error: Can't render static file for pathname "/docs/intro"
28
+ [cause]: RangeError: Maximum call stack size exceeded
29
+ at RegExp.exec (<anonymous>)
30
+ at F (server.bundle.js:15836:87)
31
+ at Ka (server.bundle.js:15845:249)
32
+ at Pa (server.bundle.js:15853:68)
33
+ ```
34
+
35
+ Fix: switched both wrappers to import from `@theme-init/X`, which always resolves to the un-wrapped initial component from the theme chain.
36
+
37
+ - **`getThemePath()` returning `undefined` crashes core.** With `askAi.enabled: false` (or `askAi.placement: 'none'`), v0.1.0's `getThemePath` returned `undefined`, which `@docusaurus/core` then handed to `path.join` inside `webpack/server.js`. Fix: the plugin now constructs its plugin object conditionally and only attaches `getThemePath` when the theme is enabled. This is the idiomatic signal that a plugin contributes no theme.
38
+
39
+ ### Notes for consumers
40
+
41
+ No config changes required. Bump the version and rebuild.
42
+
3
43
  ## 0.1.0
4
44
 
5
45
  Initial release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackql/docusaurus-plugin-aeo",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
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
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -113,23 +113,33 @@ module.exports = function pluginAeo(context, rawOptions) {
113
113
  // Map<pluginName, { plugin: { name, id }, content: any }>
114
114
  const loadedContentByPlugin = new Map();
115
115
 
116
- return {
117
- name: '@stackql/docusaurus-plugin-aeo',
116
+ const themeEnabled =
117
+ options.askAi.enabled && options.askAi.placement !== 'none';
118
118
 
119
- getThemePath() {
120
- if (!options.askAi.enabled || options.askAi.placement === 'none') {
121
- return undefined;
122
- }
123
- return path.resolve(__dirname, './theme');
124
- },
119
+ const plugin = {
120
+ name: '@stackql/docusaurus-plugin-aeo',
125
121
 
126
- getClientModules() {
127
- return [];
122
+ // Surface the askAi config to theme components. setGlobalData MUST be
123
+ // called from contentLoaded - Docusaurus does not accept it from
124
+ // allContentLoaded, and theme components read it via
125
+ // usePluginData('@stackql/docusaurus-plugin-aeo') at render time.
126
+ async contentLoaded({ actions }) {
127
+ await actions.setGlobalData({
128
+ askAi: {
129
+ enabled: options.askAi.enabled,
130
+ providerOrder: options.askAi.providerOrder,
131
+ promptTemplate: options.askAi.promptTemplate,
132
+ placement: options.askAi.placement,
133
+ companionsEnabled: options.companions.enabled,
134
+ },
135
+ });
128
136
  },
129
137
 
130
- // Surface the askAi config to theme components via a global data
131
- // injection. Docusaurus client code can read this through useDocusaurusContext().
132
- async contentLoaded({ actions, allContent }) {
138
+ // Cross-plugin loaded content (docs, blog, pages) is only delivered to
139
+ // allContentLoaded in Docusaurus 3.x. contentLoaded receives only the
140
+ // current plugin's own content, so feature 1 needs this hook to see
141
+ // the docs/blog source files it has to mirror.
142
+ async allContentLoaded({ allContent }) {
133
143
  if (allContent) {
134
144
  for (const [pluginName, byId] of Object.entries(allContent)) {
135
145
  if (!byId) continue;
@@ -143,16 +153,6 @@ module.exports = function pluginAeo(context, rawOptions) {
143
153
  }
144
154
  }
145
155
 
146
- await actions.setGlobalData({
147
- askAi: {
148
- enabled: options.askAi.enabled,
149
- providerOrder: options.askAi.providerOrder,
150
- promptTemplate: options.askAi.promptTemplate,
151
- placement: options.askAi.placement,
152
- companionsEnabled: options.companions.enabled,
153
- },
154
- });
155
-
156
156
  if (options.aiRoutes.validate) {
157
157
  validateAiRoutes({
158
158
  loadedContentByPlugin,
@@ -188,13 +188,18 @@ module.exports = function pluginAeo(context, rawOptions) {
188
188
  }
189
189
  },
190
190
  };
191
- };
192
191
 
193
- module.exports.validateOptions = function validateDocusaurusOptions({
194
- options,
195
- validate: _validate,
196
- }) {
197
- // Docusaurus passes a Joi validator we deliberately don't use - the plugin
198
- // entry runs its own handwritten validator at construction time.
199
- return options || {};
192
+ // Only contribute a theme path when the Ask AI button is enabled.
193
+ // Returning undefined or an invalid value from getThemePath crashes
194
+ // @docusaurus/core in webpack/server.js; omitting the method entirely
195
+ // is the idiomatic signal that this plugin contributes no theme.
196
+ if (themeEnabled) {
197
+ plugin.getThemePath = () => path.resolve(__dirname, './theme');
198
+ }
199
+
200
+ return plugin;
200
201
  };
202
+
203
+ // Intentionally no validateOptions export: Docusaurus applies its own default
204
+ // option normalization (including id: 'default') when this is absent.
205
+ // The plugin's handwritten validator runs at construction time.
@@ -1,5 +1,8 @@
1
1
  import React from 'react';
2
- import Footer from '@theme-original/BlogPostItem/Footer';
2
+ // See note in src/theme/DocItem/Footer/index.jsx - @theme-init avoids the
3
+ // SSR recursion that @theme-original triggers when a plugin (not a theme)
4
+ // contributes the wrapper.
5
+ import Footer from '@theme-init/BlogPostItem/Footer';
3
6
  import AskAiButton from '@theme/AskAiButton';
4
7
 
5
8
  export default function FooterWrapper(props) {
@@ -1,5 +1,10 @@
1
1
  import React from 'react';
2
- import Footer from '@theme-original/DocItem/Footer';
2
+ // Use @theme-init (the initial component from the theme chain, before any
3
+ // wrappers) instead of @theme-original. When a plugin contributes both the
4
+ // wrapper and is the only contributor in the wrapper layer,
5
+ // @theme-original/X resolves back to this wrapper file and renders infinitely
6
+ // on every SSR pass. @theme-init/X always resolves to the un-wrapped origin.
7
+ import Footer from '@theme-init/DocItem/Footer';
3
8
  import AskAiButton from '@theme/AskAiButton';
4
9
 
5
10
  export default function FooterWrapper(props) {