@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 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) · [Usage & errors](../../internal-docs/USAGE.md) · [Architecture](../../internal-docs/ARCHITECTURE.md) · [FEATURES — C\* IDs](../../internal-docs/FEATURES.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
- 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."
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 renderTags(seo) {
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 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) });
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
- // 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);
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 useSEO() {
527
- throw new SEOError("USE_SEO_NOT_AVAILABLE");
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 fromNextSeo(_nextSeoExport) {
617
- throw new SEOError("MIGRATE_NOT_IMPLEMENTED");
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;