@shevky/core 0.0.2 → 0.0.4

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