@pas7/llm-seo 0.2.0 → 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 CHANGED
@@ -110,6 +110,7 @@ llm-seo check [options]
110
110
  Options:
111
111
  -c, --config <path> Path to config file
112
112
  --fail-on <level> fail threshold: warn|error (default: error)
113
+ --check-machine-hints-live Live-check machine hint URLs over HTTP
113
114
  -v, --verbose Verbose logs
114
115
  ```
115
116
 
@@ -155,6 +156,7 @@ import {
155
156
 
156
157
  Use helpers from `@pas7/llm-seo/adapters/next` to normalize manifest items and build scripts.
157
158
  See [`examples/next-static-export`](./examples/next-static-export).
159
+ For hybrid section routing, use `createSectionManifest(...)` and `applySectionCanonicalOverrides(...)`.
158
160
  Works with `@pas7/nextjs-sitemap-hreflang` in one build pipeline:
159
161
 
160
162
  ```bash
@@ -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 '../manifest.schema-B_z3rxRV.js';
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';
@@ -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");
@@ -704,6 +1043,6 @@ function createNextPlugin() {
704
1043
  };
705
1044
  }
706
1045
 
707
- 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 };
708
1047
  //# sourceMappingURL=index.js.map
709
1048
  //# sourceMappingURL=index.js.map