@consilioweb/payload-seo-analyzer 1.10.0 → 1.11.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/README.md +4 -0
- package/dist/client.cjs +330 -83
- package/dist/client.js +330 -83
- package/dist/index.cjs +370 -17
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +370 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3441,6 +3441,130 @@ function createAiAltTextHandler(uploadsCollection, seoConfig) {
|
|
|
3441
3441
|
};
|
|
3442
3442
|
}
|
|
3443
3443
|
|
|
3444
|
+
// src/endpoints/aiContentBrief.ts
|
|
3445
|
+
var DEFAULT_MODEL3 = "claude-opus-4-8";
|
|
3446
|
+
var trimList = (arr, max, itemMax = 160) => Array.isArray(arr) ? arr.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean).slice(0, max).map((x) => x.length > itemMax ? `${x.slice(0, itemMax - 1)}\u2026` : x) : [];
|
|
3447
|
+
function parseBrief(raw) {
|
|
3448
|
+
let s = raw.trim();
|
|
3449
|
+
if (s.startsWith("```")) s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
3450
|
+
if (!s.startsWith("{")) {
|
|
3451
|
+
const start = s.indexOf("{");
|
|
3452
|
+
const end = s.lastIndexOf("}");
|
|
3453
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
3454
|
+
s = s.slice(start, end + 1);
|
|
3455
|
+
}
|
|
3456
|
+
try {
|
|
3457
|
+
const p = JSON.parse(s);
|
|
3458
|
+
return sanitizeBrief({
|
|
3459
|
+
outline: Array.isArray(p.outline) ? p.outline.map((o) => {
|
|
3460
|
+
const r = o || {};
|
|
3461
|
+
return { level: r.level === "h3" ? "h3" : "h2", text: typeof r.text === "string" ? r.text : "" };
|
|
3462
|
+
}) : [],
|
|
3463
|
+
entities: trimList(p.entities, 30),
|
|
3464
|
+
questions: trimList(p.questions, 15),
|
|
3465
|
+
internalLinkIdeas: trimList(p.internalLinkIdeas, 10),
|
|
3466
|
+
recommendedWordCount: typeof p.recommendedWordCount === "number" ? p.recommendedWordCount : 0,
|
|
3467
|
+
notes: trimList(p.notes, 6)
|
|
3468
|
+
});
|
|
3469
|
+
} catch {
|
|
3470
|
+
return null;
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
function sanitizeBrief(b) {
|
|
3474
|
+
return {
|
|
3475
|
+
outline: b.outline.filter((o) => o.text && o.text.trim()).slice(0, 25).map((o) => ({ level: o.level === "h3" ? "h3" : "h2", text: o.text.trim().slice(0, 160) })),
|
|
3476
|
+
entities: trimList(b.entities, 30),
|
|
3477
|
+
questions: trimList(b.questions, 15),
|
|
3478
|
+
internalLinkIdeas: trimList(b.internalLinkIdeas, 10),
|
|
3479
|
+
recommendedWordCount: Math.min(1e4, Math.max(0, Math.round(b.recommendedWordCount || 0))),
|
|
3480
|
+
notes: trimList(b.notes, 6)
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
async function callClaudeBrief(apiKey, model, language, params) {
|
|
3484
|
+
const systemPrompt = `You are an SEO content strategist applying June 2026 best practices.
|
|
3485
|
+
Produce a concise WRITING BRIEF for the target keyword so a writer can create a page that ranks AND is citable by AI engines.
|
|
3486
|
+
Rules:
|
|
3487
|
+
- Base the brief on genuine search intent for the keyword; cover entities and questions a complete page must address.
|
|
3488
|
+
- Be specific and non-generic; no filler. Write in ${language === "en" ? "English" : "French"}.
|
|
3489
|
+
- Do not invent facts, brands, prices or statistics.
|
|
3490
|
+
Return ONLY a JSON object (no markdown, no prose) with EXACTLY this shape:
|
|
3491
|
+
{"outline":[{"level":"h2"|"h3","text":string}],"entities":[string],"questions":[string],"internalLinkIdeas":[string],"recommendedWordCount":number,"notes":[string]}
|
|
3492
|
+
- outline: 5-12 headings (logical H2/H3 structure).
|
|
3493
|
+
- entities: 8-20 key terms/concepts to mention.
|
|
3494
|
+
- questions: 4-10 questions the page should answer (People-Also-Ask style).
|
|
3495
|
+
- internalLinkIdeas: 3-8 topics worth linking to internally.
|
|
3496
|
+
- notes: up to 4 short strategic tips.`;
|
|
3497
|
+
const userPrompt = `Target keyword: ${params.keyword}
|
|
3498
|
+
${params.pageTitle ? `Existing page title: ${params.pageTitle}` : ""}
|
|
3499
|
+
${params.existingContent ? `Existing content (first 2000 chars, complement it \u2014 don't repeat):
|
|
3500
|
+
${params.existingContent.substring(0, 2e3)}` : ""}
|
|
3501
|
+
|
|
3502
|
+
Return the JSON brief now:`;
|
|
3503
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
3504
|
+
method: "POST",
|
|
3505
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
3506
|
+
body: JSON.stringify({
|
|
3507
|
+
model,
|
|
3508
|
+
max_tokens: 1500,
|
|
3509
|
+
system: systemPrompt,
|
|
3510
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
3511
|
+
})
|
|
3512
|
+
});
|
|
3513
|
+
if (!response.ok) {
|
|
3514
|
+
const body = await response.text();
|
|
3515
|
+
throw new Error(`Claude API error ${response.status}: ${body}`);
|
|
3516
|
+
}
|
|
3517
|
+
const data = await response.json();
|
|
3518
|
+
if (data.stop_reason === "refusal") return null;
|
|
3519
|
+
const text = (data.content?.find((b) => b.type === "text")?.text || "").trim();
|
|
3520
|
+
if (!text) return null;
|
|
3521
|
+
return parseBrief(text);
|
|
3522
|
+
}
|
|
3523
|
+
function createAiContentBriefHandler(targetCollections, seoConfig) {
|
|
3524
|
+
return async (req) => {
|
|
3525
|
+
try {
|
|
3526
|
+
if (!req.user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
3527
|
+
const body = await parseJsonBody(req);
|
|
3528
|
+
const keyword = typeof body.keyword === "string" ? body.keyword.trim() : "";
|
|
3529
|
+
if (!keyword) return Response.json({ error: "Missing required field: keyword" }, { status: 400 });
|
|
3530
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
3531
|
+
if (!apiKey) {
|
|
3532
|
+
return Response.json(
|
|
3533
|
+
{ error: "AI not configured. Set ANTHROPIC_API_KEY to generate a content brief.", code: "no_api_key" },
|
|
3534
|
+
{ status: 400 }
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
let pageTitle;
|
|
3538
|
+
let existingContent;
|
|
3539
|
+
const collection = typeof body.collection === "string" ? body.collection : void 0;
|
|
3540
|
+
const id = body.id != null ? String(body.id) : void 0;
|
|
3541
|
+
if (collection && id && (!targetCollections || targetCollections.includes(collection))) {
|
|
3542
|
+
try {
|
|
3543
|
+
const doc = await req.payload.findByID({ collection, id, depth: 1, overrideAccess: true });
|
|
3544
|
+
pageTitle = doc.title || void 0;
|
|
3545
|
+
existingContent = extractDocContent(doc).text || void 0;
|
|
3546
|
+
} catch {
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
const model = process.env.SEO_AI_MODEL || DEFAULT_MODEL3;
|
|
3550
|
+
const language = seoConfig?.locale === "en" ? "en" : "fr";
|
|
3551
|
+
let brief;
|
|
3552
|
+
try {
|
|
3553
|
+
brief = await callClaudeBrief(apiKey, model, language, { keyword, pageTitle, existingContent });
|
|
3554
|
+
} catch (e) {
|
|
3555
|
+
req.payload.logger.error(`[seo] ai-content-brief Claude error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
3556
|
+
return Response.json({ error: "Content brief generation failed." }, { status: 502 });
|
|
3557
|
+
}
|
|
3558
|
+
if (!brief) return Response.json({ error: "The model did not return a brief (possibly declined)." }, { status: 502 });
|
|
3559
|
+
return Response.json({ keyword, brief, model });
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
3562
|
+
req.payload.logger.error(`[seo] ai-content-brief error: ${message}`);
|
|
3563
|
+
return Response.json({ error: message }, { status: 500 });
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3444
3568
|
// src/endpoints/cannibalization.ts
|
|
3445
3569
|
function canonicalIntent(keyword) {
|
|
3446
3570
|
return keyword.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter(Boolean).sort().join(" ");
|
|
@@ -5238,24 +5362,46 @@ function buildArticleSchema(doc, siteUrl) {
|
|
|
5238
5362
|
}
|
|
5239
5363
|
return schema;
|
|
5240
5364
|
}
|
|
5241
|
-
function
|
|
5365
|
+
function buildLocationNode(loc, doc, siteUrl) {
|
|
5242
5366
|
const meta = doc.meta || {};
|
|
5243
|
-
const
|
|
5244
|
-
"@
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
url: `${siteUrl}/${doc.slug || ""}`
|
|
5367
|
+
const node = {
|
|
5368
|
+
"@type": typeof loc.type === "string" && loc.type || "LocalBusiness",
|
|
5369
|
+
name: loc.name || doc.title || meta.title || "",
|
|
5370
|
+
description: loc.description || meta.description || "",
|
|
5371
|
+
url: loc.url || `${siteUrl}/${doc.slug || ""}`
|
|
5249
5372
|
};
|
|
5250
|
-
if (
|
|
5251
|
-
if (
|
|
5252
|
-
if (
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5373
|
+
if (loc.telephone) node.telephone = loc.telephone;
|
|
5374
|
+
if (loc.email) node.email = loc.email;
|
|
5375
|
+
if (loc.priceRange) node.priceRange = loc.priceRange;
|
|
5376
|
+
const address = loc.address;
|
|
5377
|
+
if (address && typeof address === "object") {
|
|
5378
|
+
node.address = { "@type": "PostalAddress", ...address };
|
|
5379
|
+
} else if (typeof address === "string" && address) {
|
|
5380
|
+
node.address = address;
|
|
5381
|
+
}
|
|
5382
|
+
const geo = loc.geo || {};
|
|
5383
|
+
const lat = geo.latitude ?? loc.latitude ?? loc.lat;
|
|
5384
|
+
const lng = geo.longitude ?? loc.longitude ?? loc.lng;
|
|
5385
|
+
if (lat != null && lng != null) {
|
|
5386
|
+
node.geo = { "@type": "GeoCoordinates", latitude: lat, longitude: lng };
|
|
5387
|
+
}
|
|
5388
|
+
if (Array.isArray(loc.openingHours) && loc.openingHours.length > 0) {
|
|
5389
|
+
node.openingHours = loc.openingHours;
|
|
5390
|
+
} else if (typeof loc.openingHours === "string" && loc.openingHours) {
|
|
5391
|
+
node.openingHours = loc.openingHours;
|
|
5392
|
+
}
|
|
5393
|
+
return node;
|
|
5394
|
+
}
|
|
5395
|
+
function buildLocalBusinessSchema(doc, siteUrl) {
|
|
5396
|
+
const locations = Array.isArray(doc.locations) ? doc.locations.filter((l) => !!l && typeof l === "object") : [];
|
|
5397
|
+
if (locations.length > 1) {
|
|
5398
|
+
return {
|
|
5399
|
+
"@context": "https://schema.org",
|
|
5400
|
+
"@graph": locations.map((loc) => buildLocationNode(loc, doc, siteUrl))
|
|
5256
5401
|
};
|
|
5257
5402
|
}
|
|
5258
|
-
|
|
5403
|
+
const base = locations.length === 1 ? locations[0] : doc;
|
|
5404
|
+
return { "@context": "https://schema.org", ...buildLocationNode(base, doc, siteUrl) };
|
|
5259
5405
|
}
|
|
5260
5406
|
function buildBreadcrumbSchema(doc, siteUrl) {
|
|
5261
5407
|
const slug = doc.slug || "";
|
|
@@ -6006,6 +6152,191 @@ function createSitemapHandler(targetCollections) {
|
|
|
6006
6152
|
};
|
|
6007
6153
|
}
|
|
6008
6154
|
|
|
6155
|
+
// src/endpoints/sitemapExtensions.ts
|
|
6156
|
+
function escapeXml2(str) {
|
|
6157
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6158
|
+
}
|
|
6159
|
+
function resolveSiteUrl3(seoConfig) {
|
|
6160
|
+
return (seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
|
|
6161
|
+
}
|
|
6162
|
+
function docPath(slug) {
|
|
6163
|
+
return slug === "home" || slug === "" ? "" : `/${slug}`;
|
|
6164
|
+
}
|
|
6165
|
+
function xmlResponse(xml, status = 200) {
|
|
6166
|
+
return new Response(xml, {
|
|
6167
|
+
status,
|
|
6168
|
+
headers: { "Content-Type": "application/xml", "Cache-Control": "public, max-age=3600, s-maxage=3600" }
|
|
6169
|
+
});
|
|
6170
|
+
}
|
|
6171
|
+
function mediaUrl(media, siteUrl) {
|
|
6172
|
+
if (typeof media.url === "string" && media.url) {
|
|
6173
|
+
return media.url.startsWith("http") ? media.url : `${siteUrl}${media.url}`;
|
|
6174
|
+
}
|
|
6175
|
+
if (typeof media.filename === "string" && media.filename) {
|
|
6176
|
+
return `${siteUrl}/media/${media.filename}`;
|
|
6177
|
+
}
|
|
6178
|
+
return void 0;
|
|
6179
|
+
}
|
|
6180
|
+
function collectMediaUrls(node, mimePrefix, siteUrl, out, depth = 0) {
|
|
6181
|
+
if (!node || typeof node !== "object" || depth > 8) return;
|
|
6182
|
+
if (Array.isArray(node)) {
|
|
6183
|
+
for (const item of node) collectMediaUrls(item, mimePrefix, siteUrl, out, depth + 1);
|
|
6184
|
+
return;
|
|
6185
|
+
}
|
|
6186
|
+
const obj = node;
|
|
6187
|
+
const mime = typeof obj.mimeType === "string" ? obj.mimeType : "";
|
|
6188
|
+
if (mime.startsWith(mimePrefix)) {
|
|
6189
|
+
const url = mediaUrl(obj, siteUrl);
|
|
6190
|
+
if (url) out.add(url);
|
|
6191
|
+
}
|
|
6192
|
+
for (const key of Object.keys(obj)) {
|
|
6193
|
+
if (key === "sizes" || key === "_status") continue;
|
|
6194
|
+
collectMediaUrls(obj[key], mimePrefix, siteUrl, out, depth + 1);
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
async function eachPublishedDoc(payload, collections, depth, onDoc) {
|
|
6198
|
+
const BATCH = Math.min(100, Math.max(1, parseInt(process.env.SEO_SITEMAP_BATCH_SIZE || "50", 10) || 50));
|
|
6199
|
+
const MAX = Math.max(1, parseInt(process.env.SEO_SITEMAP_MAX_DOCS || "5000", 10) || 5e3);
|
|
6200
|
+
let count = 0;
|
|
6201
|
+
for (const collection of collections) {
|
|
6202
|
+
try {
|
|
6203
|
+
let page = 1;
|
|
6204
|
+
let hasMore = true;
|
|
6205
|
+
while (hasMore) {
|
|
6206
|
+
const res = await payload.find({ collection, limit: BATCH, page, depth, overrideAccess: true });
|
|
6207
|
+
for (const doc of res.docs) {
|
|
6208
|
+
if (doc._status === "draft") continue;
|
|
6209
|
+
if (count >= MAX) return;
|
|
6210
|
+
onDoc(doc, collection);
|
|
6211
|
+
count++;
|
|
6212
|
+
}
|
|
6213
|
+
hasMore = res.hasNextPage;
|
|
6214
|
+
page++;
|
|
6215
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
6216
|
+
}
|
|
6217
|
+
} catch {
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
function createNewsSitemapHandler(targetCollections, seoConfig) {
|
|
6222
|
+
return async (req) => {
|
|
6223
|
+
try {
|
|
6224
|
+
const siteUrl = resolveSiteUrl3(seoConfig);
|
|
6225
|
+
const language = seoConfig?.locale === "en" ? "en" : "fr";
|
|
6226
|
+
let publication = seoConfig?.siteName || "";
|
|
6227
|
+
if (!publication && siteUrl) {
|
|
6228
|
+
try {
|
|
6229
|
+
publication = new URL(siteUrl).hostname;
|
|
6230
|
+
} catch {
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
const cutoff = Date.now() - 48 * 36e5;
|
|
6234
|
+
const entries = [];
|
|
6235
|
+
await eachPublishedDoc(req.payload, targetCollections, 0, (doc) => {
|
|
6236
|
+
const dateStr = typeof doc.publishedAt === "string" && doc.publishedAt || typeof doc.date === "string" && doc.date || typeof doc.createdAt === "string" && doc.createdAt || "";
|
|
6237
|
+
if (!dateStr) return;
|
|
6238
|
+
const t = new Date(dateStr).getTime();
|
|
6239
|
+
if (isNaN(t) || t < cutoff) return;
|
|
6240
|
+
const title = doc.title || doc.meta?.title || "";
|
|
6241
|
+
if (!title) return;
|
|
6242
|
+
const loc = `${siteUrl}${docPath(doc.slug || "")}`;
|
|
6243
|
+
entries.push(
|
|
6244
|
+
` <url>
|
|
6245
|
+
<loc>${escapeXml2(loc)}</loc>
|
|
6246
|
+
<news:news>
|
|
6247
|
+
<news:publication>
|
|
6248
|
+
<news:name>${escapeXml2(publication)}</news:name>
|
|
6249
|
+
<news:language>${language}</news:language>
|
|
6250
|
+
</news:publication>
|
|
6251
|
+
<news:publication_date>${new Date(dateStr).toISOString()}</news:publication_date>
|
|
6252
|
+
<news:title>${escapeXml2(title)}</news:title>
|
|
6253
|
+
</news:news>
|
|
6254
|
+
</url>`
|
|
6255
|
+
);
|
|
6256
|
+
});
|
|
6257
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6258
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
|
|
6259
|
+
${entries.join("\n")}
|
|
6260
|
+
</urlset>`;
|
|
6261
|
+
return xmlResponse(xml);
|
|
6262
|
+
} catch (error) {
|
|
6263
|
+
req.payload.logger.error(`[seo] sitemap-news error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
6264
|
+
return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
|
|
6265
|
+
}
|
|
6266
|
+
};
|
|
6267
|
+
}
|
|
6268
|
+
function createImageSitemapHandler(targetCollections, seoConfig) {
|
|
6269
|
+
return async (req) => {
|
|
6270
|
+
try {
|
|
6271
|
+
const siteUrl = resolveSiteUrl3(seoConfig);
|
|
6272
|
+
const entries = [];
|
|
6273
|
+
await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
|
|
6274
|
+
const urls = /* @__PURE__ */ new Set();
|
|
6275
|
+
collectMediaUrls(doc, "image/", siteUrl, urls);
|
|
6276
|
+
if (urls.size === 0) return;
|
|
6277
|
+
const loc = `${siteUrl}${docPath(doc.slug || "")}`;
|
|
6278
|
+
const imgs = Array.from(urls).slice(0, 1e3).map((u) => ` <image:image><image:loc>${escapeXml2(u)}</image:loc></image:image>`).join("\n");
|
|
6279
|
+
entries.push(` <url>
|
|
6280
|
+
<loc>${escapeXml2(loc)}</loc>
|
|
6281
|
+
${imgs}
|
|
6282
|
+
</url>`);
|
|
6283
|
+
});
|
|
6284
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6285
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
|
6286
|
+
${entries.join("\n")}
|
|
6287
|
+
</urlset>`;
|
|
6288
|
+
return xmlResponse(xml);
|
|
6289
|
+
} catch (error) {
|
|
6290
|
+
req.payload.logger.error(`[seo] sitemap-images error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
6291
|
+
return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
|
|
6292
|
+
}
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
6295
|
+
function createVideoSitemapHandler(targetCollections, seoConfig) {
|
|
6296
|
+
return async (req) => {
|
|
6297
|
+
try {
|
|
6298
|
+
const siteUrl = resolveSiteUrl3(seoConfig);
|
|
6299
|
+
const entries = [];
|
|
6300
|
+
await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
|
|
6301
|
+
const meta = doc.meta || {};
|
|
6302
|
+
const videoUrls = /* @__PURE__ */ new Set();
|
|
6303
|
+
collectMediaUrls(doc, "video/", siteUrl, videoUrls);
|
|
6304
|
+
for (const k of ["videoUrl", "contentUrl", "playerUrl"]) {
|
|
6305
|
+
if (typeof doc[k] === "string" && doc[k]) videoUrls.add(doc[k]);
|
|
6306
|
+
}
|
|
6307
|
+
if (videoUrls.size === 0) return;
|
|
6308
|
+
const title = doc.title || meta.title || "";
|
|
6309
|
+
const description = meta.description || title;
|
|
6310
|
+
const thumbs = /* @__PURE__ */ new Set();
|
|
6311
|
+
collectMediaUrls(meta.image, "image/", siteUrl, thumbs);
|
|
6312
|
+
if (thumbs.size === 0) collectMediaUrls(doc, "image/", siteUrl, thumbs);
|
|
6313
|
+
const thumbnail = Array.from(thumbs)[0] || "";
|
|
6314
|
+
const loc = `${siteUrl}${docPath(doc.slug || "")}`;
|
|
6315
|
+
const videos = Array.from(videoUrls).slice(0, 100).map(
|
|
6316
|
+
(u) => ` <video:video>
|
|
6317
|
+
${thumbnail ? ` <video:thumbnail_loc>${escapeXml2(thumbnail)}</video:thumbnail_loc>
|
|
6318
|
+
` : ""} <video:title>${escapeXml2(title || "Video")}</video:title>
|
|
6319
|
+
<video:description>${escapeXml2(description || title || "Video")}</video:description>
|
|
6320
|
+
<video:content_loc>${escapeXml2(u)}</video:content_loc>
|
|
6321
|
+
</video:video>`
|
|
6322
|
+
).join("\n");
|
|
6323
|
+
entries.push(` <url>
|
|
6324
|
+
<loc>${escapeXml2(loc)}</loc>
|
|
6325
|
+
${videos}
|
|
6326
|
+
</url>`);
|
|
6327
|
+
});
|
|
6328
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6329
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
|
6330
|
+
${entries.join("\n")}
|
|
6331
|
+
</urlset>`;
|
|
6332
|
+
return xmlResponse(xml);
|
|
6333
|
+
} catch (error) {
|
|
6334
|
+
req.payload.logger.error(`[seo] sitemap-video error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
6335
|
+
return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
|
|
6336
|
+
}
|
|
6337
|
+
};
|
|
6338
|
+
}
|
|
6339
|
+
|
|
6009
6340
|
// src/collections/SeoScoreHistory.ts
|
|
6010
6341
|
function createSeoScoreHistoryCollection() {
|
|
6011
6342
|
return {
|
|
@@ -8494,6 +8825,9 @@ var fr = {
|
|
|
8494
8825
|
pagesAnalyzed: "pages analys\xE9es",
|
|
8495
8826
|
markCornerstone: "Marquer pilier",
|
|
8496
8827
|
unmarkCornerstone: "D\xE9marquer pilier",
|
|
8828
|
+
bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
|
|
8829
|
+
bulkOptimizing: "Optimisation\u2026",
|
|
8830
|
+
bulkConfirm: "Confirmer ?",
|
|
8497
8831
|
searchPlaceholder: "Rechercher (titre, slug, keyword)...",
|
|
8498
8832
|
allCollections: "Toutes les collections",
|
|
8499
8833
|
allScores: "Tous les scores",
|
|
@@ -9086,6 +9420,9 @@ var en = {
|
|
|
9086
9420
|
pagesAnalyzed: "pages analyzed",
|
|
9087
9421
|
markCornerstone: "Mark as cornerstone",
|
|
9088
9422
|
unmarkCornerstone: "Unmark cornerstone",
|
|
9423
|
+
bulkOptimizeMeta: "Optimize meta (AI)",
|
|
9424
|
+
bulkOptimizing: "Optimizing\u2026",
|
|
9425
|
+
bulkConfirm: "Confirm?",
|
|
9089
9426
|
searchPlaceholder: "Search (title, slug, keyword)...",
|
|
9090
9427
|
allCollections: "All collections",
|
|
9091
9428
|
allScores: "All scores",
|
|
@@ -9937,7 +10274,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
9937
10274
|
{ path: `${basePath}/ai-rewrite`, method: "post", handler: createAiRewriteHandler(targetCollections) },
|
|
9938
10275
|
{ path: `${basePath}/ai-optimize`, method: "post", handler: createAiOptimizeHandler(targetCollections, seoConfig) },
|
|
9939
10276
|
{ path: `${basePath}/alt-text-audit`, method: "get", handler: createAltTextAuditHandler(uploadsCollection) },
|
|
9940
|
-
{ path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) }
|
|
10277
|
+
{ path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) },
|
|
10278
|
+
{ path: `${basePath}/ai-content-brief`, method: "post", handler: withRateLimit(createAiContentBriefHandler(targetCollections, seoConfig)) }
|
|
9941
10279
|
);
|
|
9942
10280
|
}
|
|
9943
10281
|
if (features.cannibalization) {
|
|
@@ -10029,6 +10367,21 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10029
10367
|
path: `${basePath}/sitemap.xml`,
|
|
10030
10368
|
method: "get",
|
|
10031
10369
|
handler: createSitemapHandler(targetCollections)
|
|
10370
|
+
},
|
|
10371
|
+
{
|
|
10372
|
+
path: `${basePath}/sitemap-news.xml`,
|
|
10373
|
+
method: "get",
|
|
10374
|
+
handler: createNewsSitemapHandler(targetCollections, seoConfig)
|
|
10375
|
+
},
|
|
10376
|
+
{
|
|
10377
|
+
path: `${basePath}/sitemap-images.xml`,
|
|
10378
|
+
method: "get",
|
|
10379
|
+
handler: createImageSitemapHandler(targetCollections, seoConfig)
|
|
10380
|
+
},
|
|
10381
|
+
{
|
|
10382
|
+
path: `${basePath}/sitemap-video.xml`,
|
|
10383
|
+
method: "get",
|
|
10384
|
+
handler: createVideoSitemapHandler(targetCollections, seoConfig)
|
|
10032
10385
|
}
|
|
10033
10386
|
);
|
|
10034
10387
|
config.endpoints = [
|
|
@@ -10133,7 +10486,7 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10133
10486
|
};
|
|
10134
10487
|
|
|
10135
10488
|
// src/helpers/buildMetadata.ts
|
|
10136
|
-
function
|
|
10489
|
+
function resolveSiteUrl4(explicit) {
|
|
10137
10490
|
return (explicit || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
|
|
10138
10491
|
}
|
|
10139
10492
|
function parseRobots(doc, meta) {
|
|
@@ -10167,7 +10520,7 @@ function absoluteUrl(value, siteUrl) {
|
|
|
10167
10520
|
return `${siteUrl}${value.startsWith("/") ? "" : "/"}${value}`;
|
|
10168
10521
|
}
|
|
10169
10522
|
function buildSeoMetadata(doc, options = {}) {
|
|
10170
|
-
const siteUrl =
|
|
10523
|
+
const siteUrl = resolveSiteUrl4(options.siteUrl);
|
|
10171
10524
|
const meta = doc.meta || {};
|
|
10172
10525
|
const rawTitle = meta.title || doc.title || "";
|
|
10173
10526
|
const title = options.titleTemplate && rawTitle ? options.titleTemplate.replace("%s", rawTitle) : rawTitle;
|
package/package.json
CHANGED