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