@consilioweb/payload-seo-analyzer 1.11.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 +1 -0
- package/dist/client.cjs +180 -43
- package/dist/client.js +180 -43
- package/dist/index.cjs +290 -82
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +290 -82
- 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";
|
|
@@ -3565,6 +3584,177 @@ function createAiContentBriefHandler(targetCollections, seoConfig) {
|
|
|
3565
3584
|
};
|
|
3566
3585
|
}
|
|
3567
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
|
+
|
|
3568
3758
|
// src/endpoints/cannibalization.ts
|
|
3569
3759
|
function canonicalIntent(keyword) {
|
|
3570
3760
|
return keyword.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter(Boolean).sort().join(" ");
|
|
@@ -4149,7 +4339,7 @@ function getDateThreshold(period) {
|
|
|
4149
4339
|
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
|
|
4150
4340
|
}
|
|
4151
4341
|
}
|
|
4152
|
-
function
|
|
4342
|
+
function isAdmin6(user) {
|
|
4153
4343
|
if (!user) return false;
|
|
4154
4344
|
if (user.role === "admin") return true;
|
|
4155
4345
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -4254,7 +4444,7 @@ function createPerformanceHandler() {
|
|
|
4254
4444
|
});
|
|
4255
4445
|
}
|
|
4256
4446
|
if (method === "POST") {
|
|
4257
|
-
if (!
|
|
4447
|
+
if (!isAdmin6(req.user)) {
|
|
4258
4448
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
4259
4449
|
}
|
|
4260
4450
|
const body = await parseJsonBody(req);
|
|
@@ -5968,7 +6158,7 @@ function createAiRewriteHandler(targetCollections) {
|
|
|
5968
6158
|
}
|
|
5969
6159
|
|
|
5970
6160
|
// src/endpoints/robots.ts
|
|
5971
|
-
function
|
|
6161
|
+
function isAdmin7(user) {
|
|
5972
6162
|
if (!user) return false;
|
|
5973
6163
|
if (user.role === "admin") return true;
|
|
5974
6164
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -6017,7 +6207,7 @@ function createRobotsUpdateHandler() {
|
|
|
6017
6207
|
if (!req.user) {
|
|
6018
6208
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
6019
6209
|
}
|
|
6020
|
-
if (!
|
|
6210
|
+
if (!isAdmin7(req.user)) {
|
|
6021
6211
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
6022
6212
|
}
|
|
6023
6213
|
const body = await parseJsonBody(req);
|
|
@@ -7247,7 +7437,7 @@ function getClientIp(req) {
|
|
|
7247
7437
|
|
|
7248
7438
|
// src/endpoints/seoLogs.ts
|
|
7249
7439
|
var VALID_LOG_TYPES = ["404", "redirect", "error"];
|
|
7250
|
-
function
|
|
7440
|
+
function isAdmin8(user) {
|
|
7251
7441
|
if (!user) return false;
|
|
7252
7442
|
if (user.role === "admin") return true;
|
|
7253
7443
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -7344,7 +7534,7 @@ function createSeoLogsHandler(seoLogsSecret) {
|
|
|
7344
7534
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
7345
7535
|
}
|
|
7346
7536
|
if (method === "DELETE") {
|
|
7347
|
-
if (!
|
|
7537
|
+
if (!isAdmin8(req.user)) {
|
|
7348
7538
|
return Response.json({ error: "Admin access required" }, { status: 403 });
|
|
7349
7539
|
}
|
|
7350
7540
|
try {
|
|
@@ -7521,6 +7711,7 @@ async function doWarmUp(payload, collections = ["pages", "posts"], globals = [])
|
|
|
7521
7711
|
}
|
|
7522
7712
|
}
|
|
7523
7713
|
function startCacheWarmUp(payload, _basePath, globals = [], collections = ["pages", "posts"]) {
|
|
7714
|
+
stopCacheWarmUp();
|
|
7524
7715
|
setTimeout(() => {
|
|
7525
7716
|
void doWarmUp(payload, collections, globals);
|
|
7526
7717
|
}, STARTUP_DELAY);
|
|
@@ -7562,6 +7753,7 @@ async function doSnapshot(payload, basePath, seoConfig) {
|
|
|
7562
7753
|
}
|
|
7563
7754
|
}
|
|
7564
7755
|
function startRankTracker(payload, basePath, seoConfig) {
|
|
7756
|
+
stopRankTracker();
|
|
7565
7757
|
setTimeout(() => {
|
|
7566
7758
|
void doSnapshot(payload, basePath, seoConfig);
|
|
7567
7759
|
}, STARTUP_DELAY2);
|
|
@@ -7584,7 +7776,7 @@ function stopRankTracker() {
|
|
|
7584
7776
|
}
|
|
7585
7777
|
|
|
7586
7778
|
// src/endpoints/alerts.ts
|
|
7587
|
-
function
|
|
7779
|
+
function isAdmin9(user) {
|
|
7588
7780
|
if (!user) return false;
|
|
7589
7781
|
if (user.role === "admin") return true;
|
|
7590
7782
|
if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
|
|
@@ -7750,7 +7942,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
|
|
|
7750
7942
|
function createAlertsDigestHandler() {
|
|
7751
7943
|
return async (req) => {
|
|
7752
7944
|
try {
|
|
7753
|
-
if (!
|
|
7945
|
+
if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
7754
7946
|
const cfg = getAlertConfig();
|
|
7755
7947
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
7756
7948
|
return Response.json(
|
|
@@ -7776,7 +7968,7 @@ function createAlertsDigestHandler() {
|
|
|
7776
7968
|
function createAlertsRunHandler(siteUrl) {
|
|
7777
7969
|
return async (req) => {
|
|
7778
7970
|
try {
|
|
7779
|
-
if (!
|
|
7971
|
+
if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
7780
7972
|
const cfg = getAlertConfig();
|
|
7781
7973
|
const digest = await buildAlertDigest(req.payload, cfg);
|
|
7782
7974
|
const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
|
|
@@ -7811,6 +8003,7 @@ async function runDigest(payload, siteUrl) {
|
|
|
7811
8003
|
}
|
|
7812
8004
|
}
|
|
7813
8005
|
function startAlertsScheduler(payload, siteUrl) {
|
|
8006
|
+
stopAlertsScheduler();
|
|
7814
8007
|
const intervalHours = Math.max(1, parseInt(process.env.SEO_ALERT_INTERVAL_HOURS || "24", 10) || 24);
|
|
7815
8008
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
7816
8009
|
setTimeout(() => {
|
|
@@ -8826,8 +9019,15 @@ var fr = {
|
|
|
8826
9019
|
markCornerstone: "Marquer pilier",
|
|
8827
9020
|
unmarkCornerstone: "D\xE9marquer pilier",
|
|
8828
9021
|
bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
|
|
8829
|
-
bulkOptimizing: "
|
|
9022
|
+
bulkOptimizing: "Analyse\u2026",
|
|
8830
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)",
|
|
8831
9031
|
searchPlaceholder: "Rechercher (titre, slug, keyword)...",
|
|
8832
9032
|
allCollections: "Toutes les collections",
|
|
8833
9033
|
allScores: "Tous les scores",
|
|
@@ -9421,8 +9621,15 @@ var en = {
|
|
|
9421
9621
|
markCornerstone: "Mark as cornerstone",
|
|
9422
9622
|
unmarkCornerstone: "Unmark cornerstone",
|
|
9423
9623
|
bulkOptimizeMeta: "Optimize meta (AI)",
|
|
9424
|
-
bulkOptimizing: "
|
|
9624
|
+
bulkOptimizing: "Analyzing\u2026",
|
|
9425
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)",
|
|
9426
9633
|
searchPlaceholder: "Search (title, slug, keyword)...",
|
|
9427
9634
|
allCollections: "All collections",
|
|
9428
9635
|
allScores: "All scores",
|
|
@@ -10275,7 +10482,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
|
|
|
10275
10482
|
{ path: `${basePath}/ai-optimize`, method: "post", handler: createAiOptimizeHandler(targetCollections, seoConfig) },
|
|
10276
10483
|
{ path: `${basePath}/alt-text-audit`, method: "get", handler: createAltTextAuditHandler(uploadsCollection) },
|
|
10277
10484
|
{ path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) },
|
|
10278
|
-
{ path: `${basePath}/ai-content-brief`, method: "post", handler: withRateLimit(createAiContentBriefHandler(targetCollections, seoConfig)) }
|
|
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)) }
|
|
10279
10487
|
);
|
|
10280
10488
|
}
|
|
10281
10489
|
if (features.cannibalization) {
|
package/package.json
CHANGED