@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/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 seoInput = buildSeoInputFromDoc(doc, collection);
3054
- const analysis = analyzeSeo(seoInput, mergedConfig);
3055
- const metaGroups = /* @__PURE__ */ new Set(["title", "meta-description", "content", "url", "headings"]);
3056
- 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);
3057
- const pageTitle = doc.title || "";
3058
- const slug = doc.slug || "";
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 (apiKey) {
3074
- try {
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: analysis.score,
3107
- current: {
3108
- metaTitle: currentMetaTitle,
3109
- metaDescription: currentMetaDescription,
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 isAdmin5(user) {
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 (!isAdmin5(req.user)) {
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 isAdmin6(user) {
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 (!isAdmin6(req.user)) {
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 isAdmin7(user) {
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 (!isAdmin7(req.user)) {
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 isAdmin8(user) {
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 (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
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 (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
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: "Optimisation\u2026",
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: "Optimizing\u2026",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-seo-analyzer",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Payload CMS SEO plugin — 50+ checks, dashboard, Lexical JSON support, Flesch FR/EN readability, i18n",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",