@shevky/core 0.0.1 → 0.0.3

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.
@@ -0,0 +1,786 @@
1
+ import { i18n as _i18n, config as _cfg, format as _fmt } from "@shevky/base";
2
+
3
+ import _analytics from "../scripts/analytics.js";
4
+
5
+ /**
6
+ * @typedef {Record<string, any>} FrontMatter
7
+ */
8
+
9
+ /**
10
+ * @typedef {{ base: Record<string, string>, default?: string, [key: string]: string | Record<string, string> | undefined }} AlternateUrlMap
11
+ */
12
+
13
+ /** @type {Record<string, string>} */
14
+ const FALLBACK_ROLES = { tr: "-", en: "-" };
15
+ /** @type {Record<string, string>} */
16
+ const FALLBACK_QUOTES = { tr: "-", en: "-" };
17
+ /** @type {Record<string, string>} */
18
+ const FALLBACK_TITLES = { tr: "-", en: "-" };
19
+ /** @type {Record<string, string>} */
20
+ const FALLBACK_DESCRIPTIONS = { tr: "-", en: "-" };
21
+ const FALLBACK_OWNER = "-";
22
+ /** @type {Record<string, string>} */
23
+ const FALLBACK_TAGLINES = { tr: "-", en: "-" };
24
+
25
+ class MetaEngine {
26
+ /** @param {unknown} value */
27
+ serializeForInlineScript(value) {
28
+ return JSON.stringify(value ?? {})
29
+ .replace(/</g, "\\u003c")
30
+ .replace(/>/g, "\\u003e")
31
+ .replace(/&/g, "\\u0026")
32
+ .replace(/\u2028/g, "\\u2028")
33
+ .replace(/\u2029/g, "\\u2029");
34
+ }
35
+
36
+ /** @param {unknown} value */
37
+ _toLocaleArray(value) {
38
+ if (Array.isArray(value)) {
39
+ return value;
40
+ }
41
+
42
+ if (typeof value === "string" && value.trim().length > 0) {
43
+ return value.split(",").map((item) => item.trim());
44
+ }
45
+
46
+ return [];
47
+ }
48
+
49
+ /** @param {unknown} value @param {string[] | unknown} [fallback] */
50
+ normalizeAlternateLocales(value, fallback = []) {
51
+ const primary = this._toLocaleArray(value);
52
+ const fallbackList = this._toLocaleArray(fallback);
53
+ const source = primary.length ? primary : fallbackList;
54
+ const seen = new Set();
55
+
56
+ return source
57
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
58
+ .filter((item) => {
59
+ if (!item || seen.has(item)) {
60
+ return false;
61
+ }
62
+
63
+ seen.add(item);
64
+ return true;
65
+ });
66
+ }
67
+
68
+ /** @param {string} input */
69
+ ensureDirectoryTrailingSlash(input) {
70
+ if (typeof input !== "string") {
71
+ return input;
72
+ }
73
+
74
+ return _fmt.ensureDirectoryTrailingSlash(input);
75
+ }
76
+
77
+ /** @param {string} value */
78
+ resolveUrl(value) {
79
+ return _fmt.resolveUrl(value, _cfg.identity.url);
80
+ }
81
+
82
+ /** @param {string} lang */
83
+ resolveLanguageHomePath(lang) {
84
+ if (typeof _i18n.homePath === "function") {
85
+ const resolved = _i18n.homePath(lang);
86
+ if (resolved && typeof resolved === "string") {
87
+ return resolved;
88
+ }
89
+ }
90
+
91
+ if (!lang || lang === _i18n.default) {
92
+ return "/";
93
+ }
94
+
95
+ return `/${lang}/`.replace(/\/+/g, "/");
96
+ }
97
+
98
+ /** @param {string} lang */
99
+ buildSiteData(lang) {
100
+ const fallbackOwner = FALLBACK_OWNER;
101
+ const author = _cfg.identity.author;
102
+ const owner = _i18n.t(lang, "site.owner", fallbackOwner);
103
+ const title = _i18n.t(
104
+ lang,
105
+ "site.title",
106
+ FALLBACK_TITLES[lang] ?? FALLBACK_TITLES[_i18n.default] ?? fallbackOwner,
107
+ );
108
+
109
+ const description = _i18n.t(
110
+ lang,
111
+ "site.description",
112
+ FALLBACK_DESCRIPTIONS[lang] ?? FALLBACK_DESCRIPTIONS[_i18n.default] ?? "",
113
+ );
114
+
115
+ const role = _i18n.t(
116
+ lang,
117
+ "site.role",
118
+ FALLBACK_ROLES[lang] ??
119
+ FALLBACK_ROLES[_i18n.default] ??
120
+ FALLBACK_ROLES.en,
121
+ );
122
+
123
+ const quote = _i18n.t(
124
+ lang,
125
+ "site.quote",
126
+ FALLBACK_QUOTES[lang] ??
127
+ FALLBACK_QUOTES[_i18n.default] ??
128
+ FALLBACK_QUOTES.en,
129
+ );
130
+
131
+ return {
132
+ title,
133
+ description,
134
+ author,
135
+ owner,
136
+ role,
137
+ quote,
138
+ home: this.resolveLanguageHomePath(lang),
139
+ url: _cfg.identity.url,
140
+ currentLanguage: lang,
141
+ currentCulture: _i18n.culture(lang),
142
+ currentCanonical: (() => {
143
+ const langConfig = _i18n.build?.[lang];
144
+ if (langConfig?.canonical) {
145
+ return langConfig.canonical;
146
+ }
147
+ const fallbackPath = lang === _i18n.default ? "/" : `/${lang}/`;
148
+ return this.resolveUrl(fallbackPath);
149
+ })(),
150
+ currentLangLabel:
151
+ typeof _i18n.languageLabel === "function"
152
+ ? _i18n.languageLabel(lang)
153
+ : lang,
154
+ themeColor: _cfg.identity.themeColor,
155
+ analyticsEnabled: _analytics.enabled,
156
+ gtmId: _analytics.google.gtm,
157
+ year: new Date().getFullYear(),
158
+ languages: {
159
+ supported: _i18n.supported,
160
+ default: _i18n.default,
161
+ canonical:
162
+ _cfg?.content?.languages?.canonical &&
163
+ typeof _cfg.content.languages.canonical === "object"
164
+ ? /** @type {Record<string, string>} */ (
165
+ _cfg.content.languages.canonical
166
+ )
167
+ : {},
168
+ canonicalUrl: _i18n.supported.reduce((acc, code) => {
169
+ const langConfig = _i18n.build?.[code];
170
+ if (langConfig?.canonical) {
171
+ acc[code] = langConfig.canonical;
172
+ }
173
+ return acc;
174
+ }, /** @type {Record<string, string>} */ ({})),
175
+ cultures: _i18n.supported.reduce((acc, code) => {
176
+ acc[code] = _i18n.culture(code);
177
+ return acc;
178
+ }, /** @type {Record<string, string>} */ ({})),
179
+ },
180
+ languagesCsv: _i18n.supported.join(","),
181
+ defaultLanguage: _i18n.default,
182
+ pagination: {
183
+ pageSize: _cfg.content.pagination.pageSize,
184
+ },
185
+ features: {
186
+ postOperations: _cfg.features.postOperations,
187
+ search: _cfg.features.search,
188
+ },
189
+ };
190
+ }
191
+
192
+ /** @param {string | undefined} lang */
193
+ _pickFallbackAlternateLang(lang) {
194
+ const supported = _i18n.supported;
195
+ if (!supported.length) {
196
+ return null;
197
+ }
198
+
199
+ if (supported.length === 1) {
200
+ return supported[0];
201
+ }
202
+
203
+ if (lang && lang !== _i18n.default) {
204
+ return _i18n.default;
205
+ }
206
+
207
+ return supported.find((code) => code !== lang) ?? null;
208
+ }
209
+
210
+ /** @param {unknown} alternate @param {string} lang @returns {Record<string, string>} */
211
+ _normalizeAlternateOverrides(alternate, lang) {
212
+ if (!alternate) {
213
+ return {};
214
+ }
215
+
216
+ if (typeof alternate === "string" && alternate.trim().length > 0) {
217
+ const fallbackLang = this._pickFallbackAlternateLang(lang);
218
+ if (!fallbackLang) {
219
+ return {};
220
+ }
221
+
222
+ return { [fallbackLang]: this.resolveUrl(alternate.trim()) };
223
+ }
224
+
225
+ if (typeof alternate === "object" && !Array.isArray(alternate)) {
226
+ const alternateRecord = /** @type {Record<string, unknown>} */ (
227
+ alternate
228
+ );
229
+ /** @type {Record<string, string>} */
230
+ const map = {};
231
+ Object.keys(alternateRecord).forEach((code) => {
232
+ if (!_i18n.supported.includes(code)) {
233
+ return;
234
+ }
235
+ const value = alternateRecord[code];
236
+ if (typeof value === "string" && value.trim().length > 0) {
237
+ map[code] = this.resolveUrl(value.trim());
238
+ }
239
+ });
240
+ return map;
241
+ }
242
+
243
+ return {};
244
+ }
245
+
246
+ /** @param {{ alternate?: unknown } | null | undefined} front @param {string} lang @param {string} canonicalUrl */
247
+ buildAlternateUrlMap(front, lang, canonicalUrl) {
248
+ const overrides = this._normalizeAlternateOverrides(front?.alternate, lang);
249
+ /** @type {AlternateUrlMap} */
250
+ const result = { base: /** @type {Record<string, string>} */ ({}) };
251
+
252
+ _i18n.supported.forEach((code) => {
253
+ const langConfig = _i18n.build[code];
254
+ const fallbackPath = code === _i18n.default ? "/" : `/${code}/`;
255
+ const baseUrl = langConfig?.canonical
256
+ ? this.ensureDirectoryTrailingSlash(langConfig.canonical)
257
+ : this.resolveUrl(fallbackPath);
258
+ result.base[code] = baseUrl;
259
+
260
+ if (code === lang) {
261
+ result[code] = canonicalUrl;
262
+ return;
263
+ }
264
+
265
+ if (overrides[code]) {
266
+ result[code] = overrides[code];
267
+ return;
268
+ }
269
+
270
+ if (langConfig?.canonical) {
271
+ result[code] = langConfig.canonical;
272
+ return;
273
+ }
274
+
275
+ result[code] = this.resolveUrl(fallbackPath);
276
+ });
277
+
278
+ result.default = canonicalUrl;
279
+
280
+ return result;
281
+ }
282
+
283
+ /** @param {AlternateUrlMap | null | undefined} alternateMap */
284
+ buildAlternateLinkList(alternateMap) {
285
+ if (!alternateMap) {
286
+ return [];
287
+ }
288
+
289
+ /** @type {Record<string, string>} */
290
+ const baseMap = alternateMap.base ?? {};
291
+ return _i18n.supported.map((code) => ({
292
+ lang: code,
293
+ hreflang: code,
294
+ url: alternateMap[code] ?? alternateMap.default ?? "",
295
+ label: _i18n.languageLabel(code),
296
+ baseUrl: baseMap[code] ?? baseMap[_i18n.default] ?? "",
297
+ }));
298
+ }
299
+
300
+ /** @param {string} lang @param {string} slug */
301
+ defaultCanonical(lang, slug) {
302
+ const cleanedSlug = (slug ?? "").replace(/^\/+/, "").replace(/\/+$/, "");
303
+ const langConfig = _i18n.build[lang];
304
+ let base = langConfig?.canonical;
305
+ if (!base) {
306
+ const fallbackPath = lang === _i18n.default ? "/" : `/${lang}/`;
307
+ base = this.resolveUrl(fallbackPath);
308
+ }
309
+
310
+ const normalizedBase = base.replace(/\/+$/, "/");
311
+ if (!cleanedSlug) {
312
+ return normalizedBase;
313
+ }
314
+
315
+ return `${normalizedBase}${cleanedSlug}/`;
316
+ }
317
+
318
+ /** @param {string} value */
319
+ canonicalToRelativePath(value) {
320
+ if (!value) return null;
321
+ let path = value;
322
+ if (path.startsWith("~/")) {
323
+ path = path.slice(2);
324
+ } else if (/^https?:\/\//i.test(path)) {
325
+ path = path.replace(/^https?:\/\/[^/]+/i, "");
326
+ }
327
+ path = path.trim();
328
+ if (!path) return null;
329
+ return path.replace(/^\/+/, "").replace(/\/+$/, "");
330
+ }
331
+
332
+ /** @param {string | null | undefined} canonical @param {string} lang @param {string} slug */
333
+ buildContentUrl(canonical, lang, slug) {
334
+ const normalizedLang = lang ?? _i18n.default;
335
+ if (typeof canonical === "string" && canonical.trim().length > 0) {
336
+ const trimmedCanonical = canonical.trim();
337
+ const relative = this.canonicalToRelativePath(trimmedCanonical);
338
+ if (relative) {
339
+ const normalizedRelative = `/${relative}`.replace(/\/+/g, "/");
340
+ return this.ensureDirectoryTrailingSlash(normalizedRelative);
341
+ }
342
+
343
+ return this.ensureDirectoryTrailingSlash(trimmedCanonical);
344
+ }
345
+ const fallback = this.canonicalToRelativePath(
346
+ this.defaultCanonical(normalizedLang, slug),
347
+ );
348
+ if (fallback) {
349
+ const normalizedFallback = `/${fallback}`.replace(/\/+/g, "/");
350
+ return this.ensureDirectoryTrailingSlash(normalizedFallback);
351
+ }
352
+ const slugSegment = slug ? `/${slug}` : "/";
353
+ if (normalizedLang !== _i18n.default) {
354
+ const langPath = `/${normalizedLang}${slugSegment}`.replace(/\/+/g, "/");
355
+ return this.ensureDirectoryTrailingSlash(langPath);
356
+ }
357
+
358
+ const normalizedSlug = slugSegment.replace(/\/+/g, "/");
359
+ return this.ensureDirectoryTrailingSlash(normalizedSlug);
360
+ }
361
+
362
+ /** @param {FrontMatter} front @param {string} lang */
363
+ _resolveArticleSection(front, lang) {
364
+ const rawCategory =
365
+ typeof front.category === "string"
366
+ ? front.category.trim().toLowerCase()
367
+ : "";
368
+ if (!rawCategory) return "";
369
+ if (lang === "tr") {
370
+ if (rawCategory === "yasam-ogrenme") return "Yaşam & Öğrenme";
371
+ if (rawCategory === "teknik-notlar") return "Teknik Notlar";
372
+ }
373
+ if (lang === "en") {
374
+ if (rawCategory === "life-learning") return "Life & Learning";
375
+ if (rawCategory === "technical-notes") return "Technical Notes";
376
+ }
377
+ return rawCategory;
378
+ }
379
+
380
+ /**
381
+ * @param {FrontMatter | { keywords?: unknown } | null | undefined} source
382
+ * @param {FrontMatter | null | undefined} [fallback]
383
+ */
384
+ _resolveKeywords(source, fallback) {
385
+ if (source && Array.isArray(source.keywords) && source.keywords.length) {
386
+ return source.keywords;
387
+ }
388
+ if (
389
+ fallback &&
390
+ Array.isArray(fallback.keywords) &&
391
+ fallback.keywords.length
392
+ ) {
393
+ return fallback.keywords;
394
+ }
395
+ return [];
396
+ }
397
+
398
+ /** @param {FrontMatter} front */
399
+ _resolvePageTitle(front) {
400
+ if (typeof front.metaTitle === "string" && front.metaTitle.trim().length) {
401
+ return front.metaTitle.trim();
402
+ }
403
+ if (typeof front.title === "string" && front.title.trim().length) {
404
+ return front.title.trim();
405
+ }
406
+ return "Untitled";
407
+ }
408
+
409
+ /** @param {FrontMatter} front */
410
+ _resolveCoverSource(front) {
411
+ return typeof front.cover === "string" && front.cover.trim().length > 0
412
+ ? front.cover.trim()
413
+ : _cfg.seo.defaultImage;
414
+ }
415
+
416
+ /** @param {FrontMatter} front @param {string} lang */
417
+ _resolveOgLocales(front, lang) {
418
+ const langConfig = _i18n.build[lang] ?? {};
419
+ const ogLocale = langConfig.ogLocale ?? _i18n.culture(lang);
420
+ const defaultAltLocales =
421
+ langConfig.altLocale ??
422
+ _i18n.supported
423
+ .filter((code) => code !== lang)
424
+ .map((code) => _i18n.culture(code));
425
+ const altLocales = this.normalizeAlternateLocales(
426
+ front.ogAltLocale,
427
+ defaultAltLocales,
428
+ );
429
+ return { ogLocale, altLocales };
430
+ }
431
+
432
+ /** @param {FrontMatter} front */
433
+ _resolveContentType(front) {
434
+ const typeValue =
435
+ typeof front.type === "string" ? front.type.trim().toLowerCase() : "";
436
+ const templateValue =
437
+ typeof front.template === "string"
438
+ ? front.template.trim().toLowerCase()
439
+ : "";
440
+ const isArticle =
441
+ templateValue === "post" ||
442
+ typeValue === "article" ||
443
+ typeValue === "guide" ||
444
+ typeValue === "post";
445
+ return { typeValue, templateValue, isArticle };
446
+ }
447
+
448
+ /** @param {FrontMatter} front @param {string} lang @param {string} canonicalUrl @param {string} ogImageUrl */
449
+ _buildArticleStructuredData(front, lang, canonicalUrl, ogImageUrl) {
450
+ const authorName = _cfg.identity.author;
451
+ const articleSection = this._resolveArticleSection(front, lang);
452
+ const keywordsArray = this._resolveKeywords(front);
453
+
454
+ const structured = /** @type {Record<string, any>} */ ({
455
+ "@context": "https://schema.org",
456
+ "@type": "Article",
457
+ headline: front.title ?? "",
458
+ description: front.description ?? "",
459
+ author: {
460
+ "@type": "Person",
461
+ name: authorName,
462
+ url: _cfg.identity.url,
463
+ },
464
+ publisher: {
465
+ "@type": "Person",
466
+ name: authorName,
467
+ url: _cfg.identity.url,
468
+ },
469
+ inLanguage: lang,
470
+ mainEntityOfPage: {
471
+ "@type": "WebPage",
472
+ "@id": canonicalUrl,
473
+ },
474
+ });
475
+
476
+ if (front.date) {
477
+ structured.datePublished = _fmt.lastMod(front.date);
478
+ }
479
+
480
+ if (front.updated) {
481
+ structured.dateModified = _fmt.lastMod(front.updated);
482
+ }
483
+
484
+ if (ogImageUrl) {
485
+ structured.image = [ogImageUrl];
486
+ }
487
+
488
+ if (articleSection) {
489
+ structured.articleSection = articleSection;
490
+ }
491
+
492
+ if (keywordsArray.length) {
493
+ structured.keywords = keywordsArray;
494
+ }
495
+
496
+ return this.serializeForInlineScript(structured);
497
+ }
498
+
499
+ /** @param {FrontMatter} front @param {string} lang @param {string} canonicalUrl */
500
+ _buildHomeStructuredData(front, lang, canonicalUrl) {
501
+ const siteData = this.buildSiteData(lang);
502
+ const authorName = _cfg.identity.author;
503
+ const structured = /** @type {Record<string, any>} */ ({
504
+ "@context": "https://schema.org",
505
+ "@type": "WebSite",
506
+ name: siteData.title ?? "",
507
+ url: canonicalUrl,
508
+ inLanguage: lang,
509
+ description: siteData.description ?? "",
510
+ publisher: {
511
+ "@type": "Person",
512
+ name: authorName,
513
+ url: _cfg.identity.url,
514
+ sameAs: [
515
+ _cfg.identity.social.devto,
516
+ _cfg.identity.social.facebook,
517
+ _cfg.identity.social.github,
518
+ _cfg.identity.social.instagram,
519
+ _cfg.identity.social.linkedin,
520
+ _cfg.identity.social.mastodon,
521
+ _cfg.identity.social.medium,
522
+ _cfg.identity.social.stackoverflow,
523
+ _cfg.identity.social.substack,
524
+ _cfg.identity.social.tiktok,
525
+ _cfg.identity.social.x,
526
+ _cfg.identity.social.youtube,
527
+ ].filter((i) => i && i.trim().length > 0),
528
+ },
529
+ });
530
+ return this.serializeForInlineScript(structured);
531
+ }
532
+
533
+ /** @param {FrontMatter} front @param {string} lang */
534
+ _resolveCollectionDescription(front, lang) {
535
+ const collectionType =
536
+ front.collectionType && front.collectionType.trim().length > 0
537
+ ? front.collectionType.trim()
538
+ : "";
539
+ if (!collectionType) {
540
+ return "";
541
+ }
542
+
543
+ if (collectionType === "tag") {
544
+ return String(
545
+ _i18n.t(lang, "seo.collections.tags.description", "") ?? "",
546
+ ).replace("{{label}}", String(front.listKey ?? ""));
547
+ }
548
+
549
+ if (collectionType === "category") {
550
+ return String(
551
+ _i18n.t(lang, "seo.collections.category.description", "") ?? "",
552
+ ).replace("{{label}}", String(front.listKey ?? ""));
553
+ }
554
+
555
+ if (collectionType === "series") {
556
+ return String(
557
+ _i18n.t(lang, "seo.collections.series.description", "") ?? "",
558
+ ).replace("{{label}}", String(front.listKey ?? ""));
559
+ }
560
+
561
+ return "";
562
+ }
563
+
564
+ _resolveSocialProfiles() {
565
+ return [
566
+ _cfg.identity.social.devto,
567
+ _cfg.identity.social.facebook,
568
+ _cfg.identity.social.github,
569
+ _cfg.identity.social.instagram,
570
+ _cfg.identity.social.linkedin,
571
+ _cfg.identity.social.mastodon,
572
+ _cfg.identity.social.medium,
573
+ _cfg.identity.social.stackoverflow,
574
+ _cfg.identity.social.substack,
575
+ _cfg.identity.social.tiktok,
576
+ _cfg.identity.social.x,
577
+ _cfg.identity.social.youtube,
578
+ ].filter((i) => i && i.trim().length > 0);
579
+ }
580
+
581
+ /**
582
+ * @param {FrontMatter} front
583
+ * @param {string} lang
584
+ * @param {string} canonicalUrl
585
+ * @param {FrontMatter | { isPolicy?: boolean, isAboutPage?: boolean, isContactPage?: boolean, isCollectionPage?: boolean, collectionType?: string, keywords?: unknown } | null | undefined} [derived]
586
+ */
587
+ _buildWebPageStructuredData(front, lang, canonicalUrl, derived = front) {
588
+ const authorName = _cfg.identity.author;
589
+ const keywordsArray = this._resolveKeywords(derived, front);
590
+ const isPolicy =
591
+ typeof derived?.isPolicy === "boolean"
592
+ ? derived.isPolicy
593
+ : front.category && front.category.trim().length > 0
594
+ ? _fmt.boolean(front.category.trim() === "policy")
595
+ : false;
596
+ const isAboutPage =
597
+ typeof derived?.isAboutPage === "boolean"
598
+ ? derived.isAboutPage
599
+ : front.type && front.type.trim().length > 0
600
+ ? _fmt.boolean(front.type.trim() === "about")
601
+ : false;
602
+ const isContactPage =
603
+ typeof derived?.isContactPage === "boolean"
604
+ ? derived.isContactPage
605
+ : front.type && front.type.trim().length > 0
606
+ ? _fmt.boolean(front.type.trim() === "contact")
607
+ : false;
608
+ const collectionType =
609
+ typeof derived?.collectionType === "string" &&
610
+ derived.collectionType.trim().length > 0
611
+ ? derived.collectionType.trim()
612
+ : front.collectionType && front.collectionType.trim().length > 0
613
+ ? front.collectionType.trim()
614
+ : "";
615
+ const isCollectionPage =
616
+ typeof derived?.isCollectionPage === "boolean"
617
+ ? derived.isCollectionPage
618
+ : collectionType === "tag" ||
619
+ collectionType === "category" ||
620
+ collectionType === "series";
621
+ const collectionDescription = isCollectionPage
622
+ ? this._resolveCollectionDescription(front, lang)
623
+ : "";
624
+
625
+ const structured = /** @type {Record<string, any>} */ ({
626
+ "@context": "https://schema.org",
627
+ "@type": isAboutPage
628
+ ? "AboutPage"
629
+ : isContactPage
630
+ ? "ContactPage"
631
+ : isCollectionPage
632
+ ? "CollectionPage"
633
+ : "WebPage",
634
+ headline: front.title ?? "",
635
+ description: front.description
636
+ ? front.description
637
+ : isCollectionPage
638
+ ? collectionDescription
639
+ : "",
640
+ publisher: {
641
+ "@type": "Person",
642
+ name: authorName,
643
+ url: _cfg.identity.url,
644
+ },
645
+ inLanguage: lang,
646
+ mainEntityOfPage: {
647
+ "@type": "WebPage",
648
+ "@id": canonicalUrl,
649
+ },
650
+ ...(isPolicy
651
+ ? { about: { "@type": "Thing", name: "Website Legal Information" } }
652
+ : {}),
653
+ ...(isAboutPage
654
+ ? {
655
+ about: {
656
+ "@type": "Person",
657
+ name: _cfg.identity.author,
658
+ url: _cfg.identity.url,
659
+ sameAs: this._resolveSocialProfiles(),
660
+ },
661
+ }
662
+ : {}),
663
+ ...(isContactPage
664
+ ? {
665
+ about: {
666
+ "@type": "Person",
667
+ name: _cfg.identity.author,
668
+ url: _cfg.identity.url,
669
+ },
670
+ contactPoint: {
671
+ "@type": "ContactPoint",
672
+ contactType: "general inquiry",
673
+ email: _cfg.identity.email,
674
+ },
675
+ }
676
+ : {}),
677
+ });
678
+
679
+ if (keywordsArray.filter((i) => i && i.trim().length > 0).length) {
680
+ structured.keywords = keywordsArray.filter(
681
+ (i) => i && i.trim().length > 0,
682
+ );
683
+ }
684
+
685
+ return this.serializeForInlineScript(structured);
686
+ }
687
+
688
+ /**
689
+ * @param {FrontMatter} front
690
+ * @param {string} lang
691
+ * @param {string} canonicalUrl
692
+ * @param {string} ogImage
693
+ * @param {FrontMatter | { isPolicy?: boolean, isAboutPage?: boolean, isContactPage?: boolean, isCollectionPage?: boolean, collectionType?: string, keywords?: unknown } | null | undefined} [derived]
694
+ */
695
+ _resolveStructuredData(front, lang, canonicalUrl, ogImage, derived = front) {
696
+ const { templateValue, isArticle } = this._resolveContentType(front);
697
+ let structuredData = null;
698
+ if (isArticle) {
699
+ structuredData = this._buildArticleStructuredData(
700
+ front,
701
+ lang,
702
+ canonicalUrl,
703
+ ogImage,
704
+ );
705
+ } else if (templateValue === "home") {
706
+ structuredData = this._buildHomeStructuredData(front, lang, canonicalUrl);
707
+ } else {
708
+ structuredData = this._buildWebPageStructuredData(
709
+ front,
710
+ lang,
711
+ canonicalUrl,
712
+ derived,
713
+ );
714
+ }
715
+ return { structuredData, isArticle };
716
+ }
717
+
718
+ /**
719
+ * @param {FrontMatter | { header?: FrontMatter } | null | undefined} input
720
+ * @param {string} lang
721
+ * @param {string} slug
722
+ */
723
+ buildPageMeta(input, lang, slug) {
724
+ const front =
725
+ input &&
726
+ typeof input === "object" &&
727
+ input.header &&
728
+ typeof input.header === "object"
729
+ ? input.header
730
+ : /** @type {FrontMatter} */ (input ?? {});
731
+ const derived =
732
+ input &&
733
+ typeof input === "object" &&
734
+ input.header &&
735
+ typeof input.header === "object"
736
+ ? input
737
+ : front;
738
+ const canonicalUrl = this.resolveUrl(
739
+ front.canonical ?? this.defaultCanonical(lang, slug),
740
+ );
741
+ const pageTitleSource = this._resolvePageTitle(front);
742
+ const { ogLocale, altLocales } = this._resolveOgLocales(front, lang);
743
+ const coverSource = this._resolveCoverSource(front);
744
+ const ogImage = this.resolveUrl(coverSource);
745
+ const alternates = this.buildAlternateUrlMap(front, lang, canonicalUrl);
746
+ const alternateLinks = this.buildAlternateLinkList(alternates);
747
+ const twitterImage = this.resolveUrl(coverSource);
748
+
749
+ const { structuredData, isArticle } = this._resolveStructuredData(
750
+ front,
751
+ lang,
752
+ canonicalUrl,
753
+ ogImage,
754
+ derived,
755
+ );
756
+ const ogType = front.ogType ?? (isArticle ? "article" : "website");
757
+
758
+ return {
759
+ title: pageTitleSource,
760
+ description: front.description ?? "",
761
+ robots: front.robots ?? "index,follow",
762
+ canonical: canonicalUrl,
763
+ alternates,
764
+ alternateLinks,
765
+ og: {
766
+ title: front.ogTitle ?? pageTitleSource,
767
+ description: front.description ?? "",
768
+ type: ogType,
769
+ url: canonicalUrl,
770
+ image: ogImage,
771
+ locale: front.ogLocale ?? ogLocale,
772
+ altLocale: altLocales,
773
+ },
774
+ twitter: {
775
+ card: front.twitterCard ?? "summary_large_image",
776
+ title: front.twitterTitle ?? pageTitleSource,
777
+ description: front.description ?? "",
778
+ image: twitterImage,
779
+ url: canonicalUrl,
780
+ },
781
+ structuredData,
782
+ };
783
+ }
784
+ }
785
+
786
+ export { MetaEngine };