@better-seo/core 0.0.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/dist/index.cjs ADDED
@@ -0,0 +1,652 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var messages = {
5
+ VALIDATION: "Invalid or incomplete SEO input.",
6
+ ADAPTER_NOT_FOUND: "No adapter registered for this framework. Import the adapter package (e.g. @better-seo/next) before calling seoForFramework.",
7
+ MIGRATE_NOT_IMPLEMENTED: "Migration helpers are not implemented yet. Track Roadmap Wave 12 / FEATURES C15.",
8
+ USE_SEO_NOT_AVAILABLE: "useSEO is provided by @better-seo/react (Roadmap Wave 5 / FEATURES V3). App Router metadata should use seo() / prepareNextSeo from @better-seo/next."
9
+ };
10
+ var SEOError = class extends Error {
11
+ code;
12
+ cause;
13
+ constructor(code, message, options) {
14
+ const base = message ?? messages[code];
15
+ super(`[@better-seo/core] [${code}]: ${base}`);
16
+ this.name = "SEOError";
17
+ this.code = code;
18
+ this.cause = options?.cause;
19
+ }
20
+ };
21
+ function isSEOError(e) {
22
+ return e instanceof SEOError;
23
+ }
24
+
25
+ // src/plugins.ts
26
+ function defineSEOPlugin(plugin) {
27
+ return plugin;
28
+ }
29
+ function runBeforeMergePlugins(input, config) {
30
+ const list = config?.plugins ?? [];
31
+ let acc = input;
32
+ for (const p of list) {
33
+ acc = p.beforeMerge?.(acc, { config }) ?? acc;
34
+ }
35
+ return acc;
36
+ }
37
+ function runAfterMergePlugins(seo, config) {
38
+ const list = config?.plugins ?? [];
39
+ let acc = seo;
40
+ for (const p of list) {
41
+ acc = p.afterMerge?.(acc, { config }) ?? acc;
42
+ }
43
+ return acc;
44
+ }
45
+
46
+ // src/schema-dedupe.ts
47
+ function dedupeSchemaByIdAndType(schemas) {
48
+ const lastIndexByKey = /* @__PURE__ */ new Map();
49
+ for (let i = 0; i < schemas.length; i++) {
50
+ const node = schemas[i];
51
+ if (!node) continue;
52
+ const id = node["@id"];
53
+ const type = node["@type"];
54
+ if (typeof id === "string" && typeof type === "string") {
55
+ lastIndexByKey.set(`${type}::${id}`, i);
56
+ }
57
+ }
58
+ const drop = /* @__PURE__ */ new Set();
59
+ for (let i = 0; i < schemas.length; i++) {
60
+ const node = schemas[i];
61
+ if (!node) continue;
62
+ const id = node["@id"];
63
+ const type = node["@type"];
64
+ if (typeof id === "string" && typeof type === "string") {
65
+ const k = `${type}::${id}`;
66
+ if (lastIndexByKey.get(k) !== i) drop.add(i);
67
+ }
68
+ }
69
+ return schemas.filter((_, i) => !drop.has(i));
70
+ }
71
+
72
+ // src/core.ts
73
+ function applyTitleTemplate(title, template) {
74
+ if (!template) return title;
75
+ return template.includes("%s") ? template.replace(/%s/g, title) : `${title} ${template}`.trim();
76
+ }
77
+ function resolveCanonical(canonical, baseUrl) {
78
+ if (!canonical) return void 0;
79
+ if (canonical.startsWith("http://") || canonical.startsWith("https://")) return canonical;
80
+ if (!baseUrl) return canonical;
81
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
82
+ const path = canonical.startsWith("/") ? canonical : `/${canonical}`;
83
+ return `${base}${path}`;
84
+ }
85
+ function mergeSchema(parent, child) {
86
+ return [...parent, ...child];
87
+ }
88
+ function createSEO(input, config) {
89
+ const mergedInput = runBeforeMergePlugins(input, config);
90
+ const rawTitle = mergedInput.meta?.title ?? mergedInput.title;
91
+ const title = typeof rawTitle === "string" ? rawTitle.trim() : "";
92
+ if (!title) {
93
+ throw new SEOError("VALIDATION", "title is required (set meta.title or top-level title).");
94
+ }
95
+ const description = mergedInput.meta?.description ?? mergedInput.description;
96
+ const canonical = resolveCanonical(
97
+ mergedInput.meta?.canonical ?? mergedInput.canonical,
98
+ config?.baseUrl
99
+ );
100
+ const robots = mergedInput.meta?.robots ?? mergedInput.robots ?? config?.defaultRobots;
101
+ const langMap = mergedInput.meta?.alternates?.languages;
102
+ const alternates = langMap !== void 0 && Object.keys(langMap).length > 0 ? { languages: langMap } : void 0;
103
+ const meta = {
104
+ title: applyTitleTemplate(title, config?.titleTemplate),
105
+ ...description !== void 0 ? { description } : {},
106
+ ...canonical !== void 0 ? { canonical } : {},
107
+ ...robots !== void 0 ? { robots } : {},
108
+ ...alternates !== void 0 ? { alternates } : {}
109
+ };
110
+ const mergeOg = config?.features?.openGraphMerge !== false;
111
+ const ogBase = mergedInput.openGraph ?? {};
112
+ const ogTitle = mergeOg ? mergedInput.openGraph?.title ?? meta.title : mergedInput.openGraph?.title;
113
+ const ogDesc = mergeOg ? mergedInput.openGraph?.description ?? meta.description : mergedInput.openGraph?.description;
114
+ let openGraph;
115
+ if (mergeOg) {
116
+ openGraph = {
117
+ ...ogBase,
118
+ title: ogTitle ?? meta.title,
119
+ ...ogDesc !== void 0 ? { description: ogDesc } : {}
120
+ };
121
+ } else if (Object.keys(ogBase).length > 0 || ogTitle !== void 0 || ogDesc !== void 0) {
122
+ openGraph = {
123
+ ...ogBase,
124
+ ...ogTitle !== void 0 ? { title: ogTitle } : {},
125
+ ...ogDesc !== void 0 ? { description: ogDesc } : {}
126
+ };
127
+ }
128
+ const ogFirstImage = mergedInput.openGraph?.images?.[0]?.url;
129
+ const twTitle = mergedInput.twitter?.title ?? meta.title;
130
+ const twDesc = mergedInput.twitter?.description ?? meta.description;
131
+ const twImage = mergedInput.twitter?.image ?? (mergeOg ? ogFirstImage : void 0);
132
+ const twitter = {
133
+ ...mergedInput.twitter ?? {},
134
+ card: mergedInput.twitter?.card ?? "summary_large_image",
135
+ title: twTitle,
136
+ ...twDesc !== void 0 ? { description: twDesc } : {},
137
+ ...twImage !== void 0 ? { image: twImage } : {}
138
+ };
139
+ const parentSchema = [];
140
+ let childSchema = mergedInput.schema ?? [];
141
+ if (config?.schemaMerge && typeof config.schemaMerge === "object" && config.schemaMerge.dedupeByIdAndType === true) {
142
+ childSchema = dedupeSchemaByIdAndType(childSchema);
143
+ }
144
+ const schema = mergeSchema(parentSchema, childSchema);
145
+ const doc = {
146
+ meta,
147
+ ...openGraph !== void 0 ? { openGraph } : {},
148
+ twitter,
149
+ schema
150
+ };
151
+ let out = runAfterMergePlugins(doc, config);
152
+ if (config?.features?.jsonLd === false) {
153
+ out = { ...out, schema: [] };
154
+ }
155
+ return out;
156
+ }
157
+ function mergeLanguageAlternates(parent, child) {
158
+ const merged = { ...parent, ...child };
159
+ return Object.keys(merged).length > 0 ? merged : void 0;
160
+ }
161
+ function withSEO(parent, child, config) {
162
+ return mergeSEO(parent, child, config);
163
+ }
164
+ function mergeSEO(parent, child, config) {
165
+ const pMeta = parent.meta;
166
+ const cMeta = child.meta ?? {};
167
+ const { alternates: pAlt, ...pRest } = pMeta;
168
+ const { alternates: cAlt, ...cRest } = cMeta;
169
+ const mergedLang = mergeLanguageAlternates(pAlt?.languages, cAlt?.languages);
170
+ const mergedAlternates = mergedLang !== void 0 ? { languages: mergedLang } : void 0;
171
+ const mergedChild = {
172
+ ...child,
173
+ meta: {
174
+ ...pRest,
175
+ ...cRest,
176
+ title: cRest.title ?? child.title ?? pMeta.title,
177
+ description: cRest.description ?? child.description ?? pMeta.description,
178
+ canonical: cRest.canonical ?? child.canonical ?? pMeta.canonical,
179
+ robots: cRest.robots ?? child.robots ?? pMeta.robots,
180
+ ...mergedAlternates !== void 0 ? { alternates: mergedAlternates } : {}
181
+ },
182
+ openGraph: { ...parent.openGraph ?? {}, ...child.openGraph },
183
+ twitter: { ...parent.twitter ?? {}, ...child.twitter },
184
+ schema: [...parent.schema, ...child.schema ?? []]
185
+ };
186
+ return createSEO(mergedChild, config);
187
+ }
188
+
189
+ // src/schema.ts
190
+ function webPage(parts) {
191
+ return {
192
+ "@context": "https://schema.org",
193
+ "@type": "WebPage",
194
+ name: parts.name,
195
+ ...parts.description !== void 0 ? { description: parts.description } : {},
196
+ url: parts.url
197
+ };
198
+ }
199
+ function article(parts) {
200
+ return {
201
+ "@context": "https://schema.org",
202
+ "@type": "Article",
203
+ headline: parts.headline,
204
+ ...parts.description !== void 0 ? { description: parts.description } : {},
205
+ ...parts.datePublished !== void 0 ? { datePublished: parts.datePublished } : {},
206
+ url: parts.url
207
+ };
208
+ }
209
+ function organization(parts) {
210
+ return {
211
+ "@context": "https://schema.org",
212
+ "@type": "Organization",
213
+ name: parts.name,
214
+ ...parts.url !== void 0 ? { url: parts.url } : {},
215
+ ...parts.logo !== void 0 ? { logo: parts.logo } : {}
216
+ };
217
+ }
218
+ function person(parts) {
219
+ return {
220
+ "@context": "https://schema.org",
221
+ "@type": "Person",
222
+ name: parts.name,
223
+ ...parts.url !== void 0 ? { url: parts.url } : {}
224
+ };
225
+ }
226
+ function product(parts) {
227
+ return {
228
+ "@context": "https://schema.org",
229
+ "@type": "Product",
230
+ name: parts.name,
231
+ url: parts.url,
232
+ ...parts.description !== void 0 ? { description: parts.description } : {},
233
+ ...parts.sku !== void 0 ? { sku: parts.sku } : {},
234
+ ...parts.image !== void 0 ? { image: parts.image } : {}
235
+ };
236
+ }
237
+ function breadcrumbList(parts) {
238
+ return {
239
+ "@context": "https://schema.org",
240
+ "@type": "BreadcrumbList",
241
+ itemListElement: parts.items.map((item, i) => ({
242
+ "@type": "ListItem",
243
+ position: i + 1,
244
+ name: item.name,
245
+ item: item.url
246
+ }))
247
+ };
248
+ }
249
+ function faqPage(parts) {
250
+ return {
251
+ "@context": "https://schema.org",
252
+ "@type": "FAQPage",
253
+ mainEntity: parts.questions.map((q) => ({
254
+ "@type": "Question",
255
+ name: q.question,
256
+ acceptedAnswer: { "@type": "Answer", text: q.answer }
257
+ }))
258
+ };
259
+ }
260
+ function techArticle(parts) {
261
+ return {
262
+ "@context": "https://schema.org",
263
+ "@type": "TechArticle",
264
+ headline: parts.headline,
265
+ ...parts.description !== void 0 ? { description: parts.description } : {},
266
+ ...parts.datePublished !== void 0 ? { datePublished: parts.datePublished } : {},
267
+ url: parts.url
268
+ };
269
+ }
270
+ function customSchema(node) {
271
+ return node;
272
+ }
273
+
274
+ // src/serialize.ts
275
+ function validateJSONLDNode(node, path = "root") {
276
+ if (node === null || node === void 0) {
277
+ throw new SEOError("VALIDATION", `JSON-LD node at ${path} is null or undefined`);
278
+ }
279
+ if (typeof node !== "object") {
280
+ throw new SEOError("VALIDATION", `JSON-LD node at ${path} must be an object`);
281
+ }
282
+ const obj = node;
283
+ if ("@context" in obj) {
284
+ const context = obj["@context"];
285
+ if (typeof context === "string") {
286
+ if (context !== "https://schema.org" && !context.startsWith("https://schema.org/")) {
287
+ console.warn(
288
+ `[@better-seo/core] Non-standard @context at ${path}: ${context}. Only https://schema.org is recommended for security.`
289
+ );
290
+ }
291
+ } else if (typeof context !== "object" || context === null) {
292
+ throw new SEOError("VALIDATION", `@context at ${path} must be a string or object`);
293
+ }
294
+ }
295
+ if ("@type" in obj) {
296
+ const type = obj["@type"];
297
+ if (typeof type !== "string" || type.length === 0) {
298
+ throw new SEOError("VALIDATION", `@type at ${path} must be a non-empty string`);
299
+ }
300
+ if (type.includes("<") || type.includes(">") || type.includes('"')) {
301
+ throw new SEOError("VALIDATION", `@type at ${path} contains invalid characters: ${type}`);
302
+ }
303
+ }
304
+ for (const key of Reflect.ownKeys(obj)) {
305
+ if (typeof key !== "string") {
306
+ throw new SEOError("VALIDATION", `JSON-LD keys at ${path} must be strings`);
307
+ }
308
+ const value = obj[key];
309
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
310
+ throw new SEOError("VALIDATION", `Dangerous key "${key}" detected at ${path}`);
311
+ }
312
+ if (typeof value === "object" && value !== null) {
313
+ validateJSONLDNode(value, `${path}.${key}`);
314
+ }
315
+ }
316
+ }
317
+ function serializeJSONLD(data) {
318
+ if (Array.isArray(data)) {
319
+ data.forEach((node, index) => validateJSONLDNode(node, `array[${index}]`));
320
+ } else {
321
+ validateJSONLDNode(data, "root");
322
+ }
323
+ const sanitized = JSON.parse(JSON.stringify(data));
324
+ return JSON.stringify(sanitized);
325
+ }
326
+
327
+ // src/render.ts
328
+ function renderTags(seo) {
329
+ const tags = [];
330
+ tags.push({ kind: "meta", name: "title", content: seo.meta.title });
331
+ if (seo.meta.description) {
332
+ tags.push({ kind: "meta", name: "description", content: seo.meta.description });
333
+ }
334
+ if (seo.meta.canonical) {
335
+ tags.push({ kind: "link", rel: "canonical", href: seo.meta.canonical });
336
+ }
337
+ if (seo.meta.robots) {
338
+ tags.push({ kind: "meta", name: "robots", content: seo.meta.robots });
339
+ }
340
+ const langs = seo.meta.alternates?.languages;
341
+ if (langs) {
342
+ for (const [hreflang, href] of Object.entries(langs)) {
343
+ tags.push({ kind: "link", rel: "alternate", hreflang, href });
344
+ }
345
+ }
346
+ if (seo.openGraph?.title) {
347
+ tags.push({ kind: "meta", property: "og:title", content: seo.openGraph.title });
348
+ }
349
+ if (seo.openGraph?.description) {
350
+ tags.push({ kind: "meta", property: "og:description", content: seo.openGraph.description });
351
+ }
352
+ const ogImages = seo.openGraph?.images;
353
+ if (ogImages?.length) {
354
+ const first = ogImages[0];
355
+ if (first?.url) tags.push({ kind: "meta", property: "og:image", content: first.url });
356
+ if (first?.width !== void 0) {
357
+ tags.push({ kind: "meta", property: "og:image:width", content: String(first.width) });
358
+ }
359
+ if (first?.height !== void 0) {
360
+ tags.push({ kind: "meta", property: "og:image:height", content: String(first.height) });
361
+ }
362
+ if (first?.alt) tags.push({ kind: "meta", property: "og:image:alt", content: first.alt });
363
+ }
364
+ if (seo.twitter?.card) {
365
+ tags.push({ kind: "meta", name: "twitter:card", content: seo.twitter.card });
366
+ }
367
+ if (seo.twitter?.title) {
368
+ tags.push({ kind: "meta", name: "twitter:title", content: seo.twitter.title });
369
+ }
370
+ if (seo.twitter?.description) {
371
+ tags.push({ kind: "meta", name: "twitter:description", content: seo.twitter.description });
372
+ }
373
+ if (seo.twitter?.image) {
374
+ tags.push({ kind: "meta", name: "twitter:image", content: seo.twitter.image });
375
+ }
376
+ for (const node of seo.schema) {
377
+ tags.push({ kind: "script-jsonld", json: serializeJSONLD(node) });
378
+ }
379
+ return tags;
380
+ }
381
+
382
+ // src/validate.ts
383
+ function shouldRun(options) {
384
+ if (options?.enabled === false) return false;
385
+ const isProduction = typeof process !== "undefined" && typeof process.env === "object" && process.env !== null && process.env.NODE_ENV === "production";
386
+ if (isProduction) return false;
387
+ return true;
388
+ }
389
+ function validateSEO(seo, options) {
390
+ if (!shouldRun(options)) return [];
391
+ const titleMax = options?.titleMaxLength ?? 60;
392
+ const descMax = options?.descriptionMaxLength ?? 165;
393
+ const log = options?.log !== false;
394
+ const requireDesc = options?.requireDescription === true;
395
+ const issues = [];
396
+ if (!seo.meta.title?.trim()) {
397
+ issues.push({
398
+ code: "TITLE_EMPTY",
399
+ field: "meta.title",
400
+ message: "empty title",
401
+ severity: "error"
402
+ });
403
+ } else if (seo.meta.title.length > titleMax) {
404
+ issues.push({
405
+ code: "TITLE_TOO_LONG",
406
+ field: "meta.title",
407
+ message: `length ${seo.meta.title.length} exceeds recommended ${titleMax}`,
408
+ severity: "warning"
409
+ });
410
+ }
411
+ if (!seo.meta.description?.trim()) {
412
+ issues.push({
413
+ code: requireDesc ? "DESCRIPTION_REQUIRED" : "DESCRIPTION_MISSING",
414
+ field: "meta.description",
415
+ message: requireDesc ? "description is required" : "missing description",
416
+ severity: requireDesc ? "error" : "warning"
417
+ });
418
+ } else if (seo.meta.description.length > descMax) {
419
+ issues.push({
420
+ code: "DESCRIPTION_TOO_LONG",
421
+ field: "meta.description",
422
+ message: `length ${seo.meta.description.length} exceeds recommended ${descMax}`,
423
+ severity: "warning"
424
+ });
425
+ }
426
+ const firstOg = seo.openGraph?.images?.[0];
427
+ if (firstOg?.width !== void 0 && firstOg.width < 1200) {
428
+ issues.push({
429
+ code: "OG_IMAGE_NARROW",
430
+ field: "openGraph.images[0].width",
431
+ message: "OG image width under 1200px",
432
+ severity: "warning"
433
+ });
434
+ }
435
+ for (let i = 0; i < seo.schema.length; i++) {
436
+ const node = seo.schema[i];
437
+ if (!node?.["@type"]) {
438
+ issues.push({
439
+ code: "SCHEMA_MISSING_TYPE",
440
+ field: `schema[${i}]`,
441
+ message: "missing @type",
442
+ severity: "error"
443
+ });
444
+ }
445
+ }
446
+ if (log) {
447
+ for (const i of issues) {
448
+ console.warn(
449
+ `[@better-seo/core] validateSEO [${i.severity}] ${i.code} ${i.field}: ${i.message}`
450
+ );
451
+ }
452
+ }
453
+ return issues;
454
+ }
455
+
456
+ // src/adapters/registry.ts
457
+ var adapters = /* @__PURE__ */ new Map();
458
+ function validateAdapterId(id) {
459
+ if (!id || typeof id !== "string") {
460
+ throw new SEOError("VALIDATION", "Adapter ID must be a non-empty string");
461
+ }
462
+ if (id.length > 64) {
463
+ throw new SEOError("VALIDATION", `Adapter ID too long: ${id.length} chars (max 64)`);
464
+ }
465
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
466
+ throw new SEOError("VALIDATION", `Adapter ID contains invalid characters: ${id}`);
467
+ }
468
+ if (adapters.has(id)) {
469
+ console.warn(
470
+ `[@better-seo/core] Overriding existing adapter: "${id}". This may indicate a dependency conflict or malicious package.`
471
+ );
472
+ }
473
+ }
474
+ function registerAdapter(adapter) {
475
+ if (!adapter || typeof adapter !== "object") {
476
+ throw new SEOError("VALIDATION", "Adapter must be an object");
477
+ }
478
+ validateAdapterId(adapter.id);
479
+ adapters.set(adapter.id, adapter);
480
+ }
481
+ function getAdapter(id) {
482
+ return adapters.get(id);
483
+ }
484
+ function listAdapterIds() {
485
+ return [...adapters.keys()];
486
+ }
487
+
488
+ // src/context.ts
489
+ function createSEOContext(config) {
490
+ return {
491
+ config,
492
+ createSEO: (input) => createSEO(input, config),
493
+ mergeSEO: (parent, child) => mergeSEO(parent, child, config)
494
+ };
495
+ }
496
+
497
+ // src/singleton.ts
498
+ var globalConfig;
499
+ function initSEO(config) {
500
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
501
+ console.warn(
502
+ "[@better-seo/core] \u26A0\uFE0F initSEO() uses global state and is NOT safe for multi-tenant or serverless environments. Use createSEOContext() instead. See: https://github.com/OWNER/better-seo-js/blob/main/internal-docs/ARCHITECTURE.md"
503
+ );
504
+ }
505
+ globalConfig = config;
506
+ }
507
+ function getGlobalSEOConfig() {
508
+ return globalConfig;
509
+ }
510
+ function resetSEOConfigForTests() {
511
+ globalConfig = void 0;
512
+ }
513
+
514
+ // src/voila.ts
515
+ function seoForFramework(adapterId, input, config) {
516
+ const adapter = getAdapter(adapterId);
517
+ if (!adapter) {
518
+ throw new SEOError(
519
+ "ADAPTER_NOT_FOUND",
520
+ `no adapter "${adapterId}" registered (import your framework package, e.g. @better-seo/next).`
521
+ );
522
+ }
523
+ const doc = createSEO(input, config);
524
+ return adapter.toFramework(doc);
525
+ }
526
+ function useSEO() {
527
+ throw new SEOError("USE_SEO_NOT_AVAILABLE");
528
+ }
529
+
530
+ // src/rules.ts
531
+ function normalizeRoutePath(path) {
532
+ if (!path || path === "/") return "/";
533
+ const t = path.startsWith("/") ? path : `/${path}`;
534
+ const c = t.replace(/\/+/g, "/");
535
+ return c.endsWith("/") && c.length > 1 ? c.slice(0, -1) : c;
536
+ }
537
+ function pathSegments(path) {
538
+ const n = normalizeRoutePath(path);
539
+ if (n === "/") return [];
540
+ return n.slice(1).split("/");
541
+ }
542
+ function matchSegment(pat, seg) {
543
+ if (pat === "*") return true;
544
+ if (pat.endsWith("*") && pat.length > 1) {
545
+ return seg.startsWith(pat.slice(0, -1));
546
+ }
547
+ return pat === seg;
548
+ }
549
+ function matchRouteGlob(pattern, route) {
550
+ const ps = pathSegments(pattern);
551
+ const rs = pathSegments(route);
552
+ const dfs = (pi, ri) => {
553
+ if (pi === ps.length) return ri === rs.length;
554
+ if (ps[pi] === "**") {
555
+ if (dfs(pi + 1, ri)) return true;
556
+ if (ri < rs.length && dfs(pi, ri + 1)) return true;
557
+ return false;
558
+ }
559
+ if (ri === rs.length) return false;
560
+ if (!matchSegment(ps[pi], rs[ri])) return false;
561
+ return dfs(pi + 1, ri + 1);
562
+ };
563
+ return dfs(0, 0);
564
+ }
565
+ function matchRouteLegacyStar(pattern, route) {
566
+ const prefix = pattern.slice(0, -1);
567
+ const r = normalizeRoutePath(route);
568
+ const p = normalizeRoutePath(prefix);
569
+ if (r === p) return true;
570
+ return r.startsWith(prefix) || r.startsWith(`${p}/`);
571
+ }
572
+ function matchSingleSegmentStars(pattern, route) {
573
+ const ps = pathSegments(pattern);
574
+ const rs = pathSegments(route);
575
+ if (ps.length !== rs.length) return false;
576
+ for (let i = 0; i < ps.length; i++) {
577
+ if (!matchSegment(ps[i], rs[i])) return false;
578
+ }
579
+ return true;
580
+ }
581
+ function matchRoute(pattern, route) {
582
+ if (pattern === "*") return true;
583
+ const r = route || "/";
584
+ if (pattern.includes("**")) {
585
+ return matchRouteGlob(pattern, r);
586
+ }
587
+ if (pattern.endsWith("*") && pattern.indexOf("*") === pattern.length - 1) {
588
+ return matchRouteLegacyStar(pattern, r);
589
+ }
590
+ if (pattern.includes("*")) {
591
+ return matchSingleSegmentStars(pattern, r);
592
+ }
593
+ return normalizeRoutePath(r) === normalizeRoutePath(pattern);
594
+ }
595
+ function applyRules(route, rules) {
596
+ const sorted = [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
597
+ let acc = {};
598
+ for (const rule of sorted) {
599
+ if (matchRoute(rule.match, route)) {
600
+ acc = { ...acc, ...rule.seo };
601
+ }
602
+ }
603
+ return acc;
604
+ }
605
+ function applyRulesToSEO(route, base, rules, config) {
606
+ const partial = applyRules(route, rules);
607
+ if (Object.keys(partial).length === 0) return base;
608
+ return mergeSEO(base, partial, config);
609
+ }
610
+ function createSEOForRoute(route, input, rules, config) {
611
+ const mergedInput = { ...applyRules(route, rules), ...input };
612
+ return createSEO(mergedInput, config);
613
+ }
614
+
615
+ // src/migrate.ts
616
+ function fromNextSeo(_nextSeoExport) {
617
+ throw new SEOError("MIGRATE_NOT_IMPLEMENTED");
618
+ }
619
+
620
+ exports.SEOError = SEOError;
621
+ exports.applyRules = applyRules;
622
+ exports.applyRulesToSEO = applyRulesToSEO;
623
+ exports.article = article;
624
+ exports.breadcrumbList = breadcrumbList;
625
+ exports.createSEO = createSEO;
626
+ exports.createSEOContext = createSEOContext;
627
+ exports.createSEOForRoute = createSEOForRoute;
628
+ exports.customSchema = customSchema;
629
+ exports.defineSEOPlugin = defineSEOPlugin;
630
+ exports.faqPage = faqPage;
631
+ exports.fromNextSeo = fromNextSeo;
632
+ exports.getAdapter = getAdapter;
633
+ exports.getGlobalSEOConfig = getGlobalSEOConfig;
634
+ exports.initSEO = initSEO;
635
+ exports.isSEOError = isSEOError;
636
+ exports.listAdapterIds = listAdapterIds;
637
+ exports.mergeSEO = mergeSEO;
638
+ exports.organization = organization;
639
+ exports.person = person;
640
+ exports.product = product;
641
+ exports.registerAdapter = registerAdapter;
642
+ exports.renderTags = renderTags;
643
+ exports.resetSEOConfigForTests = resetSEOConfigForTests;
644
+ exports.seoForFramework = seoForFramework;
645
+ exports.serializeJSONLD = serializeJSONLD;
646
+ exports.techArticle = techArticle;
647
+ exports.useSEO = useSEO;
648
+ exports.validateSEO = validateSEO;
649
+ exports.webPage = webPage;
650
+ exports.withSEO = withSEO;
651
+ //# sourceMappingURL=index.cjs.map
652
+ //# sourceMappingURL=index.cjs.map