@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.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 seoInput = buildSeoInputFromDoc(doc, collection);
3056
- const analysis = analyzeSeo(seoInput, mergedConfig);
3057
- const metaGroups = /* @__PURE__ */ new Set(["title", "meta-description", "content", "url", "headings"]);
3058
- 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);
3059
- const pageTitle = doc.title || "";
3060
- const slug = doc.slug || "";
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 (apiKey) {
3076
- try {
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: analysis.score,
3109
- current: {
3110
- metaTitle: currentMetaTitle,
3111
- metaDescription: currentMetaDescription,
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";
@@ -3567,6 +3586,177 @@ function createAiContentBriefHandler(targetCollections, seoConfig) {
3567
3586
  };
3568
3587
  }
3569
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
+
3570
3760
  // src/endpoints/cannibalization.ts
3571
3761
  function canonicalIntent(keyword) {
3572
3762
  return keyword.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter(Boolean).sort().join(" ");
@@ -4151,7 +4341,7 @@ function getDateThreshold(period) {
4151
4341
  return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
4152
4342
  }
4153
4343
  }
4154
- function isAdmin5(user) {
4344
+ function isAdmin6(user) {
4155
4345
  if (!user) return false;
4156
4346
  if (user.role === "admin") return true;
4157
4347
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -4256,7 +4446,7 @@ function createPerformanceHandler() {
4256
4446
  });
4257
4447
  }
4258
4448
  if (method === "POST") {
4259
- if (!isAdmin5(req.user)) {
4449
+ if (!isAdmin6(req.user)) {
4260
4450
  return Response.json({ error: "Admin access required" }, { status: 403 });
4261
4451
  }
4262
4452
  const body = await parseJsonBody(req);
@@ -5970,7 +6160,7 @@ function createAiRewriteHandler(targetCollections) {
5970
6160
  }
5971
6161
 
5972
6162
  // src/endpoints/robots.ts
5973
- function isAdmin6(user) {
6163
+ function isAdmin7(user) {
5974
6164
  if (!user) return false;
5975
6165
  if (user.role === "admin") return true;
5976
6166
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -6019,7 +6209,7 @@ function createRobotsUpdateHandler() {
6019
6209
  if (!req.user) {
6020
6210
  return Response.json({ error: "Unauthorized" }, { status: 401 });
6021
6211
  }
6022
- if (!isAdmin6(req.user)) {
6212
+ if (!isAdmin7(req.user)) {
6023
6213
  return Response.json({ error: "Admin access required" }, { status: 403 });
6024
6214
  }
6025
6215
  const body = await parseJsonBody(req);
@@ -7249,7 +7439,7 @@ function getClientIp(req) {
7249
7439
 
7250
7440
  // src/endpoints/seoLogs.ts
7251
7441
  var VALID_LOG_TYPES = ["404", "redirect", "error"];
7252
- function isAdmin7(user) {
7442
+ function isAdmin8(user) {
7253
7443
  if (!user) return false;
7254
7444
  if (user.role === "admin") return true;
7255
7445
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7346,7 +7536,7 @@ function createSeoLogsHandler(seoLogsSecret) {
7346
7536
  return Response.json({ error: "Unauthorized" }, { status: 401 });
7347
7537
  }
7348
7538
  if (method === "DELETE") {
7349
- if (!isAdmin7(req.user)) {
7539
+ if (!isAdmin8(req.user)) {
7350
7540
  return Response.json({ error: "Admin access required" }, { status: 403 });
7351
7541
  }
7352
7542
  try {
@@ -7523,6 +7713,7 @@ async function doWarmUp(payload, collections = ["pages", "posts"], globals = [])
7523
7713
  }
7524
7714
  }
7525
7715
  function startCacheWarmUp(payload, _basePath, globals = [], collections = ["pages", "posts"]) {
7716
+ stopCacheWarmUp();
7526
7717
  setTimeout(() => {
7527
7718
  void doWarmUp(payload, collections, globals);
7528
7719
  }, STARTUP_DELAY);
@@ -7564,6 +7755,7 @@ async function doSnapshot(payload, basePath, seoConfig) {
7564
7755
  }
7565
7756
  }
7566
7757
  function startRankTracker(payload, basePath, seoConfig) {
7758
+ stopRankTracker();
7567
7759
  setTimeout(() => {
7568
7760
  void doSnapshot(payload, basePath, seoConfig);
7569
7761
  }, STARTUP_DELAY2);
@@ -7586,7 +7778,7 @@ function stopRankTracker() {
7586
7778
  }
7587
7779
 
7588
7780
  // src/endpoints/alerts.ts
7589
- function isAdmin8(user) {
7781
+ function isAdmin9(user) {
7590
7782
  if (!user) return false;
7591
7783
  if (user.role === "admin") return true;
7592
7784
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7752,7 +7944,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
7752
7944
  function createAlertsDigestHandler() {
7753
7945
  return async (req) => {
7754
7946
  try {
7755
- if (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7947
+ if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7756
7948
  const cfg = getAlertConfig();
7757
7949
  const digest = await buildAlertDigest(req.payload, cfg);
7758
7950
  return Response.json(
@@ -7778,7 +7970,7 @@ function createAlertsDigestHandler() {
7778
7970
  function createAlertsRunHandler(siteUrl) {
7779
7971
  return async (req) => {
7780
7972
  try {
7781
- if (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7973
+ if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7782
7974
  const cfg = getAlertConfig();
7783
7975
  const digest = await buildAlertDigest(req.payload, cfg);
7784
7976
  const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
@@ -7813,6 +8005,7 @@ async function runDigest(payload, siteUrl) {
7813
8005
  }
7814
8006
  }
7815
8007
  function startAlertsScheduler(payload, siteUrl) {
8008
+ stopAlertsScheduler();
7816
8009
  const intervalHours = Math.max(1, parseInt(process.env.SEO_ALERT_INTERVAL_HOURS || "24", 10) || 24);
7817
8010
  const intervalMs = intervalHours * 60 * 60 * 1e3;
7818
8011
  setTimeout(() => {
@@ -8828,8 +9021,15 @@ var fr = {
8828
9021
  markCornerstone: "Marquer pilier",
8829
9022
  unmarkCornerstone: "D\xE9marquer pilier",
8830
9023
  bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
8831
- bulkOptimizing: "Optimisation\u2026",
9024
+ bulkOptimizing: "Analyse\u2026",
8832
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)",
8833
9033
  searchPlaceholder: "Rechercher (titre, slug, keyword)...",
8834
9034
  allCollections: "Toutes les collections",
8835
9035
  allScores: "Tous les scores",
@@ -9423,8 +9623,15 @@ var en = {
9423
9623
  markCornerstone: "Mark as cornerstone",
9424
9624
  unmarkCornerstone: "Unmark cornerstone",
9425
9625
  bulkOptimizeMeta: "Optimize meta (AI)",
9426
- bulkOptimizing: "Optimizing\u2026",
9626
+ bulkOptimizing: "Analyzing\u2026",
9427
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)",
9428
9635
  searchPlaceholder: "Search (title, slug, keyword)...",
9429
9636
  allCollections: "All collections",
9430
9637
  allScores: "All scores",
@@ -10277,7 +10484,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10277
10484
  { path: `${basePath}/ai-optimize`, method: "post", handler: createAiOptimizeHandler(targetCollections, seoConfig) },
10278
10485
  { path: `${basePath}/alt-text-audit`, method: "get", handler: createAltTextAuditHandler(uploadsCollection) },
10279
10486
  { path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) },
10280
- { path: `${basePath}/ai-content-brief`, method: "post", handler: withRateLimit(createAiContentBriefHandler(targetCollections, seoConfig)) }
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)) }
10281
10489
  );
10282
10490
  }
10283
10491
  if (features.cannibalization) {
package/dist/index.d.cts CHANGED
@@ -470,6 +470,13 @@ interface DashboardTranslations {
470
470
  bulkOptimizeMeta: string;
471
471
  bulkOptimizing: string;
472
472
  bulkConfirm: string;
473
+ bulkPreviewTitle: string;
474
+ bulkApply: string;
475
+ bulkApplying: string;
476
+ bulkCancel: string;
477
+ bulkExport: string;
478
+ bulkNoChanges: string;
479
+ bulkCappedNote: string;
473
480
  searchPlaceholder: string;
474
481
  allCollections: string;
475
482
  allScores: string;
package/dist/index.d.ts CHANGED
@@ -470,6 +470,13 @@ interface DashboardTranslations {
470
470
  bulkOptimizeMeta: string;
471
471
  bulkOptimizing: string;
472
472
  bulkConfirm: string;
473
+ bulkPreviewTitle: string;
474
+ bulkApply: string;
475
+ bulkApplying: string;
476
+ bulkCancel: string;
477
+ bulkExport: string;
478
+ bulkNoChanges: string;
479
+ bulkCappedNote: string;
473
480
  searchPlaceholder: string;
474
481
  allCollections: string;
475
482
  allScores: string;