@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.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/audit-54XLVCWD.js +14 -0
- package/dist/audit-54XLVCWD.js.map +1 -0
- package/dist/auth.d.ts +640 -0
- package/dist/auth.js +94 -0
- package/dist/auth.js.map +1 -0
- package/dist/can-YLUHRJAB.js +19 -0
- package/dist/can-YLUHRJAB.js.map +1 -0
- package/dist/chunk-2G264RCD.js +68 -0
- package/dist/chunk-2G264RCD.js.map +1 -0
- package/dist/chunk-2YDGE7YX.js +92 -0
- package/dist/chunk-2YDGE7YX.js.map +1 -0
- package/dist/chunk-473S4TER.js +538 -0
- package/dist/chunk-473S4TER.js.map +1 -0
- package/dist/chunk-4ZLMEKFX.js +18 -0
- package/dist/chunk-4ZLMEKFX.js.map +1 -0
- package/dist/chunk-55FU6WED.js +179 -0
- package/dist/chunk-55FU6WED.js.map +1 -0
- package/dist/chunk-6YI5K2TI.js +1959 -0
- package/dist/chunk-6YI5K2TI.js.map +1 -0
- package/dist/chunk-BHK3AD3Q.js +41 -0
- package/dist/chunk-BHK3AD3Q.js.map +1 -0
- package/dist/chunk-CRUQBZUF.js +39 -0
- package/dist/chunk-CRUQBZUF.js.map +1 -0
- package/dist/chunk-CTSQ7BRI.js +175 -0
- package/dist/chunk-CTSQ7BRI.js.map +1 -0
- package/dist/chunk-DK2JBJH7.js +81 -0
- package/dist/chunk-DK2JBJH7.js.map +1 -0
- package/dist/chunk-DP2PREDU.js +597 -0
- package/dist/chunk-DP2PREDU.js.map +1 -0
- package/dist/chunk-EQ2Z3KMD.js +24 -0
- package/dist/chunk-EQ2Z3KMD.js.map +1 -0
- package/dist/chunk-FZ7O6DWI.js +305 -0
- package/dist/chunk-FZ7O6DWI.js.map +1 -0
- package/dist/chunk-ISLYFQWL.js +1270 -0
- package/dist/chunk-ISLYFQWL.js.map +1 -0
- package/dist/chunk-JJL74ZPK.js +68 -0
- package/dist/chunk-JJL74ZPK.js.map +1 -0
- package/dist/chunk-JKXAPSU4.js +24 -0
- package/dist/chunk-JKXAPSU4.js.map +1 -0
- package/dist/chunk-KU5M27ZC.js +24 -0
- package/dist/chunk-KU5M27ZC.js.map +1 -0
- package/dist/chunk-LSHHRDVR.js +34 -0
- package/dist/chunk-LSHHRDVR.js.map +1 -0
- package/dist/chunk-M43PGOQY.js +715 -0
- package/dist/chunk-M43PGOQY.js.map +1 -0
- package/dist/chunk-MEJAHXIO.js +150 -0
- package/dist/chunk-MEJAHXIO.js.map +1 -0
- package/dist/chunk-NUCGHWCF.js +101 -0
- package/dist/chunk-NUCGHWCF.js.map +1 -0
- package/dist/chunk-OK5HOCQI.js +845 -0
- package/dist/chunk-OK5HOCQI.js.map +1 -0
- package/dist/chunk-OROPGO65.js +13 -0
- package/dist/chunk-OROPGO65.js.map +1 -0
- package/dist/chunk-PPAS4SZR.js +176 -0
- package/dist/chunk-PPAS4SZR.js.map +1 -0
- package/dist/chunk-PPBWRKO2.js +171 -0
- package/dist/chunk-PPBWRKO2.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QO7LAQZH.js +321 -0
- package/dist/chunk-QO7LAQZH.js.map +1 -0
- package/dist/chunk-QVJ2HCAX.js +225 -0
- package/dist/chunk-QVJ2HCAX.js.map +1 -0
- package/dist/chunk-RIPHIRPP.js +68 -0
- package/dist/chunk-RIPHIRPP.js.map +1 -0
- package/dist/chunk-S27S42QY.js +134 -0
- package/dist/chunk-S27S42QY.js.map +1 -0
- package/dist/chunk-SBCVAC2Z.js +40 -0
- package/dist/chunk-SBCVAC2Z.js.map +1 -0
- package/dist/chunk-TFJ4MKPH.js +694 -0
- package/dist/chunk-TFJ4MKPH.js.map +1 -0
- package/dist/chunk-THX3SHYA.js +75 -0
- package/dist/chunk-THX3SHYA.js.map +1 -0
- package/dist/chunk-UGQSQO5B.js +222 -0
- package/dist/chunk-UGQSQO5B.js.map +1 -0
- package/dist/chunk-V2UNHGAP.js +26 -0
- package/dist/chunk-V2UNHGAP.js.map +1 -0
- package/dist/chunk-VGTPQXNQ.js +2790 -0
- package/dist/chunk-VGTPQXNQ.js.map +1 -0
- package/dist/chunk-VNIHXQ7W.js +194 -0
- package/dist/chunk-VNIHXQ7W.js.map +1 -0
- package/dist/chunk-WV272MPW.js +31 -0
- package/dist/chunk-WV272MPW.js.map +1 -0
- package/dist/chunk-X5KKBOUS.js +26 -0
- package/dist/chunk-X5KKBOUS.js.map +1 -0
- package/dist/chunk-XANPEOJC.js +17 -0
- package/dist/chunk-XANPEOJC.js.map +1 -0
- package/dist/chunk-XPVQIHAQ.js +83 -0
- package/dist/chunk-XPVQIHAQ.js.map +1 -0
- package/dist/chunk-ZCINJSS4.js +75 -0
- package/dist/chunk-ZCINJSS4.js.map +1 -0
- package/dist/community.d.ts +1425 -0
- package/dist/community.js +206 -0
- package/dist/community.js.map +1 -0
- package/dist/config-2GDU7PCK.js +32 -0
- package/dist/config-2GDU7PCK.js.map +1 -0
- package/dist/context-MNZ4QXPC.js +16 -0
- package/dist/context-MNZ4QXPC.js.map +1 -0
- package/dist/db-schema.d.ts +4 -0
- package/dist/db-schema.js +102 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.d.ts +7 -0
- package/dist/db.js +117 -0
- package/dist/db.js.map +1 -0
- package/dist/digest-SY42GQSU.js +17 -0
- package/dist/digest-SY42GQSU.js.map +1 -0
- package/dist/errors-5OS3S2J3.js +22 -0
- package/dist/errors-5OS3S2J3.js.map +1 -0
- package/dist/host-OBOI4MJK.js +51 -0
- package/dist/host-OBOI4MJK.js.map +1 -0
- package/dist/i18n.d.ts +301 -0
- package/dist/i18n.js +68 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index-B6-_vr_m.d.ts +590 -0
- package/dist/index-CY55LC0u.d.ts +4722 -0
- package/dist/index-CeiTvwbp.d.ts +168 -0
- package/dist/index-XwP1ET8b.d.ts +61 -0
- package/dist/index.d.ts +2037 -0
- package/dist/index.js +2205 -0
- package/dist/index.js.map +1 -0
- package/dist/job-log-VZXWQUDK.js +24 -0
- package/dist/job-log-VZXWQUDK.js.map +1 -0
- package/dist/jobs.d.ts +4 -0
- package/dist/jobs.js +76 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger-DqGaOU_j.d.ts +29 -0
- package/dist/logger-S7REWDNE.js +16 -0
- package/dist/logger-S7REWDNE.js.map +1 -0
- package/dist/media.d.ts +5 -0
- package/dist/media.js +41 -0
- package/dist/media.js.map +1 -0
- package/dist/mentions-2IHFVSHW.js +23 -0
- package/dist/mentions-2IHFVSHW.js.map +1 -0
- package/dist/mutes-EWAE5FZR.js +21 -0
- package/dist/mutes-EWAE5FZR.js.map +1 -0
- package/dist/notification-prefs-VPJDU7I6.js +21 -0
- package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
- package/dist/observability.d.ts +156 -0
- package/dist/observability.js +32 -0
- package/dist/observability.js.map +1 -0
- package/dist/profanity-adapter-NU2JQSLX.js +12 -0
- package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
- package/dist/queue-XE5BC75T.js +14 -0
- package/dist/queue-XE5BC75T.js.map +1 -0
- package/dist/rate-limit.d.ts +99 -0
- package/dist/rate-limit.js +14 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry-XIXDEPVI.js +31 -0
- package/dist/registry-XIXDEPVI.js.map +1 -0
- package/dist/reputation-JRL2YQHM.js +11 -0
- package/dist/reputation-JRL2YQHM.js.map +1 -0
- package/dist/routes.d.ts +43 -0
- package/dist/routes.js +12 -0
- package/dist/routes.js.map +1 -0
- package/dist/scheduled-CIQM57HT.js +20 -0
- package/dist/scheduled-CIQM57HT.js.map +1 -0
- package/dist/seo.d.ts +410 -0
- package/dist/seo.js +44 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings-FOBIESPB.js +17 -0
- package/dist/settings-FOBIESPB.js.map +1 -0
- package/dist/spam-adapter-XX3G737Z.js +12 -0
- package/dist/spam-adapter-XX3G737Z.js.map +1 -0
- package/dist/strings-VAE47B2C.js +29 -0
- package/dist/strings-VAE47B2C.js.map +1 -0
- package/dist/templates-IFVJMCJ6.js +12 -0
- package/dist/templates-IFVJMCJ6.js.map +1 -0
- package/dist/types-TlsbXS0T.d.ts +871 -0
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|