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