@stackql/docusaurus-plugin-aeo 0.4.2 → 0.5.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 +32 -0
- package/README.md +112 -42
- package/package.json +18 -5
- package/src/askAiPrompt.js +21 -0
- package/src/companionPath.js +55 -0
- package/src/features/aiRoutes.js +3 -0
- package/src/features/alternateLinks.js +100 -0
- package/src/features/companions.js +84 -68
- package/src/features/llmsTxt.js +121 -53
- package/src/features/plainMarkdown.js +623 -0
- package/src/index.js +63 -10
- package/src/theme/AskAiButton/index.jsx +30 -4
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const emitCompanions = require('./features/companions');
|
|
3
|
+
const { collectCompanionItems } = require('./features/companions');
|
|
3
4
|
const emitLlmsTxt = require('./features/llmsTxt');
|
|
5
|
+
const injectAlternateLinks = require('./features/alternateLinks');
|
|
4
6
|
const { validateAiRoutes } = require('./features/aiRoutes');
|
|
5
7
|
|
|
6
8
|
const DEFAULT_LLMS_EXCLUDE = [
|
|
@@ -15,7 +17,7 @@ const DEFAULT_LLMS_EXCLUDE = [
|
|
|
15
17
|
const DEFAULT_PROVIDER_ORDER = ['claude', 'chatgpt', 'perplexity'];
|
|
16
18
|
const VALID_PROVIDERS = new Set(DEFAULT_PROVIDER_ORDER);
|
|
17
19
|
const DEFAULT_PROMPT_TEMPLATE =
|
|
18
|
-
'Read {
|
|
20
|
+
'Read {companionUrl} and help me understand it. Summarize the key points, then ask me one clarifying question to dig deeper.';
|
|
19
21
|
|
|
20
22
|
function normalizeOptions(raw) {
|
|
21
23
|
const opts = raw || {};
|
|
@@ -28,8 +30,13 @@ function normalizeOptions(raw) {
|
|
|
28
30
|
return {
|
|
29
31
|
companions: {
|
|
30
32
|
enabled: companions.enabled !== false,
|
|
31
|
-
format: companions.format || '
|
|
33
|
+
format: companions.format || 'plain',
|
|
32
34
|
exclude: companions.exclude || [],
|
|
35
|
+
// <link rel="alternate" type="text/markdown"> in every page that has
|
|
36
|
+
// a companion, and optionally the same as a Link header via a
|
|
37
|
+
// Netlify-style _headers file.
|
|
38
|
+
alternateLink: companions.alternateLink !== false,
|
|
39
|
+
linkHeader: companions.linkHeader === true,
|
|
33
40
|
},
|
|
34
41
|
llmsTxt: {
|
|
35
42
|
enabled: llmsTxt.enabled !== false,
|
|
@@ -45,7 +52,10 @@ function normalizeOptions(raw) {
|
|
|
45
52
|
// When set, supersedes `sections` and switches feature 2 from
|
|
46
53
|
// per-type grouping to per-instance grouping.
|
|
47
54
|
instanceSections: llmsTxt.instanceSections || null,
|
|
48
|
-
|
|
55
|
+
// true (everything), false, or { include: ['<plugin>@<id>'], maxBytes }
|
|
56
|
+
fullTxt: llmsTxt.fullTxt === undefined ? true : llmsTxt.fullTxt,
|
|
57
|
+
// "## Optional" section in llms.txt pointing at llms-full.txt
|
|
58
|
+
linkFullTxt: llmsTxt.linkFullTxt !== false,
|
|
49
59
|
},
|
|
50
60
|
askAi: {
|
|
51
61
|
enabled: askAi.enabled !== false,
|
|
@@ -63,11 +73,33 @@ function normalizeOptions(raw) {
|
|
|
63
73
|
function validateOptions(opts) {
|
|
64
74
|
const errs = [];
|
|
65
75
|
|
|
66
|
-
if (!['raw', 'plain'].includes(opts.companions.format)) {
|
|
76
|
+
if (!['raw', 'plain', 'clean'].includes(opts.companions.format)) {
|
|
67
77
|
errs.push(
|
|
68
|
-
`companions.format must be "
|
|
78
|
+
`companions.format must be "plain" (or its synonym "clean") or "raw", got "${opts.companions.format}"`,
|
|
69
79
|
);
|
|
70
80
|
}
|
|
81
|
+
if (typeof opts.companions.alternateLink !== 'boolean') {
|
|
82
|
+
errs.push('companions.alternateLink must be a boolean');
|
|
83
|
+
}
|
|
84
|
+
if (typeof opts.companions.linkHeader !== 'boolean') {
|
|
85
|
+
errs.push('companions.linkHeader must be a boolean');
|
|
86
|
+
}
|
|
87
|
+
const ft = opts.llmsTxt.fullTxt;
|
|
88
|
+
if (typeof ft !== 'boolean') {
|
|
89
|
+
if (!ft || typeof ft !== 'object' || Array.isArray(ft)) {
|
|
90
|
+
errs.push('llmsTxt.fullTxt must be a boolean or an object { include?: string[], maxBytes?: number }');
|
|
91
|
+
} else {
|
|
92
|
+
if (ft.include !== undefined && (!Array.isArray(ft.include) || ft.include.some((k) => typeof k !== 'string'))) {
|
|
93
|
+
errs.push('llmsTxt.fullTxt.include must be an array of "<plugin>@<id>" strings');
|
|
94
|
+
}
|
|
95
|
+
if (ft.maxBytes !== undefined && (!Number.isFinite(ft.maxBytes) || ft.maxBytes <= 0)) {
|
|
96
|
+
errs.push('llmsTxt.fullTxt.maxBytes must be a positive number');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (typeof opts.llmsTxt.linkFullTxt !== 'boolean') {
|
|
101
|
+
errs.push('llmsTxt.linkFullTxt must be a boolean');
|
|
102
|
+
}
|
|
71
103
|
if (!Array.isArray(opts.companions.exclude)) {
|
|
72
104
|
errs.push('companions.exclude must be an array of glob patterns');
|
|
73
105
|
}
|
|
@@ -160,10 +192,10 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
160
192
|
const plugin = {
|
|
161
193
|
name: '@stackql/docusaurus-plugin-aeo',
|
|
162
194
|
|
|
163
|
-
// Surface the askAi config to theme components
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
195
|
+
// Surface the askAi config to theme components, which read it via
|
|
196
|
+
// usePluginData('@stackql/docusaurus-plugin-aeo') at render time. The
|
|
197
|
+
// companion route list is added from allContentLoaded below; Docusaurus
|
|
198
|
+
// merges global data set from the two hooks (shallow, per plugin).
|
|
167
199
|
async contentLoaded({ actions }) {
|
|
168
200
|
await actions.setGlobalData({
|
|
169
201
|
askAi: {
|
|
@@ -180,7 +212,7 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
180
212
|
// allContentLoaded in Docusaurus 3.x. contentLoaded receives only the
|
|
181
213
|
// current plugin's own content, so feature 1 needs this hook to see
|
|
182
214
|
// the docs/blog source files it has to mirror.
|
|
183
|
-
async allContentLoaded({ allContent }) {
|
|
215
|
+
async allContentLoaded({ allContent, actions }) {
|
|
184
216
|
if (allContent) {
|
|
185
217
|
for (const [pluginName, byId] of Object.entries(allContent)) {
|
|
186
218
|
if (!byId) continue;
|
|
@@ -200,6 +232,18 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
200
232
|
verbose: options.verbose,
|
|
201
233
|
});
|
|
202
234
|
}
|
|
235
|
+
|
|
236
|
+
// Which routes will have a companion, so the Ask AI button can point
|
|
237
|
+
// at the file and stay hidden on pages without one (generated
|
|
238
|
+
// category indexes, React pages). Same list postBuild emits from.
|
|
239
|
+
if (actions && typeof actions.setGlobalData === 'function') {
|
|
240
|
+
const companionRoutes = options.companions.enabled
|
|
241
|
+
? collectCompanionItems(loadedContentByPlugin, options.companions).map(
|
|
242
|
+
(item) => item.permalink,
|
|
243
|
+
)
|
|
244
|
+
: [];
|
|
245
|
+
await actions.setGlobalData({ companionRoutes });
|
|
246
|
+
}
|
|
203
247
|
},
|
|
204
248
|
|
|
205
249
|
async postBuild(props) {
|
|
@@ -216,6 +260,15 @@ module.exports = function pluginAeo(context, rawOptions) {
|
|
|
216
260
|
console.log('[plugin-aeo] companions disabled, skipping feature 1');
|
|
217
261
|
}
|
|
218
262
|
|
|
263
|
+
if (options.companions.enabled && options.companions.alternateLink) {
|
|
264
|
+
await injectAlternateLinks({
|
|
265
|
+
props,
|
|
266
|
+
emittedCompanions,
|
|
267
|
+
linkHeader: options.companions.linkHeader,
|
|
268
|
+
verbose: options.verbose,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
219
272
|
if (options.llmsTxt.enabled) {
|
|
220
273
|
await emitLlmsTxt({
|
|
221
274
|
props,
|
|
@@ -10,6 +10,13 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
|
10
10
|
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
11
11
|
import icons from './icons.js';
|
|
12
12
|
import styles from './styles.module.css';
|
|
13
|
+
// CommonJS, shared with the Node-side features so the button and the
|
|
14
|
+
// emitted file can never disagree about where the companion lives.
|
|
15
|
+
import companionPathModule from '../../companionPath.js';
|
|
16
|
+
import askAiPromptModule from '../../askAiPrompt.js';
|
|
17
|
+
|
|
18
|
+
const { companionUrl, normalizeRoute } = companionPathModule;
|
|
19
|
+
const { fillPrompt } = askAiPromptModule;
|
|
13
20
|
|
|
14
21
|
const PROVIDER_LABELS = {
|
|
15
22
|
claude: 'Ask Claude',
|
|
@@ -24,7 +31,8 @@ const PROVIDER_URLS = {
|
|
|
24
31
|
};
|
|
25
32
|
|
|
26
33
|
const DEFAULT_PROMPT_TEMPLATE =
|
|
27
|
-
'Read {
|
|
34
|
+
'Read {companionUrl} and help me understand it. Summarize the key points, then ask me one clarifying question to dig deeper.';
|
|
35
|
+
|
|
28
36
|
|
|
29
37
|
export default function AskAiButton() {
|
|
30
38
|
const { siteConfig } = useDocusaurusContext();
|
|
@@ -37,10 +45,28 @@ export default function AskAiButton() {
|
|
|
37
45
|
|
|
38
46
|
if (cfg.enabled === false) return null;
|
|
39
47
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
48
|
+
const siteUrl = (siteConfig.url || '').replace(/\/+$/, '');
|
|
49
|
+
const route = normalizeRoute(location.pathname);
|
|
50
|
+
const pageUrl = route === '/' ? siteUrl : `${siteUrl}${route}`;
|
|
51
|
+
|
|
52
|
+
// Only pages with a companion get the button: a generated category index
|
|
53
|
+
// or a React page has no .md file, so a prompt pointing at one would
|
|
54
|
+
// send the reader to a 404. `companionRoutes` arrives from
|
|
55
|
+
// allContentLoaded; if an older core did not merge it, fall back to
|
|
56
|
+
// showing the button everywhere as before. With companions disabled the
|
|
57
|
+
// prompt uses the page itself.
|
|
58
|
+
const companionsEnabled = cfg.companionsEnabled !== false;
|
|
59
|
+
const routes = data.companionRoutes;
|
|
60
|
+
if (companionsEnabled && Array.isArray(routes)) {
|
|
61
|
+
const known = routes.some((r) => normalizeRoute(r) === route);
|
|
62
|
+
if (!known) return null;
|
|
63
|
+
}
|
|
64
|
+
const mdUrl = companionsEnabled
|
|
65
|
+
? companionUrl(siteUrl, route, siteConfig.trailingSlash)
|
|
66
|
+
: pageUrl;
|
|
67
|
+
|
|
42
68
|
const promptTemplate = cfg.promptTemplate || DEFAULT_PROMPT_TEMPLATE;
|
|
43
|
-
const prompt = promptTemplate
|
|
69
|
+
const prompt = fillPrompt(promptTemplate, { pageUrl, companionUrl: mdUrl });
|
|
44
70
|
const encoded = encodeURIComponent(prompt);
|
|
45
71
|
|
|
46
72
|
const order =
|