@consilioweb/payload-seo-analyzer 1.10.0 → 1.12.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 +5 -0
- package/dist/client.cjs +467 -83
- package/dist/client.js +467 -83
- package/dist/index.cjs +657 -96
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +657 -96
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1703,6 +1703,7 @@ async function buildAuditCache(payload, collections, globals, seoConfig) {
|
|
|
1703
1703
|
});
|
|
1704
1704
|
for (const doc of result.docs) {
|
|
1705
1705
|
if (ignoredSlugs.includes(doc.slug)) continue;
|
|
1706
|
+
if (doc._status === "draft") continue;
|
|
1706
1707
|
if (allResults.length >= MAX_DOCS2) {
|
|
1707
1708
|
capped = true;
|
|
1708
1709
|
break collectionsLoop;
|
|
@@ -2678,6 +2679,9 @@ function createRedirectsHandler(redirectsCollection) {
|
|
|
2678
2679
|
return Response.json({ error: "Missing id or ids" }, { status: 400 });
|
|
2679
2680
|
}
|
|
2680
2681
|
if (method === "PATCH") {
|
|
2682
|
+
if (!isAdmin3(req.user)) {
|
|
2683
|
+
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
2684
|
+
}
|
|
2681
2685
|
const patchBody = await parseJsonBody(req);
|
|
2682
2686
|
const id = typeof patchBody.id === "string" ? patchBody.id.trim() : void 0;
|
|
2683
2687
|
const from = typeof patchBody.from === "string" ? patchBody.from.trim() : void 0;
|
|
@@ -3019,6 +3023,77 @@ function sanitizeSuggestions(s, currentFocusKeyword) {
|
|
|
3019
3023
|
const rationale = s.rationale.map((r) => r.trim()).filter(Boolean).slice(0, RATIONALE_MAX_ITEMS).map((r) => r.length > RATIONALE_ITEM_MAX ? `${r.slice(0, RATIONALE_ITEM_MAX - 1)}\u2026` : r);
|
|
3020
3024
|
return { metaTitle, metaDescription, focusKeyword, rationale };
|
|
3021
3025
|
}
|
|
3026
|
+
async function optimizeDocMeta(payload, opts) {
|
|
3027
|
+
const { collection, id, mergedConfig } = opts;
|
|
3028
|
+
let doc;
|
|
3029
|
+
try {
|
|
3030
|
+
doc = await payload.findByID({ collection, id, depth: 1, overrideAccess: true });
|
|
3031
|
+
} catch {
|
|
3032
|
+
return { ok: false, error: `Document not found: ${collection}/${id}`, status: 404 };
|
|
3033
|
+
}
|
|
3034
|
+
const seoInput = buildSeoInputFromDoc(doc, collection);
|
|
3035
|
+
const analysis = analyzeSeo(seoInput, mergedConfig);
|
|
3036
|
+
const metaGroups = /* @__PURE__ */ new Set(["title", "meta-description", "content", "url", "headings"]);
|
|
3037
|
+
const issues = analysis.checks.filter((c) => (c.status === "fail" || c.status === "warning") && metaGroups.has(c.group)).map((c) => ({ label: c.label, message: c.message })).slice(0, 12);
|
|
3038
|
+
const pageTitle = doc.title || "";
|
|
3039
|
+
const slug = doc.slug || "";
|
|
3040
|
+
const currentFocusKeyword = doc.focusKeyword || "";
|
|
3041
|
+
const currentMetaTitle = seoInput.metaTitle || "";
|
|
3042
|
+
const currentMetaDescription = seoInput.metaDescription || "";
|
|
3043
|
+
const content = extractDocContent(doc).text;
|
|
3044
|
+
const apiKey = opts.apiKey;
|
|
3045
|
+
const model = opts.model || DEFAULT_MODEL;
|
|
3046
|
+
let suggestions;
|
|
3047
|
+
let method;
|
|
3048
|
+
const heuristicFallback = () => ({
|
|
3049
|
+
metaTitle: generateMetaTitle(pageTitle, currentFocusKeyword, slug),
|
|
3050
|
+
metaDescription: generateMetaDescription(content, currentFocusKeyword, slug),
|
|
3051
|
+
focusKeyword: currentFocusKeyword,
|
|
3052
|
+
rationale: []
|
|
3053
|
+
});
|
|
3054
|
+
if (apiKey) {
|
|
3055
|
+
try {
|
|
3056
|
+
const aiResult = await callClaudeOptimize(apiKey, model, {
|
|
3057
|
+
pageTitle,
|
|
3058
|
+
slug,
|
|
3059
|
+
focusKeyword: currentFocusKeyword,
|
|
3060
|
+
currentMetaTitle,
|
|
3061
|
+
currentMetaDescription,
|
|
3062
|
+
issues,
|
|
3063
|
+
content
|
|
3064
|
+
});
|
|
3065
|
+
if (aiResult) {
|
|
3066
|
+
suggestions = aiResult;
|
|
3067
|
+
method = "ai";
|
|
3068
|
+
} else {
|
|
3069
|
+
suggestions = heuristicFallback();
|
|
3070
|
+
method = "heuristic";
|
|
3071
|
+
}
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
payload.logger.error(`[seo] ai-optimize Claude API error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
3074
|
+
suggestions = heuristicFallback();
|
|
3075
|
+
method = "heuristic";
|
|
3076
|
+
}
|
|
3077
|
+
} else {
|
|
3078
|
+
suggestions = heuristicFallback();
|
|
3079
|
+
method = "heuristic";
|
|
3080
|
+
}
|
|
3081
|
+
const sanitized = sanitizeSuggestions(suggestions, currentFocusKeyword);
|
|
3082
|
+
return {
|
|
3083
|
+
ok: true,
|
|
3084
|
+
title: pageTitle,
|
|
3085
|
+
method,
|
|
3086
|
+
...method === "ai" ? { model } : {},
|
|
3087
|
+
score: analysis.score,
|
|
3088
|
+
current: {
|
|
3089
|
+
metaTitle: currentMetaTitle,
|
|
3090
|
+
metaDescription: currentMetaDescription,
|
|
3091
|
+
focusKeyword: currentFocusKeyword
|
|
3092
|
+
},
|
|
3093
|
+
suggestions: sanitized,
|
|
3094
|
+
issues
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3022
3097
|
function createAiOptimizeHandler(targetCollections, seoConfig, localeMapping) {
|
|
3023
3098
|
return async (req) => {
|
|
3024
3099
|
try {
|
|
@@ -3034,83 +3109,27 @@ function createAiOptimizeHandler(targetCollections, seoConfig, localeMapping) {
|
|
|
3034
3109
|
if (targetCollections && !targetCollections.includes(collection)) {
|
|
3035
3110
|
return Response.json({ error: "Collection not allowed" }, { status: 403 });
|
|
3036
3111
|
}
|
|
3037
|
-
let doc;
|
|
3038
|
-
try {
|
|
3039
|
-
const result = await req.payload.findByID({
|
|
3040
|
-
collection,
|
|
3041
|
-
id,
|
|
3042
|
-
depth: 1,
|
|
3043
|
-
overrideAccess: true
|
|
3044
|
-
});
|
|
3045
|
-
doc = result;
|
|
3046
|
-
} catch {
|
|
3047
|
-
return Response.json({ error: `Document not found: ${collection}/${id}` }, { status: 404 });
|
|
3048
|
-
}
|
|
3049
3112
|
const { config: mergedConfig } = await loadMergedConfig(req.payload, seoConfig, {
|
|
3050
3113
|
reqLocale: req.locale,
|
|
3051
3114
|
localeMapping
|
|
3052
3115
|
});
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
const currentFocusKeyword = doc.focusKeyword || "";
|
|
3060
|
-
const currentMetaTitle = seoInput.metaTitle || "";
|
|
3061
|
-
const currentMetaDescription = seoInput.metaDescription || "";
|
|
3062
|
-
const content = extractDocContent(doc).text;
|
|
3063
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
3064
|
-
const model = process.env.SEO_AI_MODEL || DEFAULT_MODEL;
|
|
3065
|
-
let suggestions;
|
|
3066
|
-
let method;
|
|
3067
|
-
const heuristicFallback = () => ({
|
|
3068
|
-
metaTitle: generateMetaTitle(pageTitle, currentFocusKeyword, slug),
|
|
3069
|
-
metaDescription: generateMetaDescription(content, currentFocusKeyword, slug),
|
|
3070
|
-
focusKeyword: currentFocusKeyword,
|
|
3071
|
-
rationale: []
|
|
3116
|
+
const r = await optimizeDocMeta(req.payload, {
|
|
3117
|
+
collection,
|
|
3118
|
+
id,
|
|
3119
|
+
mergedConfig,
|
|
3120
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
3121
|
+
model: process.env.SEO_AI_MODEL
|
|
3072
3122
|
});
|
|
3073
|
-
if (
|
|
3074
|
-
|
|
3075
|
-
const aiResult = await callClaudeOptimize(apiKey, model, {
|
|
3076
|
-
pageTitle,
|
|
3077
|
-
slug,
|
|
3078
|
-
focusKeyword: currentFocusKeyword,
|
|
3079
|
-
currentMetaTitle,
|
|
3080
|
-
currentMetaDescription,
|
|
3081
|
-
issues,
|
|
3082
|
-
content
|
|
3083
|
-
});
|
|
3084
|
-
if (aiResult) {
|
|
3085
|
-
suggestions = aiResult;
|
|
3086
|
-
method = "ai";
|
|
3087
|
-
} else {
|
|
3088
|
-
suggestions = heuristicFallback();
|
|
3089
|
-
method = "heuristic";
|
|
3090
|
-
}
|
|
3091
|
-
} catch (error) {
|
|
3092
|
-
req.payload.logger.error(
|
|
3093
|
-
`[seo] ai-optimize Claude API error: ${error instanceof Error ? error.message : "unknown"}`
|
|
3094
|
-
);
|
|
3095
|
-
suggestions = heuristicFallback();
|
|
3096
|
-
method = "heuristic";
|
|
3097
|
-
}
|
|
3098
|
-
} else {
|
|
3099
|
-
suggestions = heuristicFallback();
|
|
3100
|
-
method = "heuristic";
|
|
3123
|
+
if (!r.ok) {
|
|
3124
|
+
return Response.json({ error: r.error }, { status: r.status || 500 });
|
|
3101
3125
|
}
|
|
3102
|
-
const sanitized = sanitizeSuggestions(suggestions, currentFocusKeyword);
|
|
3103
3126
|
return Response.json({
|
|
3104
|
-
method,
|
|
3105
|
-
...method === "ai" ? { model } : {},
|
|
3106
|
-
score:
|
|
3107
|
-
current:
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
focusKeyword: currentFocusKeyword
|
|
3111
|
-
},
|
|
3112
|
-
suggestions: sanitized,
|
|
3113
|
-
issues
|
|
3127
|
+
method: r.method,
|
|
3128
|
+
...r.method === "ai" ? { model: r.model } : {},
|
|
3129
|
+
score: r.score,
|
|
3130
|
+
current: r.current,
|
|
3131
|
+
suggestions: r.suggestions,
|
|
3132
|
+
issues: r.issues
|
|
3114
3133
|
});
|
|
3115
3134
|
} catch (error) {
|
|
3116
3135
|
const message = error instanceof Error ? error.message : "Internal server error";
|
|
@@ -3441,6 +3460,301 @@ function createAiAltTextHandler(uploadsCollection, seoConfig) {
|
|
|
3441
3460
|
};
|
|
3442
3461
|
}
|
|
3443
3462
|
|
|
3463
|
+
// src/endpoints/aiContentBrief.ts
|
|
3464
|
+
var DEFAULT_MODEL3 = "claude-opus-4-8";
|
|
3465
|
+
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) : [];
|
|
3466
|
+
function parseBrief(raw) {
|
|
3467
|
+
let s = raw.trim();
|
|
3468
|
+
if (s.startsWith("```")) s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
3469
|
+
if (!s.startsWith("{")) {
|
|
3470
|
+
const start = s.indexOf("{");
|
|
3471
|
+
const end = s.lastIndexOf("}");
|
|
3472
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
3473
|
+
s = s.slice(start, end + 1);
|
|
3474
|
+
}
|
|
3475
|
+
try {
|
|
3476
|
+
const p = JSON.parse(s);
|
|
3477
|
+
return sanitizeBrief({
|
|
3478
|
+
outline: Array.isArray(p.outline) ? p.outline.map((o) => {
|
|
3479
|
+
const r = o || {};
|
|
3480
|
+
return { level: r.level === "h3" ? "h3" : "h2", text: typeof r.text === "string" ? r.text : "" };
|
|
3481
|
+
}) : [],
|
|
3482
|
+
entities: trimList(p.entities, 30),
|
|
3483
|
+
questions: trimList(p.questions, 15),
|
|
3484
|
+
internalLinkIdeas: trimList(p.internalLinkIdeas, 10),
|
|
3485
|
+
recommendedWordCount: typeof p.recommendedWordCount === "number" ? p.recommendedWordCount : 0,
|
|
3486
|
+
notes: trimList(p.notes, 6)
|
|
3487
|
+
});
|
|
3488
|
+
} catch {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
function sanitizeBrief(b) {
|
|
3493
|
+
return {
|
|
3494
|
+
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) })),
|
|
3495
|
+
entities: trimList(b.entities, 30),
|
|
3496
|
+
questions: trimList(b.questions, 15),
|
|
3497
|
+
internalLinkIdeas: trimList(b.internalLinkIdeas, 10),
|
|
3498
|
+
recommendedWordCount: Math.min(1e4, Math.max(0, Math.round(b.recommendedWordCount || 0))),
|
|
3499
|
+
notes: trimList(b.notes, 6)
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
async function callClaudeBrief(apiKey, model, language, params) {
|
|
3503
|
+
const systemPrompt = `You are an SEO content strategist applying June 2026 best practices.
|
|
3504
|
+
Produce a concise WRITING BRIEF for the target keyword so a writer can create a page that ranks AND is citable by AI engines.
|
|
3505
|
+
Rules:
|
|
3506
|
+
- Base the brief on genuine search intent for the keyword; cover entities and questions a complete page must address.
|
|
3507
|
+
- Be specific and non-generic; no filler. Write in ${language === "en" ? "English" : "French"}.
|
|
3508
|
+
- Do not invent facts, brands, prices or statistics.
|
|
3509
|
+
Return ONLY a JSON object (no markdown, no prose) with EXACTLY this shape:
|
|
3510
|
+
{"outline":[{"level":"h2"|"h3","text":string}],"entities":[string],"questions":[string],"internalLinkIdeas":[string],"recommendedWordCount":number,"notes":[string]}
|
|
3511
|
+
- outline: 5-12 headings (logical H2/H3 structure).
|
|
3512
|
+
- entities: 8-20 key terms/concepts to mention.
|
|
3513
|
+
- questions: 4-10 questions the page should answer (People-Also-Ask style).
|
|
3514
|
+
- internalLinkIdeas: 3-8 topics worth linking to internally.
|
|
3515
|
+
- notes: up to 4 short strategic tips.`;
|
|
3516
|
+
const userPrompt = `Target keyword: ${params.keyword}
|
|
3517
|
+
${params.pageTitle ? `Existing page title: ${params.pageTitle}` : ""}
|
|
3518
|
+
${params.existingContent ? `Existing content (first 2000 chars, complement it \u2014 don't repeat):
|
|
3519
|
+
${params.existingContent.substring(0, 2e3)}` : ""}
|
|
3520
|
+
|
|
3521
|
+
Return the JSON brief now:`;
|
|
3522
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
3523
|
+
method: "POST",
|
|
3524
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
3525
|
+
body: JSON.stringify({
|
|
3526
|
+
model,
|
|
3527
|
+
max_tokens: 1500,
|
|
3528
|
+
system: systemPrompt,
|
|
3529
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
3530
|
+
})
|
|
3531
|
+
});
|
|
3532
|
+
if (!response.ok) {
|
|
3533
|
+
const body = await response.text();
|
|
3534
|
+
throw new Error(`Claude API error ${response.status}: ${body}`);
|
|
3535
|
+
}
|
|
3536
|
+
const data = await response.json();
|
|
3537
|
+
if (data.stop_reason === "refusal") return null;
|
|
3538
|
+
const text = (data.content?.find((b) => b.type === "text")?.text || "").trim();
|
|
3539
|
+
if (!text) return null;
|
|
3540
|
+
return parseBrief(text);
|
|
3541
|
+
}
|
|
3542
|
+
function createAiContentBriefHandler(targetCollections, seoConfig) {
|
|
3543
|
+
return async (req) => {
|
|
3544
|
+
try {
|
|
3545
|
+
if (!req.user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
3546
|
+
const body = await parseJsonBody(req);
|
|
3547
|
+
const keyword = typeof body.keyword === "string" ? body.keyword.trim() : "";
|
|
3548
|
+
if (!keyword) return Response.json({ error: "Missing required field: keyword" }, { status: 400 });
|
|
3549
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
3550
|
+
if (!apiKey) {
|
|
3551
|
+
return Response.json(
|
|
3552
|
+
{ error: "AI not configured. Set ANTHROPIC_API_KEY to generate a content brief.", code: "no_api_key" },
|
|
3553
|
+
{ status: 400 }
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3556
|
+
let pageTitle;
|
|
3557
|
+
let existingContent;
|
|
3558
|
+
const collection = typeof body.collection === "string" ? body.collection : void 0;
|
|
3559
|
+
const id = body.id != null ? String(body.id) : void 0;
|
|
3560
|
+
if (collection && id && (!targetCollections || targetCollections.includes(collection))) {
|
|
3561
|
+
try {
|
|
3562
|
+
const doc = await req.payload.findByID({ collection, id, depth: 1, overrideAccess: true });
|
|
3563
|
+
pageTitle = doc.title || void 0;
|
|
3564
|
+
existingContent = extractDocContent(doc).text || void 0;
|
|
3565
|
+
} catch {
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
const model = process.env.SEO_AI_MODEL || DEFAULT_MODEL3;
|
|
3569
|
+
const language = seoConfig?.locale === "en" ? "en" : "fr";
|
|
3570
|
+
let brief;
|
|
3571
|
+
try {
|
|
3572
|
+
brief = await callClaudeBrief(apiKey, model, language, { keyword, pageTitle, existingContent });
|
|
3573
|
+
} catch (e) {
|
|
3574
|
+
req.payload.logger.error(`[seo] ai-content-brief Claude error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
3575
|
+
return Response.json({ error: "Content brief generation failed." }, { status: 502 });
|
|
3576
|
+
}
|
|
3577
|
+
if (!brief) return Response.json({ error: "The model did not return a brief (possibly declined)." }, { status: 502 });
|
|
3578
|
+
return Response.json({ keyword, brief, model });
|
|
3579
|
+
} catch (error) {
|
|
3580
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
3581
|
+
req.payload.logger.error(`[seo] ai-content-brief error: ${message}`);
|
|
3582
|
+
return Response.json({ error: message }, { status: 500 });
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/endpoints/aiOptimizeBulk.ts
|
|
3588
|
+
var TITLE_MAX = 70;
|
|
3589
|
+
var DESC_MAX = 160;
|
|
3590
|
+
var KEYWORD_MAX2 = 60;
|
|
3591
|
+
function isAdmin5(user) {
|
|
3592
|
+
if (!user) return false;
|
|
3593
|
+
if (user.role === "admin") return true;
|
|
3594
|
+
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
3595
|
+
return false;
|
|
3596
|
+
}
|
|
3597
|
+
function createAiOptimizeBulkHandler(targetCollections, seoConfig, localeMapping) {
|
|
3598
|
+
return async (req) => {
|
|
3599
|
+
try {
|
|
3600
|
+
if (!isAdmin5(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
3601
|
+
const body = await parseJsonBody(req);
|
|
3602
|
+
const rawIds = Array.isArray(body.ids) ? body.ids.map(String) : [];
|
|
3603
|
+
const defaultCollection = typeof body.collection === "string" ? body.collection : void 0;
|
|
3604
|
+
const apply = body.apply === true;
|
|
3605
|
+
const MAX = 100;
|
|
3606
|
+
const limit = Math.min(MAX, Math.max(1, parseInt(String(body.limit ?? 50), 10) || 50));
|
|
3607
|
+
const corrections = Array.isArray(body.corrections) ? body.corrections : null;
|
|
3608
|
+
if (apply && corrections && corrections.length > 0) {
|
|
3609
|
+
let applied2 = 0;
|
|
3610
|
+
let i2 = 0;
|
|
3611
|
+
const rows = [];
|
|
3612
|
+
for (const c of corrections.slice(0, MAX)) {
|
|
3613
|
+
i2++;
|
|
3614
|
+
const collection = typeof c.collection === "string" ? c.collection : defaultCollection;
|
|
3615
|
+
const id = c.id != null ? String(c.id) : void 0;
|
|
3616
|
+
if (!collection || !id || collection.startsWith("global:")) continue;
|
|
3617
|
+
if (targetCollections && !targetCollections.includes(collection)) continue;
|
|
3618
|
+
const metaTitle = typeof c.metaTitle === "string" ? truncateWords(c.metaTitle.trim(), TITLE_MAX) : "";
|
|
3619
|
+
const metaDescription = typeof c.metaDescription === "string" ? truncateWords(c.metaDescription.trim(), DESC_MAX) : "";
|
|
3620
|
+
const focusKeyword = typeof c.focusKeyword === "string" ? c.focusKeyword.trim().slice(0, KEYWORD_MAX2) : "";
|
|
3621
|
+
const patch = {};
|
|
3622
|
+
if (metaTitle || metaDescription) patch.meta = { title: metaTitle, description: metaDescription };
|
|
3623
|
+
if (focusKeyword) patch.focusKeyword = focusKeyword;
|
|
3624
|
+
try {
|
|
3625
|
+
if (Object.keys(patch).length > 0) {
|
|
3626
|
+
await req.payload.update({ collection, id, data: patch, overrideAccess: true });
|
|
3627
|
+
applied2++;
|
|
3628
|
+
rows.push({ collection, id, applied: true });
|
|
3629
|
+
}
|
|
3630
|
+
} catch (e) {
|
|
3631
|
+
rows.push({ collection, id, applied: false, error: e instanceof Error ? e.message : "error" });
|
|
3632
|
+
}
|
|
3633
|
+
if (i2 % 5 === 0) await new Promise((resolve) => setImmediate(resolve));
|
|
3634
|
+
}
|
|
3635
|
+
return Response.json(
|
|
3636
|
+
{ processed: rows.length, applied: applied2, mode: "corrections", results: rows },
|
|
3637
|
+
{ headers: { "Cache-Control": "no-store" } }
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3640
|
+
if (rawIds.length === 0) {
|
|
3641
|
+
return Response.json({ error: 'Provide a non-empty "ids" array' }, { status: 400 });
|
|
3642
|
+
}
|
|
3643
|
+
const targets = [];
|
|
3644
|
+
for (const raw of rawIds) {
|
|
3645
|
+
let collection;
|
|
3646
|
+
let id;
|
|
3647
|
+
if (raw.includes("::")) {
|
|
3648
|
+
const [c, i2] = raw.split("::");
|
|
3649
|
+
collection = c;
|
|
3650
|
+
id = i2;
|
|
3651
|
+
} else {
|
|
3652
|
+
collection = defaultCollection;
|
|
3653
|
+
id = raw;
|
|
3654
|
+
}
|
|
3655
|
+
if (!collection || !id) continue;
|
|
3656
|
+
if (collection.startsWith("global:")) continue;
|
|
3657
|
+
if (targetCollections && !targetCollections.includes(collection)) continue;
|
|
3658
|
+
targets.push({ collection, id });
|
|
3659
|
+
}
|
|
3660
|
+
const capped = targets.length > limit;
|
|
3661
|
+
const slice = targets.slice(0, limit);
|
|
3662
|
+
const { config: mergedConfig } = await loadMergedConfig(req.payload, seoConfig, {
|
|
3663
|
+
reqLocale: req.locale,
|
|
3664
|
+
localeMapping
|
|
3665
|
+
});
|
|
3666
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
3667
|
+
const model = process.env.SEO_AI_MODEL;
|
|
3668
|
+
const results = [];
|
|
3669
|
+
let applied = 0;
|
|
3670
|
+
let i = 0;
|
|
3671
|
+
for (const target of slice) {
|
|
3672
|
+
i++;
|
|
3673
|
+
try {
|
|
3674
|
+
const r = await optimizeDocMeta(req.payload, {
|
|
3675
|
+
collection: target.collection,
|
|
3676
|
+
id: target.id,
|
|
3677
|
+
mergedConfig,
|
|
3678
|
+
apiKey,
|
|
3679
|
+
model
|
|
3680
|
+
});
|
|
3681
|
+
if (!r.ok || !r.current || !r.suggestions) {
|
|
3682
|
+
results.push({
|
|
3683
|
+
collection: target.collection,
|
|
3684
|
+
id: target.id,
|
|
3685
|
+
title: "",
|
|
3686
|
+
before: { metaTitle: "", metaDescription: "", focusKeyword: "" },
|
|
3687
|
+
after: { metaTitle: "", metaDescription: "", focusKeyword: "" },
|
|
3688
|
+
changed: false,
|
|
3689
|
+
applied: false,
|
|
3690
|
+
error: r.error || "optimize_failed"
|
|
3691
|
+
});
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
const before = r.current;
|
|
3695
|
+
const after = {
|
|
3696
|
+
metaTitle: r.suggestions.metaTitle,
|
|
3697
|
+
metaDescription: r.suggestions.metaDescription,
|
|
3698
|
+
focusKeyword: r.suggestions.focusKeyword
|
|
3699
|
+
};
|
|
3700
|
+
const changed = after.metaTitle !== before.metaTitle || after.metaDescription !== before.metaDescription || after.focusKeyword !== before.focusKeyword;
|
|
3701
|
+
let didApply = false;
|
|
3702
|
+
if (apply && changed) {
|
|
3703
|
+
const patch = {};
|
|
3704
|
+
if (after.metaTitle || after.metaDescription) {
|
|
3705
|
+
patch.meta = { title: after.metaTitle, description: after.metaDescription };
|
|
3706
|
+
}
|
|
3707
|
+
if (after.focusKeyword && after.focusKeyword !== before.focusKeyword) {
|
|
3708
|
+
patch.focusKeyword = after.focusKeyword;
|
|
3709
|
+
}
|
|
3710
|
+
if (Object.keys(patch).length > 0) {
|
|
3711
|
+
await req.payload.update({ collection: target.collection, id: target.id, data: patch, overrideAccess: true });
|
|
3712
|
+
didApply = true;
|
|
3713
|
+
applied++;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
results.push({
|
|
3717
|
+
collection: target.collection,
|
|
3718
|
+
id: target.id,
|
|
3719
|
+
title: r.title || "",
|
|
3720
|
+
before,
|
|
3721
|
+
after,
|
|
3722
|
+
changed,
|
|
3723
|
+
applied: didApply,
|
|
3724
|
+
method: r.method
|
|
3725
|
+
});
|
|
3726
|
+
} catch (e) {
|
|
3727
|
+
results.push({
|
|
3728
|
+
collection: target.collection,
|
|
3729
|
+
id: target.id,
|
|
3730
|
+
title: "",
|
|
3731
|
+
before: { metaTitle: "", metaDescription: "", focusKeyword: "" },
|
|
3732
|
+
after: { metaTitle: "", metaDescription: "", focusKeyword: "" },
|
|
3733
|
+
changed: false,
|
|
3734
|
+
applied: false,
|
|
3735
|
+
error: e instanceof Error ? e.message : "error"
|
|
3736
|
+
});
|
|
3737
|
+
}
|
|
3738
|
+
if (i % 3 === 0) await new Promise((resolve) => setImmediate(resolve));
|
|
3739
|
+
}
|
|
3740
|
+
return Response.json(
|
|
3741
|
+
{
|
|
3742
|
+
processed: results.length,
|
|
3743
|
+
changedCount: results.filter((r) => r.changed).length,
|
|
3744
|
+
applied,
|
|
3745
|
+
capped,
|
|
3746
|
+
results
|
|
3747
|
+
},
|
|
3748
|
+
{ headers: { "Cache-Control": "no-store" } }
|
|
3749
|
+
);
|
|
3750
|
+
} catch (error) {
|
|
3751
|
+
const message = error instanceof Error ? error.message : "Internal server error";
|
|
3752
|
+
req.payload.logger.error(`[seo] ai-optimize-bulk error: ${message}`);
|
|
3753
|
+
return Response.json({ error: message }, { status: 500 });
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3444
3758
|
// src/endpoints/cannibalization.ts
|
|
3445
3759
|
function canonicalIntent(keyword) {
|
|
3446
3760
|
return keyword.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter(Boolean).sort().join(" ");
|
|
@@ -4025,7 +4339,7 @@ function getDateThreshold(period) {
|
|
|
4025
4339
|
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
4026
4340
|
}
|
|
4027
4341
|
}
|
|
4028
|
-
function
|
|
4342
|
+
function isAdmin6(user) {
|
|
4029
4343
|
if (!user) return false;
|
|
4030
4344
|
if (user.role === "admin") return true;
|
|
4031
4345
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -4130,7 +4444,7 @@ function createPerformanceHandler() {
|
|
|
4130
4444
|
});
|
|
4131
4445
|
}
|
|
4132
4446
|
if (method === "POST") {
|
|
4133
|
-
if (!
|
|
4447
|
+
if (!isAdmin6(req.user)) {
|
|
4134
4448
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
4135
4449
|
}
|
|
4136
4450
|
const body = await parseJsonBody(req);
|
|
@@ -5238,24 +5552,46 @@ function buildArticleSchema(doc, siteUrl) {
|
|
|
5238
5552
|
}
|
|
5239
5553
|
return schema;
|
|
5240
5554
|
}
|
|
5241
|
-
function
|
|
5555
|
+
function buildLocationNode(loc, doc, siteUrl) {
|
|
5242
5556
|
const meta = doc.meta || {};
|
|
5243
|
-
const
|
|
5244
|
-
"@
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
url: `${siteUrl}/${doc.slug || ""}`
|
|
5557
|
+
const node = {
|
|
5558
|
+
"@type": typeof loc.type === "string" && loc.type || "LocalBusiness",
|
|
5559
|
+
name: loc.name || doc.title || meta.title || "",
|
|
5560
|
+
description: loc.description || meta.description || "",
|
|
5561
|
+
url: loc.url || `${siteUrl}/${doc.slug || ""}`
|
|
5249
5562
|
};
|
|
5250
|
-
if (
|
|
5251
|
-
if (
|
|
5252
|
-
if (
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5563
|
+
if (loc.telephone) node.telephone = loc.telephone;
|
|
5564
|
+
if (loc.email) node.email = loc.email;
|
|
5565
|
+
if (loc.priceRange) node.priceRange = loc.priceRange;
|
|
5566
|
+
const address = loc.address;
|
|
5567
|
+
if (address && typeof address === "object") {
|
|
5568
|
+
node.address = { "@type": "PostalAddress", ...address };
|
|
5569
|
+
} else if (typeof address === "string" && address) {
|
|
5570
|
+
node.address = address;
|
|
5571
|
+
}
|
|
5572
|
+
const geo = loc.geo || {};
|
|
5573
|
+
const lat = geo.latitude ?? loc.latitude ?? loc.lat;
|
|
5574
|
+
const lng = geo.longitude ?? loc.longitude ?? loc.lng;
|
|
5575
|
+
if (lat != null && lng != null) {
|
|
5576
|
+
node.geo = { "@type": "GeoCoordinates", latitude: lat, longitude: lng };
|
|
5577
|
+
}
|
|
5578
|
+
if (Array.isArray(loc.openingHours) && loc.openingHours.length > 0) {
|
|
5579
|
+
node.openingHours = loc.openingHours;
|
|
5580
|
+
} else if (typeof loc.openingHours === "string" && loc.openingHours) {
|
|
5581
|
+
node.openingHours = loc.openingHours;
|
|
5582
|
+
}
|
|
5583
|
+
return node;
|
|
5584
|
+
}
|
|
5585
|
+
function buildLocalBusinessSchema(doc, siteUrl) {
|
|
5586
|
+
const locations = Array.isArray(doc.locations) ? doc.locations.filter((l) => !!l && typeof l === "object") : [];
|
|
5587
|
+
if (locations.length > 1) {
|
|
5588
|
+
return {
|
|
5589
|
+
"@context": "https://schema.org",
|
|
5590
|
+
"@graph": locations.map((loc) => buildLocationNode(loc, doc, siteUrl))
|
|
5256
5591
|
};
|
|
5257
5592
|
}
|
|
5258
|
-
|
|
5593
|
+
const base = locations.length === 1 ? locations[0] : doc;
|
|
5594
|
+
return { "@context": "https://schema.org", ...buildLocationNode(base, doc, siteUrl) };
|
|
5259
5595
|
}
|
|
5260
5596
|
function buildBreadcrumbSchema(doc, siteUrl) {
|
|
5261
5597
|
const slug = doc.slug || "";
|
|
@@ -5822,7 +6158,7 @@ function createAiRewriteHandler(targetCollections) {
|
|
|
5822
6158
|
}
|
|
5823
6159
|
|
|
5824
6160
|
// src/endpoints/robots.ts
|
|
5825
|
-
function
|
|
6161
|
+
function isAdmin7(user) {
|
|
5826
6162
|
if (!user) return false;
|
|
5827
6163
|
if (user.role === "admin") return true;
|
|
5828
6164
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -5871,7 +6207,7 @@ function createRobotsUpdateHandler() {
|
|
|
5871
6207
|
if (!req.user) {
|
|
5872
6208
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5873
6209
|
}
|
|
5874
|
-
if (!
|
|
6210
|
+
if (!isAdmin7(req.user)) {
|
|
5875
6211
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
5876
6212
|
}
|
|
5877
6213
|
const body = await parseJsonBody(req);
|
|
@@ -6006,6 +6342,191 @@ function createSitemapHandler(targetCollections) {
|
|
|
6006
6342
|
};
|
|
6007
6343
|
}
|
|
6008
6344
|
|
|
6345
|
+
// src/endpoints/sitemapExtensions.ts
|
|
6346
|
+
function escapeXml2(str) {
|
|
6347
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6348
|
+
}
|
|
6349
|
+
function resolveSiteUrl3(seoConfig) {
|
|
6350
|
+
return (seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
|
|
6351
|
+
}
|
|
6352
|
+
function docPath(slug) {
|
|
6353
|
+
return slug === "home" || slug === "" ? "" : `/${slug}`;
|
|
6354
|
+
}
|
|
6355
|
+
function xmlResponse(xml, status = 200) {
|
|
6356
|
+
return new Response(xml, {
|
|
6357
|
+
status,
|
|
6358
|
+
headers: { "Content-Type": "application/xml", "Cache-Control": "public, max-age=3600, s-maxage=3600" }
|
|
6359
|
+
});
|
|
6360
|
+
}
|
|
6361
|
+
function mediaUrl(media, siteUrl) {
|
|
6362
|
+
if (typeof media.url === "string" && media.url) {
|
|
6363
|
+
return media.url.startsWith("http") ? media.url : `${siteUrl}${media.url}`;
|
|
6364
|
+
}
|
|
6365
|
+
if (typeof media.filename === "string" && media.filename) {
|
|
6366
|
+
return `${siteUrl}/media/${media.filename}`;
|
|
6367
|
+
}
|
|
6368
|
+
return void 0;
|
|
6369
|
+
}
|
|
6370
|
+
function collectMediaUrls(node, mimePrefix, siteUrl, out, depth = 0) {
|
|
6371
|
+
if (!node || typeof node !== "object" || depth > 8) return;
|
|
6372
|
+
if (Array.isArray(node)) {
|
|
6373
|
+
for (const item of node) collectMediaUrls(item, mimePrefix, siteUrl, out, depth + 1);
|
|
6374
|
+
return;
|
|
6375
|
+
}
|
|
6376
|
+
const obj = node;
|
|
6377
|
+
const mime = typeof obj.mimeType === "string" ? obj.mimeType : "";
|
|
6378
|
+
if (mime.startsWith(mimePrefix)) {
|
|
6379
|
+
const url = mediaUrl(obj, siteUrl);
|
|
6380
|
+
if (url) out.add(url);
|
|
6381
|
+
}
|
|
6382
|
+
for (const key of Object.keys(obj)) {
|
|
6383
|
+
if (key === "sizes" || key === "_status") continue;
|
|
6384
|
+
collectMediaUrls(obj[key], mimePrefix, siteUrl, out, depth + 1);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
async function eachPublishedDoc(payload, collections, depth, onDoc) {
|
|
6388
|
+
const BATCH = Math.min(100, Math.max(1, parseInt(process.env.SEO_SITEMAP_BATCH_SIZE || "50", 10) || 50));
|
|
6389
|
+
const MAX = Math.max(1, parseInt(process.env.SEO_SITEMAP_MAX_DOCS || "5000", 10) || 5e3);
|
|
6390
|
+
let count = 0;
|
|
6391
|
+
for (const collection of collections) {
|
|
6392
|
+
try {
|
|
6393
|
+
let page = 1;
|
|
6394
|
+
let hasMore = true;
|
|
6395
|
+
while (hasMore) {
|
|
6396
|
+
const res = await payload.find({ collection, limit: BATCH, page, depth, overrideAccess: true });
|
|
6397
|
+
for (const doc of res.docs) {
|
|
6398
|
+
if (doc._status === "draft") continue;
|
|
6399
|
+
if (count >= MAX) return;
|
|
6400
|
+
onDoc(doc, collection);
|
|
6401
|
+
count++;
|
|
6402
|
+
}
|
|
6403
|
+
hasMore = res.hasNextPage;
|
|
6404
|
+
page++;
|
|
6405
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
6406
|
+
}
|
|
6407
|
+
} catch {
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
function createNewsSitemapHandler(targetCollections, seoConfig) {
|
|
6412
|
+
return async (req) => {
|
|
6413
|
+
try {
|
|
6414
|
+
const siteUrl = resolveSiteUrl3(seoConfig);
|
|
6415
|
+
const language = seoConfig?.locale === "en" ? "en" : "fr";
|
|
6416
|
+
let publication = seoConfig?.siteName || "";
|
|
6417
|
+
if (!publication && siteUrl) {
|
|
6418
|
+
try {
|
|
6419
|
+
publication = new URL(siteUrl).hostname;
|
|
6420
|
+
} catch {
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
const cutoff = Date.now() - 48 * 36e5;
|
|
6424
|
+
const entries = [];
|
|
6425
|
+
await eachPublishedDoc(req.payload, targetCollections, 0, (doc) => {
|
|
6426
|
+
const dateStr = typeof doc.publishedAt === "string" && doc.publishedAt || typeof doc.date === "string" && doc.date || typeof doc.createdAt === "string" && doc.createdAt || "";
|
|
6427
|
+
if (!dateStr) return;
|
|
6428
|
+
const t = new Date(dateStr).getTime();
|
|
6429
|
+
if (isNaN(t) || t < cutoff) return;
|
|
6430
|
+
const title = doc.title || doc.meta?.title || "";
|
|
6431
|
+
if (!title) return;
|
|
6432
|
+
const loc = `${siteUrl}${docPath(doc.slug || "")}`;
|
|
6433
|
+
entries.push(
|
|
6434
|
+
` <url>
|
|
6435
|
+
<loc>${escapeXml2(loc)}</loc>
|
|
6436
|
+
<news:news>
|
|
6437
|
+
<news:publication>
|
|
6438
|
+
<news:name>${escapeXml2(publication)}</news:name>
|
|
6439
|
+
<news:language>${language}</news:language>
|
|
6440
|
+
</news:publication>
|
|
6441
|
+
<news:publication_date>${new Date(dateStr).toISOString()}</news:publication_date>
|
|
6442
|
+
<news:title>${escapeXml2(title)}</news:title>
|
|
6443
|
+
</news:news>
|
|
6444
|
+
</url>`
|
|
6445
|
+
);
|
|
6446
|
+
});
|
|
6447
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6448
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
|
|
6449
|
+
${entries.join("\n")}
|
|
6450
|
+
</urlset>`;
|
|
6451
|
+
return xmlResponse(xml);
|
|
6452
|
+
} catch (error) {
|
|
6453
|
+
req.payload.logger.error(`[seo] sitemap-news error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
6454
|
+
return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
|
|
6455
|
+
}
|
|
6456
|
+
};
|
|
6457
|
+
}
|
|
6458
|
+
function createImageSitemapHandler(targetCollections, seoConfig) {
|
|
6459
|
+
return async (req) => {
|
|
6460
|
+
try {
|
|
6461
|
+
const siteUrl = resolveSiteUrl3(seoConfig);
|
|
6462
|
+
const entries = [];
|
|
6463
|
+
await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
|
|
6464
|
+
const urls = /* @__PURE__ */ new Set();
|
|
6465
|
+
collectMediaUrls(doc, "image/", siteUrl, urls);
|
|
6466
|
+
if (urls.size === 0) return;
|
|
6467
|
+
const loc = `${siteUrl}${docPath(doc.slug || "")}`;
|
|
6468
|
+
const imgs = Array.from(urls).slice(0, 1e3).map((u) => ` <image:image><image:loc>${escapeXml2(u)}</image:loc></image:image>`).join("\n");
|
|
6469
|
+
entries.push(` <url>
|
|
6470
|
+
<loc>${escapeXml2(loc)}</loc>
|
|
6471
|
+
${imgs}
|
|
6472
|
+
</url>`);
|
|
6473
|
+
});
|
|
6474
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6475
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
|
6476
|
+
${entries.join("\n")}
|
|
6477
|
+
</urlset>`;
|
|
6478
|
+
return xmlResponse(xml);
|
|
6479
|
+
} catch (error) {
|
|
6480
|
+
req.payload.logger.error(`[seo] sitemap-images error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
6481
|
+
return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
|
|
6482
|
+
}
|
|
6483
|
+
};
|
|
6484
|
+
}
|
|
6485
|
+
function createVideoSitemapHandler(targetCollections, seoConfig) {
|
|
6486
|
+
return async (req) => {
|
|
6487
|
+
try {
|
|
6488
|
+
const siteUrl = resolveSiteUrl3(seoConfig);
|
|
6489
|
+
const entries = [];
|
|
6490
|
+
await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
|
|
6491
|
+
const meta = doc.meta || {};
|
|
6492
|
+
const videoUrls = /* @__PURE__ */ new Set();
|
|
6493
|
+
collectMediaUrls(doc, "video/", siteUrl, videoUrls);
|
|
6494
|
+
for (const k of ["videoUrl", "contentUrl", "playerUrl"]) {
|
|
6495
|
+
if (typeof doc[k] === "string" && doc[k]) videoUrls.add(doc[k]);
|
|
6496
|
+
}
|
|
6497
|
+
if (videoUrls.size === 0) return;
|
|
6498
|
+
const title = doc.title || meta.title || "";
|
|
6499
|
+
const description = meta.description || title;
|
|
6500
|
+
const thumbs = /* @__PURE__ */ new Set();
|
|
6501
|
+
collectMediaUrls(meta.image, "image/", siteUrl, thumbs);
|
|
6502
|
+
if (thumbs.size === 0) collectMediaUrls(doc, "image/", siteUrl, thumbs);
|
|
6503
|
+
const thumbnail = Array.from(thumbs)[0] || "";
|
|
6504
|
+
const loc = `${siteUrl}${docPath(doc.slug || "")}`;
|
|
6505
|
+
const videos = Array.from(videoUrls).slice(0, 100).map(
|
|
6506
|
+
(u) => ` <video:video>
|
|
6507
|
+
${thumbnail ? ` <video:thumbnail_loc>${escapeXml2(thumbnail)}</video:thumbnail_loc>
|
|
6508
|
+
` : ""} <video:title>${escapeXml2(title || "Video")}</video:title>
|
|
6509
|
+
<video:description>${escapeXml2(description || title || "Video")}</video:description>
|
|
6510
|
+
<video:content_loc>${escapeXml2(u)}</video:content_loc>
|
|
6511
|
+
</video:video>`
|
|
6512
|
+
).join("\n");
|
|
6513
|
+
entries.push(` <url>
|
|
6514
|
+
<loc>${escapeXml2(loc)}</loc>
|
|
6515
|
+
${videos}
|
|
6516
|
+
</url>`);
|
|
6517
|
+
});
|
|
6518
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6519
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
|
6520
|
+
${entries.join("\n")}
|
|
6521
|
+
</urlset>`;
|
|
6522
|
+
return xmlResponse(xml);
|
|
6523
|
+
} catch (error) {
|
|
6524
|
+
req.payload.logger.error(`[seo] sitemap-video error: ${error instanceof Error ? error.message : "unknown"}`);
|
|
6525
|
+
return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
|
|
6526
|
+
}
|
|
6527
|
+
};
|
|
6528
|
+
}
|
|
6529
|
+
|
|
6009
6530
|
// src/collections/SeoScoreHistory.ts
|
|
6010
6531
|
function createSeoScoreHistoryCollection() {
|
|
6011
6532
|
return {
|
|
@@ -6916,7 +7437,7 @@ function getClientIp(req) {
|
|
|
6916
7437
|
|
|
6917
7438
|
// src/endpoints/seoLogs.ts
|
|
6918
7439
|
var VALID_LOG_TYPES = ["404", "redirect", "error"];
|
|
6919
|
-
function
|
|
7440
|
+
function isAdmin8(user) {
|
|
6920
7441
|
if (!user) return false;
|
|
6921
7442
|
if (user.role === "admin") return true;
|
|
6922
7443
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -7013,7 +7534,7 @@ function createSeoLogsHandler(seoLogsSecret) {
|
|
|
7013
7534
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
7014
7535
|
}
|
|
7015
7536
|
if (method === "DELETE") {
|
|
7016
|
-
if (!
|
|
7537
|
+
if (!isAdmin8(req.user)) {
|
|
7017
7538
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
7018
7539
|
}
|
|
7019
7540
|
try {
|
|
@@ -7190,6 +7711,7 @@ async function doWarmUp(payload, collections = ["pages", "posts"], globals = [])
|
|
|
7190
7711
|
}
|
|
7191
7712
|
}
|
|
7192
7713
|
function startCacheWarmUp(payload, _basePath, globals = [], collections = ["pages", "posts"]) {
|
|
7714
|
+
stopCacheWarmUp();
|
|
7193
7715
|
setTimeout(() => {
|
|
7194
7716
|
void doWarmUp(payload, collections, globals);
|
|
7195
7717
|
}, STARTUP_DELAY);
|
|
@@ -7231,6 +7753,7 @@ async function doSnapshot(payload, basePath, seoConfig) {
|
|
|
7231
7753
|
}
|
|
7232
7754
|
}
|
|
7233
7755
|
function startRankTracker(payload, basePath, seoConfig) {
|
|
7756
|
+
stopRankTracker();
|
|
7234
7757
|
setTimeout(() => {
|
|
7235
7758
|
void doSnapshot(payload, basePath, seoConfig);
|
|
7236
7759
|
}, STARTUP_DELAY2);
|
|
@@ -7253,7 +7776,7 @@ function stopRankTracker() {
|
|
|
7253
7776
|
}
|
|
7254
7777
|
|
|
7255
7778
|
// src/endpoints/alerts.ts
|
|
7256
|
-
function
|
|
7779
|
+
function isAdmin9(user) {
|
|
7257
7780
|
if (!user) return false;
|
|
7258
7781
|
if (user.role === "admin") return true;
|
|
7259
7782
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -7419,7 +7942,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
|
|
|
7419
7942
|
function createAlertsDigestHandler() {
|
|
7420
7943
|
return async (req) => {
|
|
7421
7944
|
try {
|
|
7422
|
-
if (!
|
|
7945
|
+
if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
7423
7946
|
const cfg = getAlertConfig();
|
|
7424
7947
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
7425
7948
|
return Response.json(
|
|
@@ -7445,7 +7968,7 @@ function createAlertsDigestHandler() {
|
|
|
7445
7968
|
function createAlertsRunHandler(siteUrl) {
|
|
7446
7969
|
return async (req) => {
|
|
7447
7970
|
try {
|
|
7448
|
-
if (!
|
|
7971
|
+
if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
7449
7972
|
const cfg = getAlertConfig();
|
|
7450
7973
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
7451
7974
|
const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
|
|
@@ -7480,6 +8003,7 @@ async function runDigest(payload, siteUrl) {
|
|
|
7480
8003
|
}
|
|
7481
8004
|
}
|
|
7482
8005
|
function startAlertsScheduler(payload, siteUrl) {
|
|
8006
|
+
stopAlertsScheduler();
|
|
7483
8007
|
const intervalHours = Math.max(1, parseInt(process.env.SEO_ALERT_INTERVAL_HOURS || "24", 10) || 24);
|
|
7484
8008
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
7485
8009
|
setTimeout(() => {
|
|
@@ -8494,6 +9018,16 @@ var fr = {
|
|
|
8494
9018
|
pagesAnalyzed: "pages analys\xE9es",
|
|
8495
9019
|
markCornerstone: "Marquer pilier",
|
|
8496
9020
|
unmarkCornerstone: "D\xE9marquer pilier",
|
|
9021
|
+
bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
|
|
9022
|
+
bulkOptimizing: "Analyse\u2026",
|
|
9023
|
+
bulkConfirm: "Confirmer ?",
|
|
9024
|
+
bulkPreviewTitle: "Corrections m\xE9ta propos\xE9es",
|
|
9025
|
+
bulkApply: "Appliquer",
|
|
9026
|
+
bulkApplying: "Application\u2026",
|
|
9027
|
+
bulkCancel: "Annuler",
|
|
9028
|
+
bulkExport: "Exporter CSV",
|
|
9029
|
+
bulkNoChanges: "Aucune correction n\xE9cessaire sur la s\xE9lection.",
|
|
9030
|
+
bulkCappedNote: "limite atteinte (affine la s\xE9lection)",
|
|
8497
9031
|
searchPlaceholder: "Rechercher (titre, slug, keyword)...",
|
|
8498
9032
|
allCollections: "Toutes les collections",
|
|
8499
9033
|
allScores: "Tous les scores",
|
|
@@ -9086,6 +9620,16 @@ var en = {
|
|
|
9086
9620
|
pagesAnalyzed: "pages analyzed",
|
|
9087
9621
|
markCornerstone: "Mark as cornerstone",
|
|
9088
9622
|
unmarkCornerstone: "Unmark cornerstone",
|
|
9623
|
+
bulkOptimizeMeta: "Optimize meta (AI)",
|
|
9624
|
+
bulkOptimizing: "Analyzing\u2026",
|
|
9625
|
+
bulkConfirm: "Confirm?",
|
|
9626
|
+
bulkPreviewTitle: "Proposed meta corrections",
|
|
9627
|
+
bulkApply: "Apply",
|
|
9628
|
+
bulkApplying: "Applying\u2026",
|
|
9629
|
+
bulkCancel: "Cancel",
|
|
9630
|
+
bulkExport: "Export CSV",
|
|
9631
|
+
bulkNoChanges: "No corrections needed on the selection.",
|
|
9632
|
+
bulkCappedNote: "limit reached (narrow the selection)",
|
|
9089
9633
|
searchPlaceholder: "Search (title, slug, keyword)...",
|
|
9090
9634
|
allCollections: "All collections",
|
|
9091
9635
|
allScores: "All scores",
|
|
@@ -9937,7 +10481,9 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
9937
10481
|
{ path: `${basePath}/ai-rewrite`, method: "post", handler: createAiRewriteHandler(targetCollections) },
|
|
9938
10482
|
{ path: `${basePath}/ai-optimize`, method: "post", handler: createAiOptimizeHandler(targetCollections, seoConfig) },
|
|
9939
10483
|
{ path: `${basePath}/alt-text-audit`, method: "get", handler: createAltTextAuditHandler(uploadsCollection) },
|
|
9940
|
-
{ path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) }
|
|
10484
|
+
{ path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) },
|
|
10485
|
+
{ path: `${basePath}/ai-content-brief`, method: "post", handler: withRateLimit(createAiContentBriefHandler(targetCollections, seoConfig)) },
|
|
10486
|
+
{ path: `${basePath}/ai-optimize-bulk`, method: "post", handler: withRateLimit(createAiOptimizeBulkHandler(targetCollections, seoConfig)) }
|
|
9941
10487
|
);
|
|
9942
10488
|
}
|
|
9943
10489
|
if (features.cannibalization) {
|
|
@@ -10029,6 +10575,21 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10029
10575
|
path: `${basePath}/sitemap.xml`,
|
|
10030
10576
|
method: "get",
|
|
10031
10577
|
handler: createSitemapHandler(targetCollections)
|
|
10578
|
+
},
|
|
10579
|
+
{
|
|
10580
|
+
path: `${basePath}/sitemap-news.xml`,
|
|
10581
|
+
method: "get",
|
|
10582
|
+
handler: createNewsSitemapHandler(targetCollections, seoConfig)
|
|
10583
|
+
},
|
|
10584
|
+
{
|
|
10585
|
+
path: `${basePath}/sitemap-images.xml`,
|
|
10586
|
+
method: "get",
|
|
10587
|
+
handler: createImageSitemapHandler(targetCollections, seoConfig)
|
|
10588
|
+
},
|
|
10589
|
+
{
|
|
10590
|
+
path: `${basePath}/sitemap-video.xml`,
|
|
10591
|
+
method: "get",
|
|
10592
|
+
handler: createVideoSitemapHandler(targetCollections, seoConfig)
|
|
10032
10593
|
}
|
|
10033
10594
|
);
|
|
10034
10595
|
config.endpoints = [
|
|
@@ -10133,7 +10694,7 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10133
10694
|
};
|
|
10134
10695
|
|
|
10135
10696
|
// src/helpers/buildMetadata.ts
|
|
10136
|
-
function
|
|
10697
|
+
function resolveSiteUrl4(explicit) {
|
|
10137
10698
|
return (explicit || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
|
|
10138
10699
|
}
|
|
10139
10700
|
function parseRobots(doc, meta) {
|
|
@@ -10167,7 +10728,7 @@ function absoluteUrl(value, siteUrl) {
|
|
|
10167
10728
|
return `${siteUrl}${value.startsWith("/") ? "" : "/"}${value}`;
|
|
10168
10729
|
}
|
|
10169
10730
|
function buildSeoMetadata(doc, options = {}) {
|
|
10170
|
-
const siteUrl =
|
|
10731
|
+
const siteUrl = resolveSiteUrl4(options.siteUrl);
|
|
10171
10732
|
const meta = doc.meta || {};
|
|
10172
10733
|
const rawTitle = meta.title || doc.title || "";
|
|
10173
10734
|
const title = options.titleTemplate && rawTitle ? options.titleTemplate.replace("%s", rawTitle) : rawTitle;
|