@pas7/llm-seo 0.1.6 → 0.3.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/README.md +26 -4
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.js +393 -18
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/next/index.d.ts +67 -2
- package/dist/adapters/next/index.js +393 -18
- package/dist/adapters/next/index.js.map +1 -1
- package/dist/canonical-from-manifest-BKpEmouS.d.ts +109 -0
- package/dist/cli/bin.js +400 -189
- package/dist/cli/bin.js.map +1 -1
- package/dist/config.schema-CI5OBbhQ.d.ts +1677 -0
- package/dist/core/index.d.ts +52 -66
- package/dist/core/index.js +214 -35
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +452 -115
- package/dist/index.js.map +1 -1
- package/dist/schema/index.d.ts +2 -3
- package/dist/schema/index.js +165 -85
- package/dist/schema/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/config.schema-DCnBx3Gm.d.ts +0 -824
- package/dist/manifest.schema-B_z3rxRV.d.ts +0 -384
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Deterministic LLM SEO artifacts generator & validator for modern static sites (N
|
|
|
12
12
|
One config in, deterministic artifacts out:
|
|
13
13
|
- generate `llms.txt` and `llms-full.txt`
|
|
14
14
|
- build canonical URLs from manifest items
|
|
15
|
+
- support mixed routing per manifest section (`prefix`, `suffix`, `locale-segment`, `custom`)
|
|
15
16
|
- lint policy constraints (restricted claims, duplicates, empty sections)
|
|
16
17
|
- check generated files in CI with explicit exit codes
|
|
17
18
|
|
|
@@ -42,10 +43,18 @@ export default {
|
|
|
42
43
|
hubs: ['/services', '/blog', '/projects', '/cases', '/contact'],
|
|
43
44
|
},
|
|
44
45
|
manifests: {
|
|
45
|
-
blog:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
blog: {
|
|
47
|
+
sectionPath: '/blog',
|
|
48
|
+
routeStyle: 'locale-segment',
|
|
49
|
+
items: [
|
|
50
|
+
{ slug: '/llm-seo-basics', locales: ['en', 'uk'] },
|
|
51
|
+
{ slug: '/canonical-strategy', locales: ['en'] },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
contactPages: {
|
|
55
|
+
routeStyle: 'suffix',
|
|
56
|
+
items: [{ slug: '/contact', locales: ['en', 'uk'] }],
|
|
57
|
+
},
|
|
49
58
|
},
|
|
50
59
|
contact: {
|
|
51
60
|
email: 'contact@example.com',
|
|
@@ -101,6 +110,7 @@ llm-seo check [options]
|
|
|
101
110
|
Options:
|
|
102
111
|
-c, --config <path> Path to config file
|
|
103
112
|
--fail-on <level> fail threshold: warn|error (default: error)
|
|
113
|
+
--check-machine-hints-live Live-check machine hint URLs over HTTP
|
|
104
114
|
-v, --verbose Verbose logs
|
|
105
115
|
```
|
|
106
116
|
|
|
@@ -146,6 +156,18 @@ import {
|
|
|
146
156
|
|
|
147
157
|
Use helpers from `@pas7/llm-seo/adapters/next` to normalize manifest items and build scripts.
|
|
148
158
|
See [`examples/next-static-export`](./examples/next-static-export).
|
|
159
|
+
For hybrid section routing, use `createSectionManifest(...)` and `applySectionCanonicalOverrides(...)`.
|
|
160
|
+
Works with `@pas7/nextjs-sitemap-hreflang` in one build pipeline:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
llm-seo generate --config llm-seo.config.ts
|
|
164
|
+
next build
|
|
165
|
+
nextjs-sitemap-hreflang check --in out/sitemap.xml --fail-on-missing
|
|
166
|
+
llm-seo check --config llm-seo.config.ts --fail-on error
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Hybrid routing example:
|
|
170
|
+
- [`examples/next-static-export-hybrid-routing`](./examples/next-static-export-hybrid-routing)
|
|
149
171
|
|
|
150
172
|
## Contributing and Security
|
|
151
173
|
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { BuildHookOptions, BuildHookResult, BuildScriptsResult, CreateManifestFromDataOptions, CreateManifestFromPagesDirOptions, CreateNextConfigOptions, CreateRobotsLlmsPolicySnippetOptions, FromNextContentManifestOptions, GenerateBuildScriptsOptions, NextConfigResult, NextContentManifest, NextManifestOptions, createManifestFromData, createManifestFromPagesDir, createNextConfig, createNextPlugin, createRobotsLlmsPolicySnippet, extractPagePaths, fromNextContentManifest, generateBuildScripts, generateNextManifest, postBuildHook } from './next/index.js';
|
|
2
|
-
import '../
|
|
1
|
+
export { ApplySectionCanonicalOverridesOptions, BuildHookOptions, BuildHookResult, BuildScriptsResult, CreateManifestFromDataOptions, CreateManifestFromPagesDirOptions, CreateNextConfigOptions, CreateRobotsLlmsPolicySnippetOptions, CreateSectionManifestOptions, FromNextContentManifestOptions, GenerateBuildScriptsOptions, NextConfigResult, NextContentManifest, NextManifestOptions, SectionFieldMapper, applySectionCanonicalOverrides, createManifestFromData, createManifestFromPagesDir, createNextConfig, createNextPlugin, createRobotsLlmsPolicySnippet, createSectionManifest, extractPagePaths, fromNextContentManifest, generateBuildScripts, generateNextManifest, postBuildHook } from './next/index.js';
|
|
2
|
+
import '../config.schema-CI5OBbhQ.js';
|
|
3
3
|
import 'zod';
|
|
4
|
+
import '../canonical-from-manifest-BKpEmouS.js';
|
package/dist/adapters/index.js
CHANGED
|
@@ -2,6 +2,230 @@ import { existsSync } from 'fs';
|
|
|
2
2
|
import { readFile, readdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
|
+
// src/adapters/next/manifest.ts
|
|
6
|
+
|
|
7
|
+
// src/core/normalize/url.ts
|
|
8
|
+
function normalizePath(path, preserveTrailingSlash = false) {
|
|
9
|
+
if (!path || path === "/") {
|
|
10
|
+
return "/";
|
|
11
|
+
}
|
|
12
|
+
const hadTrailingSlash = path.endsWith("/") && path !== "/";
|
|
13
|
+
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
14
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
15
|
+
const segments = [];
|
|
16
|
+
const parts = normalized.split("/");
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
if (part === "." || part === "") {
|
|
19
|
+
continue;
|
|
20
|
+
} else if (part === "..") {
|
|
21
|
+
if (segments.length > 0) {
|
|
22
|
+
segments.pop();
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
segments.push(part);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let result = "/" + segments.join("/");
|
|
29
|
+
if (preserveTrailingSlash && hadTrailingSlash && result !== "/") {
|
|
30
|
+
result += "/";
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function joinUrlParts(...parts) {
|
|
35
|
+
if (parts.length === 0) {
|
|
36
|
+
return "/";
|
|
37
|
+
}
|
|
38
|
+
const filteredParts = parts.filter((part) => part.length > 0);
|
|
39
|
+
if (filteredParts.length === 0) {
|
|
40
|
+
return "/";
|
|
41
|
+
}
|
|
42
|
+
const joined = filteredParts.map((part) => {
|
|
43
|
+
let p = part.replace(/^\/+/, "");
|
|
44
|
+
p = p.replace(/\/+$/, "");
|
|
45
|
+
return p;
|
|
46
|
+
}).filter((p) => p.length > 0).join("/");
|
|
47
|
+
return joined.length > 0 ? `/${joined}` : "/";
|
|
48
|
+
}
|
|
49
|
+
function normalizeUrl(options) {
|
|
50
|
+
const { baseUrl, path, trailingSlash, stripQuery = true, stripHash = true } = options;
|
|
51
|
+
let parsedBase;
|
|
52
|
+
try {
|
|
53
|
+
parsedBase = new URL(baseUrl);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new TypeError(`Invalid baseUrl: ${baseUrl}`);
|
|
56
|
+
}
|
|
57
|
+
const shouldPreserveTrailingSlash = trailingSlash === "preserve";
|
|
58
|
+
const normalizedPath = normalizePath(path, shouldPreserveTrailingSlash);
|
|
59
|
+
let finalPath = normalizedPath;
|
|
60
|
+
if (trailingSlash === "always") {
|
|
61
|
+
if (!finalPath.endsWith("/")) {
|
|
62
|
+
finalPath = `${finalPath}/`;
|
|
63
|
+
}
|
|
64
|
+
} else if (trailingSlash === "never") {
|
|
65
|
+
if (finalPath !== "/" && finalPath.endsWith("/")) {
|
|
66
|
+
finalPath = finalPath.slice(0, -1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const protocol = parsedBase.protocol.toLowerCase();
|
|
70
|
+
let hostname = parsedBase.hostname.toLowerCase();
|
|
71
|
+
let port = parsedBase.port;
|
|
72
|
+
if (port) {
|
|
73
|
+
const isDefaultPort = protocol === "http:" && port === "80" || protocol === "https:" && port === "443";
|
|
74
|
+
if (!isDefaultPort) {
|
|
75
|
+
hostname = `${hostname}:${port}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let fullUrl = `${protocol}//${hostname}${finalPath}`;
|
|
79
|
+
if (!stripQuery && parsedBase.search) {
|
|
80
|
+
fullUrl += parsedBase.search;
|
|
81
|
+
}
|
|
82
|
+
if (!stripHash && parsedBase.hash) {
|
|
83
|
+
fullUrl += parsedBase.hash;
|
|
84
|
+
}
|
|
85
|
+
return fullUrl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/core/normalize/sort.ts
|
|
89
|
+
function compareStrings(a, b) {
|
|
90
|
+
return a.localeCompare(b, "en", { sensitivity: "case", numeric: true });
|
|
91
|
+
}
|
|
92
|
+
function sortStrings(items) {
|
|
93
|
+
return [...items].sort(compareStrings);
|
|
94
|
+
}
|
|
95
|
+
function sortBy(items, keyFn) {
|
|
96
|
+
return [...items].sort((a, b) => compareStrings(keyFn(a), keyFn(b)));
|
|
97
|
+
}
|
|
98
|
+
function stableSortStrings(items) {
|
|
99
|
+
return [...items].sort((a, b) => a.localeCompare(b, "en", { sensitivity: "case", numeric: true }));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/core/canonical/locale.ts
|
|
103
|
+
function selectCanonicalLocale(options) {
|
|
104
|
+
const { defaultLocale, availableLocales } = options;
|
|
105
|
+
if (!availableLocales || availableLocales.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const validLocales = availableLocales.filter(
|
|
109
|
+
(locale) => typeof locale === "string" && locale.length > 0
|
|
110
|
+
);
|
|
111
|
+
if (validLocales.length === 0) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (defaultLocale && validLocales.includes(defaultLocale)) {
|
|
115
|
+
return defaultLocale;
|
|
116
|
+
}
|
|
117
|
+
const sorted = stableSortStrings(validLocales);
|
|
118
|
+
return sorted[0] ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/core/canonical/canonical-from-manifest.ts
|
|
122
|
+
function buildBaseUrlWithSubdomain(baseUrl, locale, strategy, defaultLocale) {
|
|
123
|
+
if (strategy !== "subdomain" || locale === defaultLocale) {
|
|
124
|
+
return baseUrl;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(baseUrl);
|
|
128
|
+
return `${parsed.protocol}//${locale}.${parsed.host}`;
|
|
129
|
+
} catch {
|
|
130
|
+
return baseUrl;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function createCanonicalUrlForItem(item, options) {
|
|
134
|
+
const {
|
|
135
|
+
baseUrl,
|
|
136
|
+
routePrefix,
|
|
137
|
+
defaultLocale,
|
|
138
|
+
trailingSlash,
|
|
139
|
+
localeStrategy,
|
|
140
|
+
routeStyle,
|
|
141
|
+
sectionName,
|
|
142
|
+
sectionPath,
|
|
143
|
+
pathnameFor
|
|
144
|
+
} = options;
|
|
145
|
+
if (item.canonicalOverride && typeof item.canonicalOverride === "string") {
|
|
146
|
+
return item.canonicalOverride;
|
|
147
|
+
}
|
|
148
|
+
const availableLocales = item.locales ?? [defaultLocale];
|
|
149
|
+
const canonicalLocale = selectCanonicalLocale({
|
|
150
|
+
defaultLocale,
|
|
151
|
+
availableLocales
|
|
152
|
+
});
|
|
153
|
+
const locale = canonicalLocale ?? defaultLocale;
|
|
154
|
+
const sectionBase = normalizeSectionPath(sectionPath ?? routePrefix ?? "");
|
|
155
|
+
const effectiveRouteStyle = routeStyle ?? inferRouteStyleFromLocaleStrategy(localeStrategy);
|
|
156
|
+
const normalizedSlug = normalizeItemSlug(item.slug, sectionBase);
|
|
157
|
+
const customPath = effectiveRouteStyle === "custom" ? pathnameFor?.({
|
|
158
|
+
item,
|
|
159
|
+
sectionName: sectionName ?? "",
|
|
160
|
+
slug: normalizedSlug,
|
|
161
|
+
locale,
|
|
162
|
+
defaultLocale,
|
|
163
|
+
sectionPath: sectionBase
|
|
164
|
+
}) : void 0;
|
|
165
|
+
const resolvedPath = customPath ?? buildPathFromRouteStyle({
|
|
166
|
+
routeStyle: effectiveRouteStyle,
|
|
167
|
+
sectionPath: sectionBase,
|
|
168
|
+
slug: normalizedSlug,
|
|
169
|
+
locale,
|
|
170
|
+
defaultLocale
|
|
171
|
+
});
|
|
172
|
+
const effectiveBaseUrl = buildBaseUrlWithSubdomain(baseUrl, locale, localeStrategy, defaultLocale);
|
|
173
|
+
return normalizeUrl({
|
|
174
|
+
baseUrl: effectiveBaseUrl,
|
|
175
|
+
path: resolvedPath,
|
|
176
|
+
trailingSlash,
|
|
177
|
+
stripQuery: true,
|
|
178
|
+
stripHash: true
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function inferRouteStyleFromLocaleStrategy(strategy) {
|
|
182
|
+
if (strategy !== "prefix") {
|
|
183
|
+
return "custom";
|
|
184
|
+
}
|
|
185
|
+
return "prefix";
|
|
186
|
+
}
|
|
187
|
+
function buildPathFromRouteStyle(args) {
|
|
188
|
+
const { routeStyle, sectionPath, slug, locale, defaultLocale } = args;
|
|
189
|
+
const includeLocale = locale !== defaultLocale;
|
|
190
|
+
if (routeStyle === "locale-segment") {
|
|
191
|
+
return joinUrlParts(sectionPath, locale, slug);
|
|
192
|
+
}
|
|
193
|
+
if (routeStyle === "suffix") {
|
|
194
|
+
if (includeLocale) {
|
|
195
|
+
return joinUrlParts(sectionPath, slug, locale);
|
|
196
|
+
}
|
|
197
|
+
return joinUrlParts(sectionPath, slug);
|
|
198
|
+
}
|
|
199
|
+
if (routeStyle === "custom") {
|
|
200
|
+
return joinUrlParts(sectionPath, slug);
|
|
201
|
+
}
|
|
202
|
+
if (includeLocale) {
|
|
203
|
+
return joinUrlParts(locale, sectionPath, slug);
|
|
204
|
+
}
|
|
205
|
+
return joinUrlParts(sectionPath, slug);
|
|
206
|
+
}
|
|
207
|
+
function normalizeSectionPath(sectionPath) {
|
|
208
|
+
if (!sectionPath || sectionPath === "/") {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
return sectionPath.startsWith("/") ? sectionPath : `/${sectionPath}`;
|
|
212
|
+
}
|
|
213
|
+
function normalizeItemSlug(slug, sectionPath) {
|
|
214
|
+
const normalizedSlug = slug.startsWith("/") ? slug : `/${slug}`;
|
|
215
|
+
if (!sectionPath) {
|
|
216
|
+
return normalizedSlug;
|
|
217
|
+
}
|
|
218
|
+
if (normalizedSlug === sectionPath) {
|
|
219
|
+
return "/";
|
|
220
|
+
}
|
|
221
|
+
const withSlash = `${sectionPath}/`;
|
|
222
|
+
if (normalizedSlug.startsWith(withSlash)) {
|
|
223
|
+
const relative = normalizedSlug.slice(withSlash.length);
|
|
224
|
+
return relative ? `/${relative}` : "/";
|
|
225
|
+
}
|
|
226
|
+
return normalizedSlug;
|
|
227
|
+
}
|
|
228
|
+
|
|
5
229
|
// src/adapters/next/manifest.ts
|
|
6
230
|
function fromNextContentManifest(manifest, options = {}) {
|
|
7
231
|
const { slugPrefix = "", defaultLocale } = options;
|
|
@@ -35,6 +259,132 @@ function fromNextContentManifest(manifest, options = {}) {
|
|
|
35
259
|
return result;
|
|
36
260
|
});
|
|
37
261
|
}
|
|
262
|
+
function createSectionManifest(options) {
|
|
263
|
+
const {
|
|
264
|
+
items,
|
|
265
|
+
sectionName,
|
|
266
|
+
sectionPath,
|
|
267
|
+
routeStyle,
|
|
268
|
+
defaultLocale,
|
|
269
|
+
defaultLocaleOverride,
|
|
270
|
+
pathnameFor,
|
|
271
|
+
slugKey = "slug",
|
|
272
|
+
localesKey = "locales",
|
|
273
|
+
updatedAtKey = "updatedAt",
|
|
274
|
+
publishedAtKey = "publishedAt",
|
|
275
|
+
priorityKey = "priority",
|
|
276
|
+
canonicalOverrideKey = "canonicalOverride",
|
|
277
|
+
titleFrom = "title",
|
|
278
|
+
descriptionFrom = "description"
|
|
279
|
+
} = options;
|
|
280
|
+
const normalizedItems = items.map((item) => {
|
|
281
|
+
const rawSlug = readStringField(item, slugKey);
|
|
282
|
+
if (!rawSlug) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
const normalized = {
|
|
286
|
+
slug: rawSlug.startsWith("/") ? rawSlug : `/${rawSlug}`
|
|
287
|
+
};
|
|
288
|
+
const locales = readStringArrayField(item, localesKey);
|
|
289
|
+
if (locales.length > 0) {
|
|
290
|
+
normalized.locales = locales;
|
|
291
|
+
} else if (defaultLocale) {
|
|
292
|
+
normalized.locales = [defaultLocale];
|
|
293
|
+
}
|
|
294
|
+
const updatedAt = readStringField(item, updatedAtKey);
|
|
295
|
+
if (updatedAt) {
|
|
296
|
+
normalized.updatedAt = updatedAt;
|
|
297
|
+
}
|
|
298
|
+
const publishedAt = readStringField(item, publishedAtKey);
|
|
299
|
+
if (publishedAt) {
|
|
300
|
+
normalized.publishedAt = publishedAt;
|
|
301
|
+
}
|
|
302
|
+
const priority = readNumberField(item, priorityKey);
|
|
303
|
+
if (priority !== void 0) {
|
|
304
|
+
normalized.priority = priority;
|
|
305
|
+
}
|
|
306
|
+
const canonicalOverride = readStringField(item, canonicalOverrideKey);
|
|
307
|
+
if (canonicalOverride) {
|
|
308
|
+
normalized.canonicalOverride = canonicalOverride;
|
|
309
|
+
}
|
|
310
|
+
const title = readMappedString(item, titleFrom);
|
|
311
|
+
if (title) {
|
|
312
|
+
normalized.title = title;
|
|
313
|
+
}
|
|
314
|
+
const description = readMappedString(item, descriptionFrom);
|
|
315
|
+
if (description) {
|
|
316
|
+
normalized.description = description;
|
|
317
|
+
}
|
|
318
|
+
return normalized;
|
|
319
|
+
}).filter((item) => item !== null);
|
|
320
|
+
const sortedItems = sortBy(normalizedItems, (item) => {
|
|
321
|
+
const locales = item.locales?.join(",") ?? "";
|
|
322
|
+
return `${item.slug}|${locales}`;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
items: sortedItems,
|
|
326
|
+
...sectionName && { sectionName },
|
|
327
|
+
...sectionPath && { sectionPath },
|
|
328
|
+
...routeStyle && { routeStyle },
|
|
329
|
+
...defaultLocaleOverride && { defaultLocaleOverride },
|
|
330
|
+
...pathnameFor && { pathnameFor }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function applySectionCanonicalOverrides(options) {
|
|
334
|
+
const {
|
|
335
|
+
section,
|
|
336
|
+
baseUrl,
|
|
337
|
+
defaultLocale,
|
|
338
|
+
trailingSlash = "never",
|
|
339
|
+
localeStrategy = "prefix"
|
|
340
|
+
} = options;
|
|
341
|
+
const mappedItems = section.items.map((item) => {
|
|
342
|
+
const canonicalOptions = {
|
|
343
|
+
baseUrl,
|
|
344
|
+
defaultLocale: section.defaultLocaleOverride ?? defaultLocale,
|
|
345
|
+
trailingSlash,
|
|
346
|
+
localeStrategy,
|
|
347
|
+
...section.routeStyle && { routeStyle: section.routeStyle },
|
|
348
|
+
...section.sectionPath && {
|
|
349
|
+
sectionPath: section.sectionPath,
|
|
350
|
+
routePrefix: section.sectionPath
|
|
351
|
+
},
|
|
352
|
+
...section.sectionName && { sectionName: section.sectionName },
|
|
353
|
+
...section.pathnameFor && { pathnameFor: section.pathnameFor }
|
|
354
|
+
};
|
|
355
|
+
const canonicalOverride = createCanonicalUrlForItem(item, canonicalOptions);
|
|
356
|
+
return {
|
|
357
|
+
...item,
|
|
358
|
+
canonicalOverride
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
...section,
|
|
363
|
+
items: mappedItems
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function readStringField(item, key) {
|
|
367
|
+
const value = item[key];
|
|
368
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
369
|
+
}
|
|
370
|
+
function readNumberField(item, key) {
|
|
371
|
+
const value = item[key];
|
|
372
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
373
|
+
}
|
|
374
|
+
function readStringArrayField(item, key) {
|
|
375
|
+
const value = item[key];
|
|
376
|
+
if (!Array.isArray(value)) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
return value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
380
|
+
}
|
|
381
|
+
function readMappedString(item, mapper) {
|
|
382
|
+
if (typeof mapper === "function") {
|
|
383
|
+
const value = mapper(item);
|
|
384
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
385
|
+
}
|
|
386
|
+
return readStringField(item, mapper);
|
|
387
|
+
}
|
|
38
388
|
function parseFrontmatter(content) {
|
|
39
389
|
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
40
390
|
const match = content.match(frontmatterRegex);
|
|
@@ -222,17 +572,6 @@ function generateNextManifest(options, buildManifest) {
|
|
|
222
572
|
};
|
|
223
573
|
}
|
|
224
574
|
|
|
225
|
-
// src/core/normalize/sort.ts
|
|
226
|
-
function compareStrings(a, b) {
|
|
227
|
-
return a.localeCompare(b, "en", { sensitivity: "case", numeric: true });
|
|
228
|
-
}
|
|
229
|
-
function sortStrings(items) {
|
|
230
|
-
return [...items].sort(compareStrings);
|
|
231
|
-
}
|
|
232
|
-
function sortBy(items, keyFn) {
|
|
233
|
-
return [...items].sort((a, b) => compareStrings(keyFn(a), keyFn(b)));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
575
|
// src/core/normalize/text.ts
|
|
237
576
|
function normalizeLineEndings(text, lineEndings) {
|
|
238
577
|
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -396,7 +735,7 @@ function generateLlmsTxt(manifest, _options) {
|
|
|
396
735
|
|
|
397
736
|
// src/core/generate/llms-full-txt.ts
|
|
398
737
|
function createLlmsFullTxt(options) {
|
|
399
|
-
const { config, canonicalUrls, manifestItems } = options;
|
|
738
|
+
const { config, canonicalUrls, manifestItems, manifestEntries } = options;
|
|
400
739
|
const lineEndings = config.format?.lineEndings ?? "lf";
|
|
401
740
|
const lines = [];
|
|
402
741
|
lines.push(`# ${config.brand.name} - Full LLM Context`);
|
|
@@ -413,6 +752,10 @@ function createLlmsFullTxt(options) {
|
|
|
413
752
|
lines.push(`Organization: ${config.brand.org}`);
|
|
414
753
|
}
|
|
415
754
|
lines.push(`Locales: ${config.brand.locales.join(", ")}`);
|
|
755
|
+
const lastUpdated = getLastUpdatedDate(manifestItems);
|
|
756
|
+
if (lastUpdated) {
|
|
757
|
+
lines.push(`Last Updated: ${lastUpdated}`);
|
|
758
|
+
}
|
|
416
759
|
lines.push("");
|
|
417
760
|
if (canonicalUrls.length > 0) {
|
|
418
761
|
lines.push("## All Canonical URLs");
|
|
@@ -450,11 +793,16 @@ function createLlmsFullTxt(options) {
|
|
|
450
793
|
lines.push("");
|
|
451
794
|
}
|
|
452
795
|
}
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
lines.push("## Social & Booking");
|
|
796
|
+
const hasContact = config.contact?.email || config.contact?.phone || config.contact?.social?.twitter || config.contact?.social?.linkedin || config.contact?.social?.github || config.booking?.url;
|
|
797
|
+
if (hasContact) {
|
|
798
|
+
lines.push("## Contact");
|
|
457
799
|
lines.push("");
|
|
800
|
+
if (config.contact?.email) {
|
|
801
|
+
lines.push(`- Email: ${config.contact.email}`);
|
|
802
|
+
}
|
|
803
|
+
if (config.contact?.phone) {
|
|
804
|
+
lines.push(`- Phone: ${config.contact.phone}`);
|
|
805
|
+
}
|
|
458
806
|
if (config.contact?.social?.twitter) {
|
|
459
807
|
lines.push(`- Twitter: ${config.contact.social.twitter}`);
|
|
460
808
|
}
|
|
@@ -498,7 +846,16 @@ function createLlmsFullTxt(options) {
|
|
|
498
846
|
lines.push(`- [${hub}](${hub}) - ${getHubLabel2(hub)}`);
|
|
499
847
|
}
|
|
500
848
|
}
|
|
501
|
-
if (
|
|
849
|
+
if (manifestEntries && manifestEntries.length > 0) {
|
|
850
|
+
const sortedEntries = sortBy(manifestEntries, (entry) => {
|
|
851
|
+
return `${entry.item.slug}|${entry.canonicalUrl}`;
|
|
852
|
+
});
|
|
853
|
+
for (const entry of sortedEntries) {
|
|
854
|
+
const title = entry.item.title ?? entry.item.slug;
|
|
855
|
+
const locales = entry.item.locales?.join(", ") ?? config.brand.locales[0] ?? "en";
|
|
856
|
+
lines.push(`- [${title}](${entry.canonicalUrl}) (${locales})`);
|
|
857
|
+
}
|
|
858
|
+
} else if (manifestItems.length > 0) {
|
|
502
859
|
const sortedItems = sortBy(manifestItems, (item) => item.slug);
|
|
503
860
|
for (const item of sortedItems) {
|
|
504
861
|
const url = item.canonicalOverride ?? `${config.site.baseUrl}${item.slug}`;
|
|
@@ -519,6 +876,24 @@ function createLlmsFullTxt(options) {
|
|
|
519
876
|
lineCount: finalLines.length
|
|
520
877
|
};
|
|
521
878
|
}
|
|
879
|
+
function getLastUpdatedDate(items) {
|
|
880
|
+
const timestamps = [];
|
|
881
|
+
for (const item of items) {
|
|
882
|
+
if (item.updatedAt) {
|
|
883
|
+
timestamps.push(item.updatedAt);
|
|
884
|
+
} else if (item.publishedAt) {
|
|
885
|
+
timestamps.push(item.publishedAt);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (timestamps.length === 0) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
const latest = timestamps.map((value) => new Date(value)).filter((value) => !Number.isNaN(value.getTime())).sort((a, b) => b.getTime() - a.getTime())[0];
|
|
892
|
+
if (!latest) {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
return latest.toISOString().slice(0, 10);
|
|
896
|
+
}
|
|
522
897
|
function getHubLabel2(hub) {
|
|
523
898
|
const labels = {
|
|
524
899
|
"/services": "Services overview",
|
|
@@ -668,6 +1043,6 @@ function createNextPlugin() {
|
|
|
668
1043
|
};
|
|
669
1044
|
}
|
|
670
1045
|
|
|
671
|
-
export { createManifestFromData, createManifestFromPagesDir, createNextConfig, createNextPlugin, createRobotsLlmsPolicySnippet, extractPagePaths, fromNextContentManifest, generateBuildScripts, generateNextManifest, postBuildHook };
|
|
1046
|
+
export { applySectionCanonicalOverrides, createManifestFromData, createManifestFromPagesDir, createNextConfig, createNextPlugin, createRobotsLlmsPolicySnippet, createSectionManifest, extractPagePaths, fromNextContentManifest, generateBuildScripts, generateNextManifest, postBuildHook };
|
|
672
1047
|
//# sourceMappingURL=index.js.map
|
|
673
1048
|
//# sourceMappingURL=index.js.map
|