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