@nexpress/core 0.1.0

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/audit-54XLVCWD.js +14 -0
  4. package/dist/audit-54XLVCWD.js.map +1 -0
  5. package/dist/auth.d.ts +640 -0
  6. package/dist/auth.js +94 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/can-YLUHRJAB.js +19 -0
  9. package/dist/can-YLUHRJAB.js.map +1 -0
  10. package/dist/chunk-2G264RCD.js +68 -0
  11. package/dist/chunk-2G264RCD.js.map +1 -0
  12. package/dist/chunk-2YDGE7YX.js +92 -0
  13. package/dist/chunk-2YDGE7YX.js.map +1 -0
  14. package/dist/chunk-473S4TER.js +538 -0
  15. package/dist/chunk-473S4TER.js.map +1 -0
  16. package/dist/chunk-4ZLMEKFX.js +18 -0
  17. package/dist/chunk-4ZLMEKFX.js.map +1 -0
  18. package/dist/chunk-55FU6WED.js +179 -0
  19. package/dist/chunk-55FU6WED.js.map +1 -0
  20. package/dist/chunk-6YI5K2TI.js +1959 -0
  21. package/dist/chunk-6YI5K2TI.js.map +1 -0
  22. package/dist/chunk-BHK3AD3Q.js +41 -0
  23. package/dist/chunk-BHK3AD3Q.js.map +1 -0
  24. package/dist/chunk-CRUQBZUF.js +39 -0
  25. package/dist/chunk-CRUQBZUF.js.map +1 -0
  26. package/dist/chunk-CTSQ7BRI.js +175 -0
  27. package/dist/chunk-CTSQ7BRI.js.map +1 -0
  28. package/dist/chunk-DK2JBJH7.js +81 -0
  29. package/dist/chunk-DK2JBJH7.js.map +1 -0
  30. package/dist/chunk-DP2PREDU.js +597 -0
  31. package/dist/chunk-DP2PREDU.js.map +1 -0
  32. package/dist/chunk-EQ2Z3KMD.js +24 -0
  33. package/dist/chunk-EQ2Z3KMD.js.map +1 -0
  34. package/dist/chunk-FZ7O6DWI.js +305 -0
  35. package/dist/chunk-FZ7O6DWI.js.map +1 -0
  36. package/dist/chunk-ISLYFQWL.js +1270 -0
  37. package/dist/chunk-ISLYFQWL.js.map +1 -0
  38. package/dist/chunk-JJL74ZPK.js +68 -0
  39. package/dist/chunk-JJL74ZPK.js.map +1 -0
  40. package/dist/chunk-JKXAPSU4.js +24 -0
  41. package/dist/chunk-JKXAPSU4.js.map +1 -0
  42. package/dist/chunk-KU5M27ZC.js +24 -0
  43. package/dist/chunk-KU5M27ZC.js.map +1 -0
  44. package/dist/chunk-LSHHRDVR.js +34 -0
  45. package/dist/chunk-LSHHRDVR.js.map +1 -0
  46. package/dist/chunk-M43PGOQY.js +715 -0
  47. package/dist/chunk-M43PGOQY.js.map +1 -0
  48. package/dist/chunk-MEJAHXIO.js +150 -0
  49. package/dist/chunk-MEJAHXIO.js.map +1 -0
  50. package/dist/chunk-NUCGHWCF.js +101 -0
  51. package/dist/chunk-NUCGHWCF.js.map +1 -0
  52. package/dist/chunk-OK5HOCQI.js +845 -0
  53. package/dist/chunk-OK5HOCQI.js.map +1 -0
  54. package/dist/chunk-OROPGO65.js +13 -0
  55. package/dist/chunk-OROPGO65.js.map +1 -0
  56. package/dist/chunk-PPAS4SZR.js +176 -0
  57. package/dist/chunk-PPAS4SZR.js.map +1 -0
  58. package/dist/chunk-PPBWRKO2.js +171 -0
  59. package/dist/chunk-PPBWRKO2.js.map +1 -0
  60. package/dist/chunk-PZ5AY32C.js +10 -0
  61. package/dist/chunk-PZ5AY32C.js.map +1 -0
  62. package/dist/chunk-QO7LAQZH.js +321 -0
  63. package/dist/chunk-QO7LAQZH.js.map +1 -0
  64. package/dist/chunk-QVJ2HCAX.js +225 -0
  65. package/dist/chunk-QVJ2HCAX.js.map +1 -0
  66. package/dist/chunk-RIPHIRPP.js +68 -0
  67. package/dist/chunk-RIPHIRPP.js.map +1 -0
  68. package/dist/chunk-S27S42QY.js +134 -0
  69. package/dist/chunk-S27S42QY.js.map +1 -0
  70. package/dist/chunk-SBCVAC2Z.js +40 -0
  71. package/dist/chunk-SBCVAC2Z.js.map +1 -0
  72. package/dist/chunk-TFJ4MKPH.js +694 -0
  73. package/dist/chunk-TFJ4MKPH.js.map +1 -0
  74. package/dist/chunk-THX3SHYA.js +75 -0
  75. package/dist/chunk-THX3SHYA.js.map +1 -0
  76. package/dist/chunk-UGQSQO5B.js +222 -0
  77. package/dist/chunk-UGQSQO5B.js.map +1 -0
  78. package/dist/chunk-V2UNHGAP.js +26 -0
  79. package/dist/chunk-V2UNHGAP.js.map +1 -0
  80. package/dist/chunk-VGTPQXNQ.js +2790 -0
  81. package/dist/chunk-VGTPQXNQ.js.map +1 -0
  82. package/dist/chunk-VNIHXQ7W.js +194 -0
  83. package/dist/chunk-VNIHXQ7W.js.map +1 -0
  84. package/dist/chunk-WV272MPW.js +31 -0
  85. package/dist/chunk-WV272MPW.js.map +1 -0
  86. package/dist/chunk-X5KKBOUS.js +26 -0
  87. package/dist/chunk-X5KKBOUS.js.map +1 -0
  88. package/dist/chunk-XANPEOJC.js +17 -0
  89. package/dist/chunk-XANPEOJC.js.map +1 -0
  90. package/dist/chunk-XPVQIHAQ.js +83 -0
  91. package/dist/chunk-XPVQIHAQ.js.map +1 -0
  92. package/dist/chunk-ZCINJSS4.js +75 -0
  93. package/dist/chunk-ZCINJSS4.js.map +1 -0
  94. package/dist/community.d.ts +1425 -0
  95. package/dist/community.js +206 -0
  96. package/dist/community.js.map +1 -0
  97. package/dist/config-2GDU7PCK.js +32 -0
  98. package/dist/config-2GDU7PCK.js.map +1 -0
  99. package/dist/context-MNZ4QXPC.js +16 -0
  100. package/dist/context-MNZ4QXPC.js.map +1 -0
  101. package/dist/db-schema.d.ts +4 -0
  102. package/dist/db-schema.js +102 -0
  103. package/dist/db-schema.js.map +1 -0
  104. package/dist/db.d.ts +7 -0
  105. package/dist/db.js +117 -0
  106. package/dist/db.js.map +1 -0
  107. package/dist/digest-SY42GQSU.js +17 -0
  108. package/dist/digest-SY42GQSU.js.map +1 -0
  109. package/dist/errors-5OS3S2J3.js +22 -0
  110. package/dist/errors-5OS3S2J3.js.map +1 -0
  111. package/dist/host-OBOI4MJK.js +51 -0
  112. package/dist/host-OBOI4MJK.js.map +1 -0
  113. package/dist/i18n.d.ts +301 -0
  114. package/dist/i18n.js +68 -0
  115. package/dist/i18n.js.map +1 -0
  116. package/dist/index-B6-_vr_m.d.ts +590 -0
  117. package/dist/index-CY55LC0u.d.ts +4722 -0
  118. package/dist/index-CeiTvwbp.d.ts +168 -0
  119. package/dist/index-XwP1ET8b.d.ts +61 -0
  120. package/dist/index.d.ts +2037 -0
  121. package/dist/index.js +2205 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/job-log-VZXWQUDK.js +24 -0
  124. package/dist/job-log-VZXWQUDK.js.map +1 -0
  125. package/dist/jobs.d.ts +4 -0
  126. package/dist/jobs.js +76 -0
  127. package/dist/jobs.js.map +1 -0
  128. package/dist/logger-DqGaOU_j.d.ts +29 -0
  129. package/dist/logger-S7REWDNE.js +16 -0
  130. package/dist/logger-S7REWDNE.js.map +1 -0
  131. package/dist/media.d.ts +5 -0
  132. package/dist/media.js +41 -0
  133. package/dist/media.js.map +1 -0
  134. package/dist/mentions-2IHFVSHW.js +23 -0
  135. package/dist/mentions-2IHFVSHW.js.map +1 -0
  136. package/dist/mutes-EWAE5FZR.js +21 -0
  137. package/dist/mutes-EWAE5FZR.js.map +1 -0
  138. package/dist/notification-prefs-VPJDU7I6.js +21 -0
  139. package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
  140. package/dist/observability.d.ts +156 -0
  141. package/dist/observability.js +32 -0
  142. package/dist/observability.js.map +1 -0
  143. package/dist/profanity-adapter-NU2JQSLX.js +12 -0
  144. package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
  145. package/dist/queue-XE5BC75T.js +14 -0
  146. package/dist/queue-XE5BC75T.js.map +1 -0
  147. package/dist/rate-limit.d.ts +99 -0
  148. package/dist/rate-limit.js +14 -0
  149. package/dist/rate-limit.js.map +1 -0
  150. package/dist/registry-XIXDEPVI.js +31 -0
  151. package/dist/registry-XIXDEPVI.js.map +1 -0
  152. package/dist/reputation-JRL2YQHM.js +11 -0
  153. package/dist/reputation-JRL2YQHM.js.map +1 -0
  154. package/dist/routes.d.ts +43 -0
  155. package/dist/routes.js +12 -0
  156. package/dist/routes.js.map +1 -0
  157. package/dist/scheduled-CIQM57HT.js +20 -0
  158. package/dist/scheduled-CIQM57HT.js.map +1 -0
  159. package/dist/seo.d.ts +410 -0
  160. package/dist/seo.js +44 -0
  161. package/dist/seo.js.map +1 -0
  162. package/dist/settings-FOBIESPB.js +17 -0
  163. package/dist/settings-FOBIESPB.js.map +1 -0
  164. package/dist/spam-adapter-XX3G737Z.js +12 -0
  165. package/dist/spam-adapter-XX3G737Z.js.map +1 -0
  166. package/dist/strings-VAE47B2C.js +29 -0
  167. package/dist/strings-VAE47B2C.js.map +1 -0
  168. package/dist/templates-IFVJMCJ6.js +12 -0
  169. package/dist/templates-IFVJMCJ6.js.map +1 -0
  170. package/dist/types-TlsbXS0T.d.ts +871 -0
  171. package/package.json +129 -0
@@ -0,0 +1,597 @@
1
+ import {
2
+ findDocuments
3
+ } from "./chunk-VGTPQXNQ.js";
4
+ import {
5
+ getI18nConfig
6
+ } from "./chunk-4ZLMEKFX.js";
7
+ import {
8
+ getAllCollectionSlugs,
9
+ getCollectionConfig
10
+ } from "./chunk-FZ7O6DWI.js";
11
+ import {
12
+ getDb
13
+ } from "./chunk-XANPEOJC.js";
14
+ import {
15
+ npSettings
16
+ } from "./chunk-M43PGOQY.js";
17
+
18
+ // src/seo/sitemap.ts
19
+ var DEFAULT_LIMIT_PER_COLLECTION = 5e3;
20
+ async function buildSitemap(options = {}) {
21
+ const limit = options.perCollectionLimit ?? DEFAULT_LIMIT_PER_COLLECTION;
22
+ const slugs = options.collections ?? getAllCollectionSlugs();
23
+ const entries = [];
24
+ const i18n = getI18nConfig();
25
+ const localeFilter = options.locale;
26
+ for (const slug of slugs) {
27
+ let config;
28
+ try {
29
+ config = getCollectionConfig(slug);
30
+ } catch {
31
+ continue;
32
+ }
33
+ const seo = config.seo;
34
+ if (!seo?.urlPath) continue;
35
+ if (localeFilter && !config.i18n) {
36
+ if (!i18n || localeFilter !== i18n.defaultLocale) continue;
37
+ }
38
+ let result;
39
+ try {
40
+ result = await findDocuments(
41
+ slug,
42
+ {
43
+ limit,
44
+ page: 1,
45
+ where: { status: "published" }
46
+ // For i18n collections we deliberately fetch *every*
47
+ // locale's rows even when a localeFilter is set so the
48
+ // grouping pass below can still build a complete
49
+ // hreflang-alternates list. The emission step further
50
+ // down filters siblings to the requested locale before
51
+ // pushing entries. Non-i18n collections take the
52
+ // localeFilter path through the early `continue` above.
53
+ },
54
+ // Anonymous — `access.read` must allow it for the row to
55
+ // appear. Collections gated to authenticated users won't
56
+ // throw here because the access check runs on the
57
+ // collection level (not per-row); they'll throw and we
58
+ // skip below.
59
+ void 0
60
+ );
61
+ } catch {
62
+ continue;
63
+ }
64
+ const docs = result.docs;
65
+ if (config.i18n) {
66
+ const groups = /* @__PURE__ */ new Map();
67
+ const orphans = [];
68
+ for (const doc of docs) {
69
+ const groupId = typeof doc.translationGroupId === "string" ? doc.translationGroupId : null;
70
+ if (!groupId) {
71
+ orphans.push(doc);
72
+ continue;
73
+ }
74
+ const list = groups.get(groupId);
75
+ if (list) list.push(doc);
76
+ else groups.set(groupId, [doc]);
77
+ }
78
+ for (const siblings of groups.values()) {
79
+ const alternates = [];
80
+ for (const sibling of siblings) {
81
+ const siblingPath = seo.urlPath(sibling);
82
+ const locale = typeof sibling.locale === "string" ? sibling.locale : null;
83
+ if (siblingPath && locale) {
84
+ alternates.push({ hreflang: locale, href: siblingPath });
85
+ }
86
+ }
87
+ for (const sibling of siblings) {
88
+ if (localeFilter) {
89
+ const siblingLocale = typeof sibling.locale === "string" ? sibling.locale : null;
90
+ if (siblingLocale !== localeFilter) continue;
91
+ }
92
+ const path = seo.urlPath(sibling);
93
+ if (!path || !path.startsWith("/")) continue;
94
+ entries.push({
95
+ loc: path,
96
+ lastmod: pickLastmod(sibling),
97
+ changefreq: seo.changefreq,
98
+ priority: seo.priority,
99
+ alternates: alternates.length > 1 ? alternates : void 0
100
+ });
101
+ }
102
+ }
103
+ for (const doc of orphans) {
104
+ if (localeFilter) {
105
+ const docLocale = typeof doc.locale === "string" ? doc.locale : null;
106
+ if (docLocale !== localeFilter) continue;
107
+ }
108
+ const path = seo.urlPath(doc);
109
+ if (!path || !path.startsWith("/")) continue;
110
+ entries.push({
111
+ loc: path,
112
+ lastmod: pickLastmod(doc),
113
+ changefreq: seo.changefreq,
114
+ priority: seo.priority
115
+ });
116
+ }
117
+ continue;
118
+ }
119
+ for (const doc of docs) {
120
+ const path = seo.urlPath(doc);
121
+ if (!path) continue;
122
+ if (!path.startsWith("/")) continue;
123
+ entries.push({
124
+ loc: path,
125
+ lastmod: pickLastmod(doc),
126
+ changefreq: seo.changefreq,
127
+ priority: seo.priority
128
+ });
129
+ }
130
+ }
131
+ return entries;
132
+ }
133
+ function pickLastmod(doc) {
134
+ const candidate = doc.updatedAt ?? doc.createdAt;
135
+ if (candidate instanceof Date) return candidate.toISOString();
136
+ if (typeof candidate === "string") {
137
+ const parsed = new Date(candidate);
138
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
139
+ }
140
+ return void 0;
141
+ }
142
+ function renderSitemapXml(origin, entries) {
143
+ const trimmed = origin.replace(/\/+$/, "");
144
+ const usesAlternates = entries.some(
145
+ (e) => e.alternates && e.alternates.length > 0
146
+ );
147
+ const lines = [
148
+ '<?xml version="1.0" encoding="UTF-8"?>',
149
+ usesAlternates ? '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">' : '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
150
+ ];
151
+ for (const entry of entries) {
152
+ lines.push(" <url>");
153
+ lines.push(` <loc>${escapeXml(`${trimmed}${entry.loc}`)}</loc>`);
154
+ if (entry.lastmod) {
155
+ lines.push(` <lastmod>${entry.lastmod}</lastmod>`);
156
+ }
157
+ if (entry.changefreq) {
158
+ lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
159
+ }
160
+ if (typeof entry.priority === "number") {
161
+ lines.push(` <priority>${entry.priority.toFixed(1)}</priority>`);
162
+ }
163
+ if (entry.alternates) {
164
+ for (const alt of entry.alternates) {
165
+ lines.push(
166
+ ` <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(`${trimmed}${alt.href}`)}"/>`
167
+ );
168
+ }
169
+ }
170
+ lines.push(" </url>");
171
+ }
172
+ lines.push("</urlset>");
173
+ return lines.join("\n");
174
+ }
175
+ function renderSitemapIndexXml(origin, entries) {
176
+ const trimmed = origin.replace(/\/+$/, "");
177
+ const lines = [
178
+ '<?xml version="1.0" encoding="UTF-8"?>',
179
+ '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
180
+ ];
181
+ for (const entry of entries) {
182
+ lines.push(" <sitemap>");
183
+ lines.push(` <loc>${escapeXml(`${trimmed}${entry.loc}`)}</loc>`);
184
+ if (entry.lastmod) {
185
+ lines.push(` <lastmod>${entry.lastmod}</lastmod>`);
186
+ }
187
+ lines.push(" </sitemap>");
188
+ }
189
+ lines.push("</sitemapindex>");
190
+ return lines.join("\n");
191
+ }
192
+ function escapeXml(value) {
193
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
194
+ }
195
+
196
+ // src/seo/page-metadata.ts
197
+ var DEFAULT_SITE_SEO_SETTINGS = {
198
+ siteName: "NexPress",
199
+ siteUrl: "http://localhost:3000",
200
+ defaultDescription: "",
201
+ defaultOgImage: null,
202
+ twitterHandle: null,
203
+ defaultLocale: "en_US"
204
+ };
205
+ async function getSiteSeoSettings() {
206
+ const db = getDb();
207
+ const rows = await db.select().from(npSettings);
208
+ const map = /* @__PURE__ */ new Map();
209
+ for (const row of rows) map.set(row.key, row.value);
210
+ const site = readObject(map.get("site"));
211
+ const seo = readObject(map.get("seo"));
212
+ const description = map.get("description");
213
+ return {
214
+ siteName: readString(site?.name) ?? DEFAULT_SITE_SEO_SETTINGS.siteName,
215
+ siteUrl: readString(site?.url) ?? DEFAULT_SITE_SEO_SETTINGS.siteUrl,
216
+ defaultDescription: (typeof description === "string" ? description : null) ?? DEFAULT_SITE_SEO_SETTINGS.defaultDescription,
217
+ defaultOgImage: readString(seo?.defaultOgImage) ?? DEFAULT_SITE_SEO_SETTINGS.defaultOgImage,
218
+ twitterHandle: readString(seo?.twitterHandle) ?? DEFAULT_SITE_SEO_SETTINGS.twitterHandle,
219
+ defaultLocale: readString(seo?.defaultLocale) ?? DEFAULT_SITE_SEO_SETTINGS.defaultLocale
220
+ };
221
+ }
222
+ function readObject(v) {
223
+ if (v && typeof v === "object" && !Array.isArray(v)) {
224
+ return v;
225
+ }
226
+ return null;
227
+ }
228
+ function readString(v) {
229
+ if (typeof v === "string" && v.trim().length > 0) return v.trim();
230
+ return null;
231
+ }
232
+ async function buildPageMetadata(input = {}) {
233
+ const settings = await getSiteSeoSettings();
234
+ const path = normalizePath(input.path);
235
+ const titleText = input.title?.trim() ? `${input.title.trim()} \xB7 ${settings.siteName}` : settings.siteName;
236
+ const descriptionText = input.description?.trim() ?? settings.defaultDescription;
237
+ const canonicalUrl = `${settings.siteUrl.replace(/\/+$/, "")}${path}`;
238
+ const ogImage = resolveOgImage(input.ogImage, settings);
239
+ const ogType = input.ogType ?? "website";
240
+ const metadata = {
241
+ title: titleText,
242
+ description: descriptionText,
243
+ alternates: { canonical: canonicalUrl },
244
+ openGraph: {
245
+ title: titleText,
246
+ description: descriptionText,
247
+ siteName: settings.siteName,
248
+ url: canonicalUrl,
249
+ type: ogType,
250
+ // Page-supplied locale wins over the site default so
251
+ // translated copies surface their actual language to
252
+ // social previews. Falls back to the site setting when
253
+ // the caller doesn't pass one (non-i18n routes).
254
+ locale: input.locale ?? settings.defaultLocale,
255
+ ...ogImage ? { images: [{ url: ogImage }] } : {},
256
+ ...ogType === "article" && input.publishedTime ? { publishedTime: input.publishedTime.toISOString() } : {},
257
+ ...ogType === "article" && input.modifiedTime ? { modifiedTime: input.modifiedTime.toISOString() } : {}
258
+ },
259
+ twitter: {
260
+ card: ogImage ? "summary_large_image" : "summary",
261
+ title: titleText,
262
+ description: descriptionText,
263
+ ...settings.twitterHandle ? { site: `@${settings.twitterHandle}` } : {},
264
+ ...ogImage ? { images: [ogImage] } : {}
265
+ }
266
+ };
267
+ return metadata;
268
+ }
269
+ function normalizePath(raw) {
270
+ if (!raw || !raw.startsWith("/")) return "/";
271
+ if (raw === "/") return "/";
272
+ return raw.replace(/\/+$/, "");
273
+ }
274
+ function resolveOgImage(pageImage, settings) {
275
+ const candidate = pageImage?.trim() || settings.defaultOgImage;
276
+ if (!candidate) return null;
277
+ if (/^https?:\/\//i.test(candidate)) return candidate;
278
+ if (candidate.startsWith("/")) {
279
+ return `${settings.siteUrl.replace(/\/+$/, "")}${candidate}`;
280
+ }
281
+ return candidate;
282
+ }
283
+ function validateSeoSettingsPatch(patch) {
284
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
285
+ throw new Error("Body must be a JSON object");
286
+ }
287
+ const raw = patch;
288
+ const out = {};
289
+ if ("defaultOgImage" in raw) {
290
+ const v = raw.defaultOgImage;
291
+ if (v === null || v === "") {
292
+ out.defaultOgImage = null;
293
+ } else if (typeof v === "string") {
294
+ const trimmed = v.trim();
295
+ if (!/^https?:\/\//i.test(trimmed) && !trimmed.startsWith("/")) {
296
+ throw new Error(
297
+ "defaultOgImage must be an absolute URL or a /-rooted path"
298
+ );
299
+ }
300
+ out.defaultOgImage = trimmed;
301
+ } else {
302
+ throw new Error("defaultOgImage must be a string or null");
303
+ }
304
+ }
305
+ if ("twitterHandle" in raw) {
306
+ const v = raw.twitterHandle;
307
+ if (v === null || v === "") {
308
+ out.twitterHandle = null;
309
+ } else if (typeof v === "string") {
310
+ const trimmed = v.trim().replace(/^@/, "");
311
+ if (!/^[A-Za-z0-9_]{1,15}$/.test(trimmed)) {
312
+ throw new Error(
313
+ "twitterHandle must be 1\u201315 alphanumeric/underscore characters"
314
+ );
315
+ }
316
+ out.twitterHandle = trimmed;
317
+ } else {
318
+ throw new Error("twitterHandle must be a string or null");
319
+ }
320
+ }
321
+ if ("defaultLocale" in raw) {
322
+ const v = raw.defaultLocale;
323
+ if (v === null || v === "") {
324
+ out.defaultLocale = null;
325
+ } else if (typeof v === "string") {
326
+ const trimmed = v.trim();
327
+ if (!/^[a-z]{2,3}(?:[_-][A-Za-z0-9]{2,8})?$/.test(trimmed)) {
328
+ throw new Error("defaultLocale must look like 'en' or 'en_US'");
329
+ }
330
+ out.defaultLocale = trimmed.replace("-", "_");
331
+ } else {
332
+ throw new Error("defaultLocale must be a string or null");
333
+ }
334
+ }
335
+ return out;
336
+ }
337
+
338
+ // src/seo/feed.ts
339
+ var DEFAULT_FEED_LIMIT = 50;
340
+ var DEFAULT_FEED_COLLECTION = "posts";
341
+ async function buildAtomFeed(options = {}) {
342
+ const collection = options.collection ?? DEFAULT_FEED_COLLECTION;
343
+ const limit = options.limit ?? DEFAULT_FEED_LIMIT;
344
+ let config;
345
+ try {
346
+ config = getCollectionConfig(collection);
347
+ } catch {
348
+ return null;
349
+ }
350
+ const urlPath = config.seo?.urlPath;
351
+ if (!urlPath) return null;
352
+ const settings = await getSiteSeoSettings();
353
+ const origin = settings.siteUrl.replace(/\/+$/, "");
354
+ let result;
355
+ try {
356
+ result = await findDocuments(
357
+ collection,
358
+ {
359
+ where: { status: "published" },
360
+ limit,
361
+ page: 1,
362
+ sort: "-updatedAt",
363
+ // Phase 12.4 — when the caller passed a locale AND
364
+ // this collection is i18n-enabled, scope the feed to
365
+ // that locale. findDocuments() ignores `locale` for
366
+ // non-i18n collections so passing it unconditionally
367
+ // is safe; we still gate on config.i18n to keep the
368
+ // intent obvious to readers.
369
+ ...options.locale && config.i18n ? { locale: options.locale } : {}
370
+ },
371
+ void 0
372
+ );
373
+ } catch {
374
+ return null;
375
+ }
376
+ const entries = [];
377
+ for (const doc of result.docs) {
378
+ const path = urlPath(doc);
379
+ if (!path) continue;
380
+ const link = `${origin}${path}`;
381
+ const updated = pickIso(
382
+ doc.updatedAt ?? doc.createdAt
383
+ );
384
+ if (!updated) continue;
385
+ entries.push({
386
+ id: link,
387
+ title: pickTitle(doc),
388
+ summary: pickSummary(doc),
389
+ link,
390
+ author: pickAuthor(doc),
391
+ updated,
392
+ published: pickIso(
393
+ doc.publishedAt ?? doc.createdAt
394
+ )
395
+ });
396
+ }
397
+ const extras = options.extraEntries ?? [];
398
+ if (extras.length > 0) {
399
+ const seenIds = new Set(entries.map((e) => e.id));
400
+ for (const extra of extras) {
401
+ if (seenIds.has(extra.id)) continue;
402
+ seenIds.add(extra.id);
403
+ entries.push(extra);
404
+ }
405
+ entries.sort((a, b) => a.updated < b.updated ? 1 : -1);
406
+ if (entries.length > limit) entries.length = limit;
407
+ }
408
+ return { entries, collection };
409
+ }
410
+ function pickTitle(doc) {
411
+ if (typeof doc.title === "string" && doc.title.length > 0) return doc.title;
412
+ if (typeof doc.name === "string" && doc.name.length > 0) return doc.name;
413
+ if (typeof doc.slug === "string" && doc.slug.length > 0) return doc.slug;
414
+ return "Untitled";
415
+ }
416
+ function pickSummary(doc) {
417
+ for (const key of ["excerpt", "summary", "description", "seoDescription"]) {
418
+ const value = doc[key];
419
+ if (typeof value === "string" && value.trim().length > 0) {
420
+ const trimmed = value.trim();
421
+ return trimmed.length > 500 ? `${trimmed.slice(0, 497)}\u2026` : trimmed;
422
+ }
423
+ }
424
+ return null;
425
+ }
426
+ function pickAuthor(doc) {
427
+ if (typeof doc.authorName === "string" && doc.authorName.length > 0) {
428
+ return doc.authorName;
429
+ }
430
+ return null;
431
+ }
432
+ function pickIso(value) {
433
+ if (value instanceof Date) {
434
+ const time = value.getTime();
435
+ if (Number.isNaN(time)) return null;
436
+ return value.toISOString();
437
+ }
438
+ if (typeof value === "string") {
439
+ const parsed = new Date(value);
440
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
441
+ }
442
+ return null;
443
+ }
444
+ async function renderAtomFeed(options = {}) {
445
+ const result = await buildAtomFeed(options);
446
+ if (!result) return null;
447
+ const settings = await getSiteSeoSettings();
448
+ const origin = settings.siteUrl.replace(/\/+$/, "");
449
+ const queryParts = [];
450
+ if (result.collection !== DEFAULT_FEED_COLLECTION) {
451
+ queryParts.push(`collection=${encodeURIComponent(result.collection)}`);
452
+ }
453
+ if (options.locale) {
454
+ queryParts.push(`locale=${encodeURIComponent(options.locale)}`);
455
+ }
456
+ const feedSelfUrl = `${origin}/feed.xml${queryParts.length ? `?${queryParts.join("&")}` : ""}`;
457
+ const updated = result.entries[0]?.updated ?? (/* @__PURE__ */ new Date()).toISOString();
458
+ const lines = [
459
+ '<?xml version="1.0" encoding="UTF-8"?>',
460
+ options.locale ? `<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="${escapeXml2(options.locale)}">` : '<feed xmlns="http://www.w3.org/2005/Atom">',
461
+ ` <title>${escapeXml2(settings.siteName)}</title>`,
462
+ settings.defaultDescription ? ` <subtitle>${escapeXml2(settings.defaultDescription)}</subtitle>` : "",
463
+ ` <id>${escapeXml2(feedSelfUrl)}</id>`,
464
+ ` <link rel="self" href="${escapeXml2(feedSelfUrl)}"/>`,
465
+ ` <link rel="alternate" type="text/html" href="${escapeXml2(origin)}/"/>`,
466
+ ` <updated>${updated}</updated>`
467
+ ];
468
+ for (const entry of result.entries) {
469
+ lines.push(" <entry>");
470
+ lines.push(` <id>${escapeXml2(entry.id)}</id>`);
471
+ lines.push(` <title>${escapeXml2(entry.title)}</title>`);
472
+ lines.push(
473
+ ` <link rel="alternate" type="text/html" href="${escapeXml2(entry.link)}"/>`
474
+ );
475
+ lines.push(` <updated>${entry.updated}</updated>`);
476
+ if (entry.published) {
477
+ lines.push(` <published>${entry.published}</published>`);
478
+ }
479
+ if (entry.author) {
480
+ lines.push(" <author>");
481
+ lines.push(` <name>${escapeXml2(entry.author)}</name>`);
482
+ lines.push(" </author>");
483
+ }
484
+ if (entry.summary) {
485
+ lines.push(
486
+ ` <summary type="text">${escapeXml2(entry.summary)}</summary>`
487
+ );
488
+ }
489
+ lines.push(" </entry>");
490
+ }
491
+ lines.push("</feed>");
492
+ return lines.filter(Boolean).join("\n");
493
+ }
494
+ function escapeXml2(value) {
495
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
496
+ }
497
+
498
+ // src/seo/json-ld.ts
499
+ var SCHEMA = "https://schema.org";
500
+ async function resolveOrigin(ctx = {}) {
501
+ if (ctx.origin) return ctx.origin.replace(/\/+$/, "");
502
+ const settings = await getSiteSeoSettings();
503
+ return settings.siteUrl.replace(/\/+$/, "");
504
+ }
505
+ function absoluteUrl(origin, path) {
506
+ if (/^https?:\/\//i.test(path)) return path;
507
+ return `${origin}${path.startsWith("/") ? "" : "/"}${path}`;
508
+ }
509
+ async function buildWebSiteJsonLd(ctx = {}) {
510
+ const settings = await getSiteSeoSettings();
511
+ const origin = await resolveOrigin(ctx);
512
+ return {
513
+ "@context": SCHEMA,
514
+ "@type": "WebSite",
515
+ name: settings.siteName,
516
+ url: `${origin}/`,
517
+ potentialAction: {
518
+ "@type": "SearchAction",
519
+ target: {
520
+ "@type": "EntryPoint",
521
+ urlTemplate: `${origin}/search?q={search_term_string}`
522
+ },
523
+ "query-input": "required name=search_term_string"
524
+ }
525
+ };
526
+ }
527
+ async function buildArticleJsonLd(input, ctx = {}) {
528
+ const settings = await getSiteSeoSettings();
529
+ const origin = await resolveOrigin(ctx);
530
+ const out = {
531
+ "@context": SCHEMA,
532
+ "@type": input.type ?? "BlogPosting",
533
+ headline: input.headline,
534
+ url: input.url,
535
+ publisher: {
536
+ "@type": "Organization",
537
+ name: settings.siteName,
538
+ url: `${origin}/`
539
+ }
540
+ };
541
+ if (input.description) out.description = input.description;
542
+ if (input.image) out.image = absoluteUrl(origin, input.image);
543
+ const published = toIso(input.datePublished);
544
+ if (published) out.datePublished = published;
545
+ const modified = toIso(input.dateModified);
546
+ if (modified) out.dateModified = modified;
547
+ if (input.authorName) {
548
+ out.author = { "@type": "Person", name: input.authorName };
549
+ }
550
+ return out;
551
+ }
552
+ async function buildDiscussionForumPostingJsonLd(input, ctx = {}) {
553
+ const article = await buildArticleJsonLd(input, ctx);
554
+ return { ...article, "@type": "DiscussionForumPosting" };
555
+ }
556
+ async function buildPersonJsonLd(input, ctx = {}) {
557
+ const origin = await resolveOrigin(ctx);
558
+ const out = {
559
+ "@context": SCHEMA,
560
+ "@type": "Person",
561
+ name: input.name,
562
+ url: input.url
563
+ };
564
+ if (input.alternateName) out.alternateName = input.alternateName;
565
+ if (input.image) out.image = absoluteUrl(origin, input.image);
566
+ if (input.description) out.description = input.description;
567
+ return out;
568
+ }
569
+ function toIso(value) {
570
+ if (!value) return null;
571
+ if (value instanceof Date) {
572
+ if (Number.isNaN(value.getTime())) return null;
573
+ return value.toISOString();
574
+ }
575
+ if (typeof value === "string") {
576
+ const parsed = new Date(value);
577
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
578
+ }
579
+ return null;
580
+ }
581
+
582
+ export {
583
+ buildSitemap,
584
+ renderSitemapXml,
585
+ renderSitemapIndexXml,
586
+ DEFAULT_SITE_SEO_SETTINGS,
587
+ getSiteSeoSettings,
588
+ buildPageMetadata,
589
+ validateSeoSettingsPatch,
590
+ buildAtomFeed,
591
+ renderAtomFeed,
592
+ buildWebSiteJsonLd,
593
+ buildArticleJsonLd,
594
+ buildDiscussionForumPostingJsonLd,
595
+ buildPersonJsonLd
596
+ };
597
+ //# sourceMappingURL=chunk-DP2PREDU.js.map