@stackql/docusaurus-plugin-aeo 0.4.2 → 0.5.1
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 +55 -0
- package/LICENSE +0 -0
- package/README.md +179 -45
- package/package.json +21 -5
- package/src/askAiPrompt.js +22 -0
- package/src/companionPath.js +89 -0
- package/src/features/aiRoutes.js +15 -6
- package/src/features/alternateLinks.js +133 -0
- package/src/features/companions.js +93 -104
- package/src/features/llmsTxt.js +126 -84
- package/src/features/plainMarkdown.js +623 -0
- package/src/helpers.js +34 -10
- package/src/index.js +70 -10
- package/src/routeGlob.js +60 -0
- package/src/theme/AskAiButton/brand-icons/ChatGpt.jsx +0 -0
- package/src/theme/AskAiButton/brand-icons/Claude.jsx +0 -0
- package/src/theme/AskAiButton/brand-icons/Perplexity.jsx +0 -0
- package/src/theme/AskAiButton/icons.js +0 -0
- package/src/theme/AskAiButton/index.jsx +64 -12
- package/src/theme/AskAiButton/styles.module.css +0 -0
- package/src/theme/BlogPostItem/Header/Title/index.jsx +0 -0
- package/src/theme/BlogPostItem/Header/Title/styles.module.css +0 -0
- package/src/theme/DocBreadcrumbs/index.jsx +0 -0
- package/src/theme/DocBreadcrumbs/styles.module.css +0 -0
package/src/features/llmsTxt.js
CHANGED
|
@@ -1,47 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs').promises;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!patterns || patterns.length === 0) return false;
|
|
6
|
-
for (const p of patterns) {
|
|
7
|
-
if (toRegExp(p).test(value)) return true;
|
|
8
|
-
}
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function toRegExp(glob) {
|
|
13
|
-
let re = '';
|
|
14
|
-
for (let i = 0; i < glob.length; i++) {
|
|
15
|
-
const c = glob[i];
|
|
16
|
-
if (c === '*') {
|
|
17
|
-
if (glob[i + 1] === '*') {
|
|
18
|
-
re += '.*';
|
|
19
|
-
i++;
|
|
20
|
-
if (glob[i + 1] === '/') i++;
|
|
21
|
-
} else {
|
|
22
|
-
re += '[^/]*';
|
|
23
|
-
}
|
|
24
|
-
} else if (c === '?') {
|
|
25
|
-
re += '[^/]';
|
|
26
|
-
} else if ('.+^$()|{}[]\\'.includes(c)) {
|
|
27
|
-
re += '\\' + c;
|
|
28
|
-
} else {
|
|
29
|
-
re += c;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return new RegExp('^' + re + '$');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function companionUrl(permalink, trailingSlash) {
|
|
36
|
-
if (permalink === '/' || permalink === '') {
|
|
37
|
-
return '/index.md';
|
|
38
|
-
}
|
|
39
|
-
const cleaned = permalink.replace(/\/+$/, '');
|
|
40
|
-
if (trailingSlash === false) {
|
|
41
|
-
return `${cleaned}.md`;
|
|
42
|
-
}
|
|
43
|
-
return `${cleaned}/index.md`;
|
|
44
|
-
}
|
|
3
|
+
const { companionUrl, pageUrl } = require('../companionPath');
|
|
4
|
+
const { matchesRoute } = require('../routeGlob');
|
|
45
5
|
|
|
46
6
|
function groupKind(kind, pluginId) {
|
|
47
7
|
if (kind === 'docs') return 'docs';
|
|
@@ -149,30 +109,20 @@ function buildSections(filtered, options, verbose) {
|
|
|
149
109
|
return sections;
|
|
150
110
|
}
|
|
151
111
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Filter the companions through include/exclude rules.
|
|
163
|
-
const filtered = emittedCompanions.filter((item) => {
|
|
164
|
-
if (options.include && !options.include.some((p) => toRegExp(p).test(item.permalink))) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
if (matchAny(item.permalink, options.exclude)) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
return true;
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const sections = buildSections(filtered, options, verbose);
|
|
112
|
+
// Normalised view of `llmsTxt.fullTxt`: the boolean form (`true` = every
|
|
113
|
+
// section, no cap) or the object form `{ include, maxBytes }`.
|
|
114
|
+
function fullTxtOptions(fullTxt) {
|
|
115
|
+
if (!fullTxt) return null;
|
|
116
|
+
if (fullTxt === true) return { include: null, maxBytes: null };
|
|
117
|
+
return {
|
|
118
|
+
include: Array.isArray(fullTxt.include) && fullTxt.include.length > 0 ? fullTxt.include : null,
|
|
119
|
+
maxBytes: Number.isFinite(fullTxt.maxBytes) && fullTxt.maxBytes > 0 ? fullTxt.maxBytes : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
174
122
|
|
|
175
|
-
|
|
123
|
+
// Render llms.txt from the sections. Pure, so it can be tested without a
|
|
124
|
+
// build: returns the file text.
|
|
125
|
+
function renderLlmsTxt({ siteConfig, siteUrl, trailingSlash, sections, options, companionsEnabled }) {
|
|
176
126
|
const lines = [];
|
|
177
127
|
lines.push(`# ${siteConfig.title}`);
|
|
178
128
|
lines.push('');
|
|
@@ -189,9 +139,11 @@ module.exports = async function emitLlmsTxt({
|
|
|
189
139
|
lines.push(`## ${section.title}`);
|
|
190
140
|
lines.push('');
|
|
191
141
|
for (const item of section.items) {
|
|
142
|
+
// Absolute, as the llms.txt spec's examples are: the file is read
|
|
143
|
+
// away from the site, where a relative link has no base.
|
|
192
144
|
const url = companionsEnabled
|
|
193
|
-
? companionUrl(item.permalink, trailingSlash)
|
|
194
|
-
: item.permalink;
|
|
145
|
+
? companionUrl(siteUrl, item.permalink, trailingSlash, siteConfig.baseUrl)
|
|
146
|
+
: pageUrl(siteUrl, item.permalink, siteConfig.baseUrl);
|
|
195
147
|
const desc = item.description || siteConfig.tagline || '';
|
|
196
148
|
const titleText = item.title || item.permalink;
|
|
197
149
|
if (desc) {
|
|
@@ -203,8 +155,72 @@ module.exports = async function emitLlmsTxt({
|
|
|
203
155
|
lines.push('');
|
|
204
156
|
}
|
|
205
157
|
|
|
158
|
+
// The llmstxt.org convention for secondary resources: an "Optional"
|
|
159
|
+
// section a reader can skip. llms-full.txt is the whole corpus in one
|
|
160
|
+
// file, so it belongs here rather than inline with the page list.
|
|
161
|
+
if (options.fullTxt && options.linkFullTxt !== false && companionsEnabled) {
|
|
162
|
+
lines.push('## Optional');
|
|
163
|
+
lines.push('');
|
|
164
|
+
// Next to this file: under the locale's base in a localized build.
|
|
165
|
+
const base = String(siteConfig.baseUrl || '/').replace(/\/+$/, '');
|
|
166
|
+
lines.push(
|
|
167
|
+
`- [Full text of every page](${siteUrl}${base}/llms-full.txt): one file with the content of every page listed above`,
|
|
168
|
+
);
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return lines.join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// One block of llms-full.txt: the companion body with a `Source:` line (and
|
|
176
|
+
// the title, when the body does not already open with it). A plain
|
|
177
|
+
// companion already carries both, so the header is skipped and the body
|
|
178
|
+
// is emitted as is.
|
|
179
|
+
function fullTxtBlock({ body, item, siteUrl, baseUrl }) {
|
|
180
|
+
const url = pageUrl(siteUrl, item.permalink, baseUrl);
|
|
181
|
+
const head = body.trimStart().split('\n', 8);
|
|
182
|
+
const hasH1 = /^#\s+\S/.test(head[0] || '');
|
|
183
|
+
const hasSource = head.some((l) => /^Source: \S/.test(l));
|
|
184
|
+
const header = [
|
|
185
|
+
...(hasH1 ? [] : [`# ${item.title || item.permalink}`, '']),
|
|
186
|
+
...(hasSource ? [] : [`Source: ${url}`, '']),
|
|
187
|
+
];
|
|
188
|
+
const prefix = header.length ? `${header.join('\n')}\n` : '';
|
|
189
|
+
return `${prefix}${body.trim()}\n`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = async function emitLlmsTxt({
|
|
193
|
+
props,
|
|
194
|
+
options,
|
|
195
|
+
emittedCompanions,
|
|
196
|
+
companionsEnabled,
|
|
197
|
+
verbose,
|
|
198
|
+
}) {
|
|
199
|
+
const { outDir, siteConfig } = props;
|
|
200
|
+
const trailingSlash = siteConfig.trailingSlash;
|
|
201
|
+
const siteUrl = (siteConfig.url || '').replace(/\/+$/, '');
|
|
202
|
+
|
|
203
|
+
// Filter the companions through include/exclude rules. A pattern matches
|
|
204
|
+
// a route with or without siteConfig.baseUrl (see src/routeGlob.js), so
|
|
205
|
+
// the same patterns apply in every locale's build.
|
|
206
|
+
const filtered = emittedCompanions.filter((item) => {
|
|
207
|
+
if (options.include && !matchesRoute(item.permalink, options.include, siteConfig.baseUrl)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (matchesRoute(item.permalink, options.exclude, siteConfig.baseUrl)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const sections = buildSections(filtered, options, verbose);
|
|
217
|
+
|
|
206
218
|
const llmsTxtPath = path.join(outDir, 'llms.txt');
|
|
207
|
-
await fs.writeFile(
|
|
219
|
+
await fs.writeFile(
|
|
220
|
+
llmsTxtPath,
|
|
221
|
+
renderLlmsTxt({ siteConfig, siteUrl, trailingSlash, sections, options, companionsEnabled }),
|
|
222
|
+
'utf8',
|
|
223
|
+
);
|
|
208
224
|
if (verbose) {
|
|
209
225
|
console.log(`[plugin-aeo] llmsTxt: wrote ${llmsTxtPath}`);
|
|
210
226
|
}
|
|
@@ -212,7 +228,8 @@ module.exports = async function emitLlmsTxt({
|
|
|
212
228
|
// Build llms-full.txt by concatenating the companion files that we just
|
|
213
229
|
// emitted in feature 1. Requires feature 1 to have been enabled. Section
|
|
214
230
|
// headings mirror llms.txt so an LLM can navigate the corpus by H2.
|
|
215
|
-
|
|
231
|
+
const full = fullTxtOptions(options.fullTxt);
|
|
232
|
+
if (full) {
|
|
216
233
|
if (!companionsEnabled) {
|
|
217
234
|
if (verbose) {
|
|
218
235
|
console.log(
|
|
@@ -222,9 +239,26 @@ module.exports = async function emitLlmsTxt({
|
|
|
222
239
|
return;
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
242
|
+
// `include` names content instances ("<plugin>@<id>") or, with per-type
|
|
243
|
+
// grouping, the group keys (docs, blog, pages). Sections keep the order
|
|
244
|
+
// of llms.txt; `maxBytes` stops appending once the next block would
|
|
245
|
+
// cross the cap, so the file always ends on a whole page.
|
|
246
|
+
const wanted = full.include
|
|
247
|
+
? sections.filter((s) => full.include.includes(s.key))
|
|
248
|
+
: sections;
|
|
249
|
+
if (full.include && wanted.length === 0 && verbose) {
|
|
250
|
+
console.log(
|
|
251
|
+
`[plugin-aeo] llmsTxt: fullTxt.include matched no section (have: ${sections.map((s) => s.key).join(', ')})`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const SEP = '\n---\n\n';
|
|
256
|
+
let out = '';
|
|
257
|
+
let size = 0;
|
|
258
|
+
let truncated = false;
|
|
259
|
+
let pages = 0;
|
|
260
|
+
outer: for (const section of wanted) {
|
|
261
|
+
let sectionOpen = false;
|
|
228
262
|
for (const item of section.items) {
|
|
229
263
|
let body;
|
|
230
264
|
try {
|
|
@@ -237,23 +271,31 @@ module.exports = async function emitLlmsTxt({
|
|
|
237
271
|
}
|
|
238
272
|
continue;
|
|
239
273
|
}
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
274
|
+
const block = fullTxtBlock({ body, item, siteUrl, baseUrl: siteConfig.baseUrl });
|
|
275
|
+
const chunk = `${sectionOpen ? SEP : `${out ? SEP : ''}## ${section.title}\n\n`}${block}`;
|
|
276
|
+
const bytes = Buffer.byteLength(chunk, 'utf8');
|
|
277
|
+
if (full.maxBytes && size + bytes > full.maxBytes) {
|
|
278
|
+
truncated = true;
|
|
279
|
+
break outer;
|
|
280
|
+
}
|
|
281
|
+
out += chunk;
|
|
282
|
+
size += bytes;
|
|
283
|
+
sectionOpen = true;
|
|
284
|
+
pages++;
|
|
248
285
|
}
|
|
249
|
-
if (blocks.length === 0) continue;
|
|
250
|
-
sectionChunks.push(`## ${section.title}\n\n${blocks.join('\n---\n\n')}`);
|
|
251
286
|
}
|
|
252
287
|
|
|
253
288
|
const fullPath = path.join(outDir, 'llms-full.txt');
|
|
254
|
-
await fs.writeFile(fullPath,
|
|
289
|
+
await fs.writeFile(fullPath, out, 'utf8');
|
|
255
290
|
if (verbose) {
|
|
256
|
-
console.log(
|
|
291
|
+
console.log(
|
|
292
|
+
`[plugin-aeo] llmsTxt: wrote ${fullPath} (${pages} page(s), ${size} bytes${truncated ? `, stopped at the ${full.maxBytes}-byte cap` : ''})`,
|
|
293
|
+
);
|
|
257
294
|
}
|
|
258
295
|
}
|
|
259
296
|
};
|
|
297
|
+
|
|
298
|
+
module.exports.renderLlmsTxt = renderLlmsTxt;
|
|
299
|
+
module.exports.fullTxtBlock = fullTxtBlock;
|
|
300
|
+
module.exports.fullTxtOptions = fullTxtOptions;
|
|
301
|
+
module.exports.buildSections = buildSections;
|