@better-seo/core 0.0.1 → 0.0.2
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 +1 -1
- package/dist/index.cjs +390 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -7
- package/dist/index.d.ts +106 -7
- package/dist/index.js +385 -59
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +1021 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +21 -0
- package/dist/node.d.ts +21 -0
- package/dist/node.js +980 -0
- package/dist/node.js.map +1 -0
- package/package.json +18 -12
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Framework-agnostic SEO **document model** for JavaScript and TypeScript: merge partial inputs into a canonical **`SEO`**, serialize HTML-safe **JSON-LD**, validate in development, render vanilla **tag descriptors**, and register framework adapters. **Zero runtime npm dependencies** so Node, Edge, and browser bundles stay light.
|
|
8
8
|
|
|
9
|
-
**Docs:** [Monorepo README](../../README.md) · [
|
|
9
|
+
**Docs:** [Monorepo README](../../README.md) · [Recipes](../../docs/recipes/) · [CONTRIBUTING](../../CONTRIBUTING.md)
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
var messages = {
|
|
5
5
|
VALIDATION: "Invalid or incomplete SEO input.",
|
|
6
6
|
ADAPTER_NOT_FOUND: "No adapter registered for this framework. Import the adapter package (e.g. @better-seo/next) before calling seoForFramework.",
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
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.",
|
|
8
|
+
USE_SEO_NO_PROVIDER: "useSEO() must be used within <SEOProvider> from @better-seo/react."
|
|
9
9
|
};
|
|
10
10
|
var SEOError = class extends Error {
|
|
11
11
|
code;
|
|
@@ -42,6 +42,15 @@ function runAfterMergePlugins(seo, config) {
|
|
|
42
42
|
}
|
|
43
43
|
return acc;
|
|
44
44
|
}
|
|
45
|
+
function runOnRenderTagPlugins(tags, seo, config) {
|
|
46
|
+
const list = config?.plugins ?? [];
|
|
47
|
+
let acc = [...tags];
|
|
48
|
+
for (const p of list) {
|
|
49
|
+
const next = p.onRenderTags?.(acc, { seo, config });
|
|
50
|
+
if (next !== void 0) acc = [...next];
|
|
51
|
+
}
|
|
52
|
+
return acc;
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
// src/schema-dedupe.ts
|
|
47
56
|
function dedupeSchemaByIdAndType(schemas) {
|
|
@@ -100,12 +109,23 @@ function createSEO(input, config) {
|
|
|
100
109
|
const robots = mergedInput.meta?.robots ?? mergedInput.robots ?? config?.defaultRobots;
|
|
101
110
|
const langMap = mergedInput.meta?.alternates?.languages;
|
|
102
111
|
const alternates = langMap !== void 0 && Object.keys(langMap).length > 0 ? { languages: langMap } : void 0;
|
|
112
|
+
const verification = mergedInput.meta?.verification;
|
|
113
|
+
const hasVer = Boolean(
|
|
114
|
+
verification && (verification.google || verification.yahoo || verification.yandex || verification.me || verification.other !== void 0 && Object.keys(verification.other).length > 0)
|
|
115
|
+
);
|
|
116
|
+
const rawPag = mergedInput.meta?.pagination;
|
|
117
|
+
const pagination = rawPag?.previous !== void 0 || rawPag?.next !== void 0 ? {
|
|
118
|
+
...rawPag.previous !== void 0 ? { previous: rawPag.previous } : {},
|
|
119
|
+
...rawPag.next !== void 0 ? { next: rawPag.next } : {}
|
|
120
|
+
} : void 0;
|
|
103
121
|
const meta = {
|
|
104
122
|
title: applyTitleTemplate(title, config?.titleTemplate),
|
|
105
123
|
...description !== void 0 ? { description } : {},
|
|
106
124
|
...canonical !== void 0 ? { canonical } : {},
|
|
107
125
|
...robots !== void 0 ? { robots } : {},
|
|
108
|
-
...alternates !== void 0 ? { alternates } : {}
|
|
126
|
+
...alternates !== void 0 ? { alternates } : {},
|
|
127
|
+
...hasVer ? { verification } : {},
|
|
128
|
+
...pagination !== void 0 ? { pagination } : {}
|
|
109
129
|
};
|
|
110
130
|
const mergeOg = config?.features?.openGraphMerge !== false;
|
|
111
131
|
const ogBase = mergedInput.openGraph ?? {};
|
|
@@ -161,13 +181,34 @@ function mergeLanguageAlternates(parent, child) {
|
|
|
161
181
|
function withSEO(parent, child, config) {
|
|
162
182
|
return mergeSEO(parent, child, config);
|
|
163
183
|
}
|
|
184
|
+
function mergeVerification(parent, child) {
|
|
185
|
+
if (!parent && !child) return void 0;
|
|
186
|
+
const o = {
|
|
187
|
+
...parent,
|
|
188
|
+
...child,
|
|
189
|
+
other: parent?.other || child?.other ? { ...parent?.other ?? {}, ...child?.other ?? {} } : void 0
|
|
190
|
+
};
|
|
191
|
+
const has = o.google || o.yahoo || o.yandex || o.me || o.other && Object.keys(o.other).length > 0;
|
|
192
|
+
return has ? o : void 0;
|
|
193
|
+
}
|
|
194
|
+
function mergePagination(parent, child) {
|
|
195
|
+
if (!parent && !child) return void 0;
|
|
196
|
+
const out = {
|
|
197
|
+
...parent,
|
|
198
|
+
...child
|
|
199
|
+
};
|
|
200
|
+
if (out.previous === void 0 && out.next === void 0) return void 0;
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
164
203
|
function mergeSEO(parent, child, config) {
|
|
165
204
|
const pMeta = parent.meta;
|
|
166
205
|
const cMeta = child.meta ?? {};
|
|
167
|
-
const { alternates: pAlt, ...pRest } = pMeta;
|
|
168
|
-
const { alternates: cAlt, ...cRest } = cMeta;
|
|
206
|
+
const { alternates: pAlt, verification: pVer, pagination: pPag, ...pRest } = pMeta;
|
|
207
|
+
const { alternates: cAlt, verification: cVer, pagination: cPag, ...cRest } = cMeta;
|
|
169
208
|
const mergedLang = mergeLanguageAlternates(pAlt?.languages, cAlt?.languages);
|
|
170
209
|
const mergedAlternates = mergedLang !== void 0 ? { languages: mergedLang } : void 0;
|
|
210
|
+
const mergedVer = mergeVerification(pVer, cVer);
|
|
211
|
+
const mergedPag = mergePagination(pPag, cPag);
|
|
171
212
|
const mergedChild = {
|
|
172
213
|
...child,
|
|
173
214
|
meta: {
|
|
@@ -177,7 +218,9 @@ function mergeSEO(parent, child, config) {
|
|
|
177
218
|
description: cRest.description ?? child.description ?? pMeta.description,
|
|
178
219
|
canonical: cRest.canonical ?? child.canonical ?? pMeta.canonical,
|
|
179
220
|
robots: cRest.robots ?? child.robots ?? pMeta.robots,
|
|
180
|
-
...mergedAlternates !== void 0 ? { alternates: mergedAlternates } : {}
|
|
221
|
+
...mergedAlternates !== void 0 ? { alternates: mergedAlternates } : {},
|
|
222
|
+
...mergedVer !== void 0 ? { verification: mergedVer } : {},
|
|
223
|
+
...mergedPag !== void 0 ? { pagination: mergedPag } : {}
|
|
181
224
|
},
|
|
182
225
|
openGraph: { ...parent.openGraph ?? {}, ...child.openGraph },
|
|
183
226
|
twitter: { ...parent.twitter ?? {}, ...child.twitter },
|
|
@@ -325,7 +368,28 @@ function serializeJSONLD(data) {
|
|
|
325
368
|
}
|
|
326
369
|
|
|
327
370
|
// src/render.ts
|
|
328
|
-
function
|
|
371
|
+
function verificationMetaName(key) {
|
|
372
|
+
if (key === "google") return "google-site-verification";
|
|
373
|
+
if (key === "yahoo") return "y_key";
|
|
374
|
+
if (key === "yandex") return "yandex-verification";
|
|
375
|
+
if (key === "me") return "me";
|
|
376
|
+
return key;
|
|
377
|
+
}
|
|
378
|
+
function pushVerificationMeta(tags, v) {
|
|
379
|
+
const add = (name, content) => {
|
|
380
|
+
tags.push({ kind: "meta", name, content });
|
|
381
|
+
};
|
|
382
|
+
if (v.google) add(verificationMetaName("google"), v.google);
|
|
383
|
+
if (v.yahoo) add(verificationMetaName("yahoo"), v.yahoo);
|
|
384
|
+
if (v.yandex) add(verificationMetaName("yandex"), v.yandex);
|
|
385
|
+
if (v.me) add(verificationMetaName("me"), v.me);
|
|
386
|
+
if (v.other) {
|
|
387
|
+
for (const [k, val] of Object.entries(v.other)) {
|
|
388
|
+
add(verificationMetaName(k), val);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function renderTags(seo, config) {
|
|
329
393
|
const tags = [];
|
|
330
394
|
tags.push({ kind: "meta", name: "title", content: seo.meta.title });
|
|
331
395
|
if (seo.meta.description) {
|
|
@@ -337,6 +401,16 @@ function renderTags(seo) {
|
|
|
337
401
|
if (seo.meta.robots) {
|
|
338
402
|
tags.push({ kind: "meta", name: "robots", content: seo.meta.robots });
|
|
339
403
|
}
|
|
404
|
+
if (seo.meta.verification) {
|
|
405
|
+
pushVerificationMeta(tags, seo.meta.verification);
|
|
406
|
+
}
|
|
407
|
+
const pag = seo.meta.pagination;
|
|
408
|
+
if (pag?.previous) {
|
|
409
|
+
tags.push({ kind: "link", rel: "prev", href: pag.previous });
|
|
410
|
+
}
|
|
411
|
+
if (pag?.next) {
|
|
412
|
+
tags.push({ kind: "link", rel: "next", href: pag.next });
|
|
413
|
+
}
|
|
340
414
|
const langs = seo.meta.alternates?.languages;
|
|
341
415
|
if (langs) {
|
|
342
416
|
for (const [hreflang, href] of Object.entries(langs)) {
|
|
@@ -349,17 +423,83 @@ function renderTags(seo) {
|
|
|
349
423
|
if (seo.openGraph?.description) {
|
|
350
424
|
tags.push({ kind: "meta", property: "og:description", content: seo.openGraph.description });
|
|
351
425
|
}
|
|
426
|
+
if (seo.openGraph?.url) {
|
|
427
|
+
tags.push({ kind: "meta", property: "og:url", content: seo.openGraph.url });
|
|
428
|
+
}
|
|
429
|
+
if (seo.openGraph?.type) {
|
|
430
|
+
tags.push({ kind: "meta", property: "og:type", content: seo.openGraph.type });
|
|
431
|
+
}
|
|
432
|
+
if (seo.openGraph?.siteName) {
|
|
433
|
+
tags.push({ kind: "meta", property: "og:site_name", content: seo.openGraph.siteName });
|
|
434
|
+
}
|
|
435
|
+
if (seo.openGraph?.locale) {
|
|
436
|
+
tags.push({ kind: "meta", property: "og:locale", content: seo.openGraph.locale });
|
|
437
|
+
}
|
|
438
|
+
if (seo.openGraph?.publishedTime) {
|
|
439
|
+
tags.push({
|
|
440
|
+
kind: "meta",
|
|
441
|
+
property: "article:published_time",
|
|
442
|
+
content: seo.openGraph.publishedTime
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (seo.openGraph?.modifiedTime) {
|
|
446
|
+
tags.push({
|
|
447
|
+
kind: "meta",
|
|
448
|
+
property: "article:modified_time",
|
|
449
|
+
content: seo.openGraph.modifiedTime
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (seo.openGraph?.expirationTime) {
|
|
453
|
+
tags.push({
|
|
454
|
+
kind: "meta",
|
|
455
|
+
property: "article:expiration_time",
|
|
456
|
+
content: seo.openGraph.expirationTime
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (seo.openGraph?.section) {
|
|
460
|
+
tags.push({ kind: "meta", property: "article:section", content: seo.openGraph.section });
|
|
461
|
+
}
|
|
462
|
+
const authors = seo.openGraph?.authors;
|
|
463
|
+
if (authors?.length) {
|
|
464
|
+
for (const a of authors) {
|
|
465
|
+
if (a?.trim()) tags.push({ kind: "meta", property: "article:author", content: a.trim() });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const ogTags = seo.openGraph?.tags;
|
|
469
|
+
if (ogTags?.length) {
|
|
470
|
+
for (const t of ogTags) {
|
|
471
|
+
if (t?.trim()) tags.push({ kind: "meta", property: "article:tag", content: t.trim() });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const ogVideos = seo.openGraph?.videos;
|
|
475
|
+
if (ogVideos?.length) {
|
|
476
|
+
for (const v of ogVideos) {
|
|
477
|
+
if (!v?.url) continue;
|
|
478
|
+
tags.push({ kind: "meta", property: "og:video", content: v.url });
|
|
479
|
+
if (v.secureUrl) {
|
|
480
|
+
tags.push({ kind: "meta", property: "og:video:secure_url", content: v.secureUrl });
|
|
481
|
+
}
|
|
482
|
+
if (v.type) tags.push({ kind: "meta", property: "og:video:type", content: v.type });
|
|
483
|
+
if (v.width !== void 0) {
|
|
484
|
+
tags.push({ kind: "meta", property: "og:video:width", content: String(v.width) });
|
|
485
|
+
}
|
|
486
|
+
if (v.height !== void 0) {
|
|
487
|
+
tags.push({ kind: "meta", property: "og:video:height", content: String(v.height) });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
352
491
|
const ogImages = seo.openGraph?.images;
|
|
353
492
|
if (ogImages?.length) {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
493
|
+
for (const img of ogImages) {
|
|
494
|
+
if (img?.url) tags.push({ kind: "meta", property: "og:image", content: img.url });
|
|
495
|
+
if (img?.width !== void 0) {
|
|
496
|
+
tags.push({ kind: "meta", property: "og:image:width", content: String(img.width) });
|
|
497
|
+
}
|
|
498
|
+
if (img?.height !== void 0) {
|
|
499
|
+
tags.push({ kind: "meta", property: "og:image:height", content: String(img.height) });
|
|
500
|
+
}
|
|
501
|
+
if (img?.alt) tags.push({ kind: "meta", property: "og:image:alt", content: img.alt });
|
|
361
502
|
}
|
|
362
|
-
if (first?.alt) tags.push({ kind: "meta", property: "og:image:alt", content: first.alt });
|
|
363
503
|
}
|
|
364
504
|
if (seo.twitter?.card) {
|
|
365
505
|
tags.push({ kind: "meta", name: "twitter:card", content: seo.twitter.card });
|
|
@@ -370,13 +510,19 @@ function renderTags(seo) {
|
|
|
370
510
|
if (seo.twitter?.description) {
|
|
371
511
|
tags.push({ kind: "meta", name: "twitter:description", content: seo.twitter.description });
|
|
372
512
|
}
|
|
513
|
+
if (seo.twitter?.site) {
|
|
514
|
+
tags.push({ kind: "meta", name: "twitter:site", content: seo.twitter.site });
|
|
515
|
+
}
|
|
516
|
+
if (seo.twitter?.creator) {
|
|
517
|
+
tags.push({ kind: "meta", name: "twitter:creator", content: seo.twitter.creator });
|
|
518
|
+
}
|
|
373
519
|
if (seo.twitter?.image) {
|
|
374
520
|
tags.push({ kind: "meta", name: "twitter:image", content: seo.twitter.image });
|
|
375
521
|
}
|
|
376
522
|
for (const node of seo.schema) {
|
|
377
523
|
tags.push({ kind: "script-jsonld", json: serializeJSONLD(node) });
|
|
378
524
|
}
|
|
379
|
-
return tags;
|
|
525
|
+
return runOnRenderTagPlugins(tags, seo, config);
|
|
380
526
|
}
|
|
381
527
|
|
|
382
528
|
// src/validate.ts
|
|
@@ -432,6 +578,15 @@ function validateSEO(seo, options) {
|
|
|
432
578
|
severity: "warning"
|
|
433
579
|
});
|
|
434
580
|
}
|
|
581
|
+
const ogType = seo.openGraph?.type;
|
|
582
|
+
if (ogType === "article" && !(seo.openGraph?.publishedTime && String(seo.openGraph.publishedTime).trim())) {
|
|
583
|
+
issues.push({
|
|
584
|
+
code: "ARTICLE_PUBLISHED_TIME_RECOMMENDED",
|
|
585
|
+
field: "openGraph.publishedTime",
|
|
586
|
+
message: "openGraph.type is article; set publishedTime (ISO-8601) for richer crawlers",
|
|
587
|
+
severity: "warning"
|
|
588
|
+
});
|
|
589
|
+
}
|
|
435
590
|
for (let i = 0; i < seo.schema.length; i++) {
|
|
436
591
|
const node = seo.schema[i];
|
|
437
592
|
if (!node?.["@type"]) {
|
|
@@ -484,47 +639,14 @@ function getAdapter(id) {
|
|
|
484
639
|
function listAdapterIds() {
|
|
485
640
|
return [...adapters.keys()];
|
|
486
641
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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);
|
|
642
|
+
function detectFramework() {
|
|
643
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
644
|
+
if (process.env.NEXT_RUNTIME) return "next";
|
|
645
|
+
return void 0;
|
|
525
646
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
647
|
+
function getDefaultAdapter() {
|
|
648
|
+
const id = detectFramework();
|
|
649
|
+
return id ? getAdapter(id) : void 0;
|
|
528
650
|
}
|
|
529
651
|
|
|
530
652
|
// src/rules.ts
|
|
@@ -612,9 +734,213 @@ function createSEOForRoute(route, input, rules, config) {
|
|
|
612
734
|
return createSEO(mergedInput, config);
|
|
613
735
|
}
|
|
614
736
|
|
|
737
|
+
// src/context.ts
|
|
738
|
+
function createSEOContext(config) {
|
|
739
|
+
const rules = config.rules ?? [];
|
|
740
|
+
return {
|
|
741
|
+
config,
|
|
742
|
+
createSEO: (input) => createSEO(input, config),
|
|
743
|
+
createSEOForRoute: (route, input) => createSEOForRoute(route, input, rules, config),
|
|
744
|
+
mergeSEO: (parent, child) => mergeSEO(parent, child, config)
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/singleton.ts
|
|
749
|
+
var globalConfig;
|
|
750
|
+
function initSEO(config) {
|
|
751
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
752
|
+
console.warn(
|
|
753
|
+
"[@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"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
globalConfig = config;
|
|
757
|
+
}
|
|
758
|
+
function getGlobalSEOConfig() {
|
|
759
|
+
return globalConfig;
|
|
760
|
+
}
|
|
761
|
+
function resetSEOConfigForTests() {
|
|
762
|
+
globalConfig = void 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/voila.ts
|
|
766
|
+
function seoForFramework(adapterId, input, config) {
|
|
767
|
+
const adapter = getAdapter(adapterId);
|
|
768
|
+
if (!adapter) {
|
|
769
|
+
throw new SEOError(
|
|
770
|
+
"ADAPTER_NOT_FOUND",
|
|
771
|
+
`no adapter "${adapterId}" registered (import your framework package, e.g. @better-seo/next).`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
const doc = createSEO(input, config);
|
|
775
|
+
return adapter.toFramework(doc);
|
|
776
|
+
}
|
|
777
|
+
function useSEO() {
|
|
778
|
+
throw new SEOError("USE_SEO_NOT_AVAILABLE");
|
|
779
|
+
}
|
|
780
|
+
function seoRoute(route, input, config) {
|
|
781
|
+
return createSEOForRoute(route, input, config?.rules ?? [], config);
|
|
782
|
+
}
|
|
783
|
+
|
|
615
784
|
// src/migrate.ts
|
|
616
|
-
function
|
|
617
|
-
|
|
785
|
+
function isObj(v) {
|
|
786
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
787
|
+
}
|
|
788
|
+
function pickStr(v) {
|
|
789
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
790
|
+
}
|
|
791
|
+
function mapOgImages(raw) {
|
|
792
|
+
if (!Array.isArray(raw)) return void 0;
|
|
793
|
+
const out = [];
|
|
794
|
+
for (const item of raw) {
|
|
795
|
+
if (typeof item === "string") {
|
|
796
|
+
out.push({ url: item });
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (!isObj(item)) continue;
|
|
800
|
+
const url = pickStr(item.url);
|
|
801
|
+
if (!url) continue;
|
|
802
|
+
out.push({
|
|
803
|
+
url,
|
|
804
|
+
...typeof item.width === "number" ? { width: item.width } : {},
|
|
805
|
+
...typeof item.height === "number" ? { height: item.height } : {},
|
|
806
|
+
...pickStr(item.alt) !== void 0 ? { alt: pickStr(item.alt) } : {}
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return out.length > 0 ? out : void 0;
|
|
810
|
+
}
|
|
811
|
+
function fromNextSeo(nextSeoExport) {
|
|
812
|
+
if (!isObj(nextSeoExport)) {
|
|
813
|
+
throw new SEOError("VALIDATION", "fromNextSeo expects a plain object (e.g. next-seo props).");
|
|
814
|
+
}
|
|
815
|
+
const o = nextSeoExport;
|
|
816
|
+
const og = isObj(o.openGraph) ? o.openGraph : void 0;
|
|
817
|
+
const tw = isObj(o.twitter) ? o.twitter : void 0;
|
|
818
|
+
const title = pickStr(o.title) ?? pickStr(o.defaultTitle) ?? (og ? pickStr(og.title) : void 0) ?? (tw ? pickStr(tw.title) : void 0);
|
|
819
|
+
const description = pickStr(o.description) ?? (og ? pickStr(og.description) : void 0) ?? (tw ? pickStr(tw.description) : void 0);
|
|
820
|
+
if (!title) {
|
|
821
|
+
throw new SEOError(
|
|
822
|
+
"VALIDATION",
|
|
823
|
+
"fromNextSeo could not infer a title. Set title, defaultTitle, or openGraph.title."
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
const canonical = pickStr(o.canonical) ?? (og ? pickStr(og.url) : void 0);
|
|
827
|
+
const noindex = o.noindex === true;
|
|
828
|
+
const nofollow = o.nofollow === true;
|
|
829
|
+
const robots = noindex || nofollow ? `${noindex ? "noindex" : "index"}, ${nofollow ? "nofollow" : "follow"}` : void 0;
|
|
830
|
+
let openGraph;
|
|
831
|
+
if (og) {
|
|
832
|
+
const images = mapOgImages(og.images);
|
|
833
|
+
const siteName = pickStr(og.siteName) ?? pickStr(og.site_name);
|
|
834
|
+
openGraph = {
|
|
835
|
+
...pickStr(og.title) !== void 0 ? { title: pickStr(og.title) } : {},
|
|
836
|
+
...pickStr(og.description) !== void 0 ? { description: pickStr(og.description) } : {},
|
|
837
|
+
...pickStr(og.url) !== void 0 ? { url: pickStr(og.url) } : {},
|
|
838
|
+
...pickStr(og.type) !== void 0 ? { type: pickStr(og.type) } : {},
|
|
839
|
+
...siteName !== void 0 ? { siteName } : {},
|
|
840
|
+
...pickStr(og.locale) !== void 0 ? { locale: pickStr(og.locale) } : {},
|
|
841
|
+
...images !== void 0 ? { images } : {}
|
|
842
|
+
};
|
|
843
|
+
if (Object.keys(openGraph).length === 0) openGraph = void 0;
|
|
844
|
+
}
|
|
845
|
+
let twitter;
|
|
846
|
+
if (tw) {
|
|
847
|
+
const cardRaw = pickStr(tw.card);
|
|
848
|
+
const card = cardRaw === "summary" || cardRaw === "summary_large_image" ? cardRaw : void 0;
|
|
849
|
+
const twImage = pickStr(tw.image) ?? pickStr(tw.imageSrc);
|
|
850
|
+
const twSite = pickStr(tw.site);
|
|
851
|
+
const twCreator = pickStr(tw.handle) ?? pickStr(tw.creator);
|
|
852
|
+
twitter = {
|
|
853
|
+
...card !== void 0 ? { card } : {},
|
|
854
|
+
...pickStr(tw.title) !== void 0 ? { title: pickStr(tw.title) } : {},
|
|
855
|
+
...pickStr(tw.description) !== void 0 ? { description: pickStr(tw.description) } : {},
|
|
856
|
+
...twImage !== void 0 ? { image: twImage } : {},
|
|
857
|
+
...twSite !== void 0 ? { site: twSite } : {},
|
|
858
|
+
...twCreator !== void 0 ? { creator: twCreator } : {}
|
|
859
|
+
};
|
|
860
|
+
if (Object.keys(twitter).length === 0) twitter = void 0;
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
title,
|
|
864
|
+
...description !== void 0 ? { description } : {},
|
|
865
|
+
...canonical !== void 0 ? { canonical } : {},
|
|
866
|
+
...robots !== void 0 ? { robots } : {},
|
|
867
|
+
...openGraph !== void 0 ? { openGraph } : {},
|
|
868
|
+
...twitter !== void 0 ? { twitter } : {}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/from-content.ts
|
|
873
|
+
function stripHtmlish(s) {
|
|
874
|
+
return s.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
875
|
+
}
|
|
876
|
+
function parseSimpleFrontmatter(raw) {
|
|
877
|
+
if (!raw.startsWith("---\n")) return { body: raw };
|
|
878
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
879
|
+
if (end === -1) return { body: raw };
|
|
880
|
+
const fmBlock = raw.slice(4, end);
|
|
881
|
+
const body = raw.slice(end + 5).trim();
|
|
882
|
+
let title;
|
|
883
|
+
let description;
|
|
884
|
+
for (const line of fmBlock.split("\n")) {
|
|
885
|
+
const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
886
|
+
if (!m) continue;
|
|
887
|
+
const key = m[1].toLowerCase();
|
|
888
|
+
let val = m[2].trim();
|
|
889
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
890
|
+
val = val.slice(1, -1);
|
|
891
|
+
}
|
|
892
|
+
if (key === "title") title = val;
|
|
893
|
+
if (key === "description") description = val;
|
|
894
|
+
}
|
|
895
|
+
return { body, title, description };
|
|
896
|
+
}
|
|
897
|
+
function fromContent(markdownOrPlain, options) {
|
|
898
|
+
const maxD = options?.maxDescriptionLength ?? 300;
|
|
899
|
+
const inferTitle = options?.inferTitleFromBody !== false;
|
|
900
|
+
const normalized = markdownOrPlain.replace(/\r\n/g, "\n");
|
|
901
|
+
const { body: fmBody, title: fmTitle, description: fmDesc } = parseSimpleFrontmatter(normalized);
|
|
902
|
+
let body = fmBody.trim();
|
|
903
|
+
if (!body && !fmTitle) {
|
|
904
|
+
return { title: "Untitled" };
|
|
905
|
+
}
|
|
906
|
+
let title = fmTitle;
|
|
907
|
+
let description = fmDesc;
|
|
908
|
+
const importStripped = body.split("\n").filter((line) => !/^\s*import\s+/.test(line)).join("\n").trim();
|
|
909
|
+
body = importStripped || body;
|
|
910
|
+
if (inferTitle) {
|
|
911
|
+
const h1 = /^#\s+(.+)$/m.exec(body);
|
|
912
|
+
if (!title && h1) {
|
|
913
|
+
title = h1[1].trim();
|
|
914
|
+
body = body.replace(h1[0], "").trim();
|
|
915
|
+
}
|
|
916
|
+
if (!title) {
|
|
917
|
+
const lines = body.split("\n");
|
|
918
|
+
const first = lines.find((l) => l.trim().length > 0) ?? "";
|
|
919
|
+
title = stripHtmlish(first).slice(0, 200) || "Untitled";
|
|
920
|
+
const idx = body.indexOf(first);
|
|
921
|
+
if (idx >= 0) body = body.slice(idx + first.length).trim();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (!description) {
|
|
925
|
+
const chunk = body.split(/\n\n+/).find((p) => p.trim().length > 0) ?? "";
|
|
926
|
+
const plain = stripHtmlish(chunk.replace(/^#+\s+.*$/m, "").trim());
|
|
927
|
+
description = plain.length > 0 ? plain.slice(0, maxD) : void 0;
|
|
928
|
+
}
|
|
929
|
+
const out = {};
|
|
930
|
+
if (title !== void 0) out.title = title;
|
|
931
|
+
if (description !== void 0) out.description = description;
|
|
932
|
+
if (out.title === void 0 && out.description === void 0) {
|
|
933
|
+
return { title: "Untitled" };
|
|
934
|
+
}
|
|
935
|
+
return out;
|
|
936
|
+
}
|
|
937
|
+
function fromMdxString(source, options) {
|
|
938
|
+
return fromContent(source, options);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/define-seo.ts
|
|
942
|
+
function defineSEO(input) {
|
|
943
|
+
return input;
|
|
618
944
|
}
|
|
619
945
|
|
|
620
946
|
exports.SEOError = SEOError;
|
|
@@ -626,10 +952,15 @@ exports.createSEO = createSEO;
|
|
|
626
952
|
exports.createSEOContext = createSEOContext;
|
|
627
953
|
exports.createSEOForRoute = createSEOForRoute;
|
|
628
954
|
exports.customSchema = customSchema;
|
|
955
|
+
exports.defineSEO = defineSEO;
|
|
629
956
|
exports.defineSEOPlugin = defineSEOPlugin;
|
|
957
|
+
exports.detectFramework = detectFramework;
|
|
630
958
|
exports.faqPage = faqPage;
|
|
959
|
+
exports.fromContent = fromContent;
|
|
960
|
+
exports.fromMdxString = fromMdxString;
|
|
631
961
|
exports.fromNextSeo = fromNextSeo;
|
|
632
962
|
exports.getAdapter = getAdapter;
|
|
963
|
+
exports.getDefaultAdapter = getDefaultAdapter;
|
|
633
964
|
exports.getGlobalSEOConfig = getGlobalSEOConfig;
|
|
634
965
|
exports.initSEO = initSEO;
|
|
635
966
|
exports.isSEOError = isSEOError;
|
|
@@ -642,6 +973,7 @@ exports.registerAdapter = registerAdapter;
|
|
|
642
973
|
exports.renderTags = renderTags;
|
|
643
974
|
exports.resetSEOConfigForTests = resetSEOConfigForTests;
|
|
644
975
|
exports.seoForFramework = seoForFramework;
|
|
976
|
+
exports.seoRoute = seoRoute;
|
|
645
977
|
exports.serializeJSONLD = serializeJSONLD;
|
|
646
978
|
exports.techArticle = techArticle;
|
|
647
979
|
exports.useSEO = useSEO;
|