@consilioweb/payload-seo-analyzer 1.13.0 → 1.15.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
@@ -1683,9 +1683,9 @@ function analyzeDoc(doc, collection, seoConfig) {
1683
1683
  };
1684
1684
  }
1685
1685
  var CACHE_KEY = "audit";
1686
- var auditBuildInFlight = false;
1687
- async function buildAuditCache(payload, collections, globals, seoConfig) {
1688
- const { config: mergedConfig, ignoredSlugs } = await loadMergedConfig(payload, seoConfig);
1686
+ var auditBuildsInFlight = /* @__PURE__ */ new Set();
1687
+ async function buildAuditCache(payload, collections, globals, seoConfig, reqLocale) {
1688
+ const { config: mergedConfig, ignoredSlugs } = await loadMergedConfig(payload, seoConfig, { reqLocale });
1689
1689
  const BATCH_SIZE = Math.min(100, Math.max(1, parseInt(process.env.SEO_AUDIT_BATCH_SIZE || "15", 10) || 15));
1690
1690
  const MAX_DOCS2 = Math.max(1, parseInt(process.env.SEO_AUDIT_MAX_DOCS || "1500", 10) || 1500);
1691
1691
  const allResults = [];
@@ -1793,15 +1793,15 @@ async function buildAuditCache(payload, collections, globals, seoConfig) {
1793
1793
  };
1794
1794
  return { enrichedResults, stats, capped };
1795
1795
  }
1796
- function ensureAuditBuild(payload, collections, globals, seoConfig) {
1797
- if (auditBuildInFlight) return;
1798
- auditBuildInFlight = true;
1799
- void buildAuditCache(payload, collections, globals, seoConfig).then((result) => {
1800
- seoCache.set(CACHE_KEY, result);
1796
+ function ensureAuditBuild(payload, collections, globals, seoConfig, cacheKey, reqLocale) {
1797
+ if (auditBuildsInFlight.has(cacheKey)) return;
1798
+ auditBuildsInFlight.add(cacheKey);
1799
+ void buildAuditCache(payload, collections, globals, seoConfig, reqLocale).then((result) => {
1800
+ seoCache.set(cacheKey, result);
1801
1801
  }).catch((e) => {
1802
1802
  payload.logger.error(`[seo] audit build failed: ${e instanceof Error ? e.message : "unknown"}`);
1803
1803
  }).finally(() => {
1804
- auditBuildInFlight = false;
1804
+ auditBuildsInFlight.delete(cacheKey);
1805
1805
  });
1806
1806
  }
1807
1807
  function createAuditHandler(collections, seoConfig, globals = []) {
@@ -1814,12 +1814,14 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1814
1814
  const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
1815
1815
  const limit = Math.min(500, Math.max(1, parseInt(url.searchParams.get("limit") || "300", 10)));
1816
1816
  const noCache = url.searchParams.get("nocache") === "1";
1817
- if (noCache && !auditBuildInFlight) {
1818
- seoCache.invalidateKey(CACHE_KEY);
1817
+ const reqLocale = typeof req.locale === "string" && req.locale ? req.locale : void 0;
1818
+ const cacheKey = reqLocale ? `${CACHE_KEY}:${reqLocale}` : CACHE_KEY;
1819
+ if (noCache && !auditBuildsInFlight.has(cacheKey)) {
1820
+ seoCache.invalidateKey(cacheKey);
1819
1821
  }
1820
- const cached = seoCache.get(CACHE_KEY);
1822
+ const cached = seoCache.get(cacheKey);
1821
1823
  if (!cached) {
1822
- ensureAuditBuild(req.payload, collections, globals, seoConfig);
1824
+ ensureAuditBuild(req.payload, collections, globals, seoConfig, cacheKey, reqLocale);
1823
1825
  return Response.json(
1824
1826
  { building: true, results: [], stats: null },
1825
1827
  { status: 202, headers: { "Cache-Control": "no-store" } }
@@ -7523,6 +7525,77 @@ function createCtrOpportunitiesHandler(basePath, targetCollections, seoConfig) {
7523
7525
  };
7524
7526
  }
7525
7527
 
7528
+ // src/endpoints/health.ts
7529
+ function isAdmin8(user) {
7530
+ if (!user) return false;
7531
+ if (user.role === "admin") return true;
7532
+ if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
7533
+ return false;
7534
+ }
7535
+ function createSeoHealthHandler(basePath, seoConfig) {
7536
+ return async (req) => {
7537
+ try {
7538
+ if (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7539
+ const config = {
7540
+ aiKey: !!process.env.ANTHROPIC_API_KEY,
7541
+ aiModel: process.env.SEO_AI_MODEL || "claude-opus-4-8",
7542
+ pageSpeedKey: !!(process.env.PAGESPEED_API_KEY || process.env.GOOGLE_PAGESPEED_API_KEY),
7543
+ gscConfigured: !!getGscOAuthConfig(basePath, seoConfig),
7544
+ gscEncryptionKey: !!process.env.SEO_GSC_ENCRYPTION_KEY,
7545
+ alertWebhook: !!process.env.SEO_ALERT_WEBHOOK_URL,
7546
+ alertEmail: !!process.env.SEO_ALERT_EMAIL,
7547
+ siteUrl: seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || null
7548
+ };
7549
+ const cacheStats = seoCache.stats();
7550
+ const auditCached = cacheStats.keys.some((k) => k === "audit" || k.startsWith("audit:"));
7551
+ let gscConnected = false;
7552
+ let gscEmail = null;
7553
+ let lastRankSnapshot = null;
7554
+ try {
7555
+ const authDoc = await getOrCreateGscAuthDoc(req.payload);
7556
+ gscConnected = !!authDoc.refreshTokenEnc;
7557
+ gscEmail = authDoc.connectedEmail || null;
7558
+ } catch {
7559
+ }
7560
+ try {
7561
+ const latest = await req.payload.find({
7562
+ collection: "seo-rank-history",
7563
+ sort: "-snapshotDate",
7564
+ limit: 1,
7565
+ depth: 0,
7566
+ overrideAccess: true
7567
+ });
7568
+ lastRankSnapshot = latest.docs[0]?.snapshotDate || null;
7569
+ } catch {
7570
+ }
7571
+ const warnings = [];
7572
+ if (!config.aiKey) warnings.push("ANTHROPIC_API_KEY not set \u2014 AI features fall back to heuristics.");
7573
+ if (config.gscConfigured && !gscConnected) warnings.push("GSC configured but not connected \u2014 rank tracking & CTR opportunities inactive.");
7574
+ if (config.gscConfigured && !config.gscEncryptionKey) warnings.push("SEO_GSC_ENCRYPTION_KEY not set \u2014 GSC token encrypted with a derived key (set an explicit key for stability).");
7575
+ if ((config.alertWebhook || config.alertEmail) === false) warnings.push("No alert channel configured (SEO_ALERT_WEBHOOK_URL / SEO_ALERT_EMAIL) \u2014 monitoring digest will not be delivered.");
7576
+ return Response.json(
7577
+ {
7578
+ ok: warnings.length === 0,
7579
+ config,
7580
+ runtime: {
7581
+ auditCached,
7582
+ cacheKeys: cacheStats.size,
7583
+ gscConnected,
7584
+ gscEmail,
7585
+ lastRankSnapshot
7586
+ },
7587
+ warnings
7588
+ },
7589
+ { headers: { "Cache-Control": "no-store" } }
7590
+ );
7591
+ } catch (error) {
7592
+ const message = error instanceof Error ? error.message : "Internal server error";
7593
+ req.payload.logger.error(`[seo] health error: ${message}`);
7594
+ return Response.json({ error: message }, { status: 500 });
7595
+ }
7596
+ };
7597
+ }
7598
+
7526
7599
  // src/rateLimiter.ts
7527
7600
  function createRateLimiter(maxRequests, windowMs) {
7528
7601
  const store = /* @__PURE__ */ new Map();
@@ -7570,7 +7643,7 @@ function getClientIp(req) {
7570
7643
 
7571
7644
  // src/endpoints/seoLogs.ts
7572
7645
  var VALID_LOG_TYPES = ["404", "redirect", "error"];
7573
- function isAdmin8(user) {
7646
+ function isAdmin9(user) {
7574
7647
  if (!user) return false;
7575
7648
  if (user.role === "admin") return true;
7576
7649
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7667,7 +7740,7 @@ function createSeoLogsHandler(seoLogsSecret) {
7667
7740
  return Response.json({ error: "Unauthorized" }, { status: 401 });
7668
7741
  }
7669
7742
  if (method === "DELETE") {
7670
- if (!isAdmin8(req.user)) {
7743
+ if (!isAdmin9(req.user)) {
7671
7744
  return Response.json({ error: "Admin access required" }, { status: 403 });
7672
7745
  }
7673
7746
  try {
@@ -7909,7 +7982,7 @@ function stopRankTracker() {
7909
7982
  }
7910
7983
 
7911
7984
  // src/endpoints/alerts.ts
7912
- function isAdmin9(user) {
7985
+ function isAdmin10(user) {
7913
7986
  if (!user) return false;
7914
7987
  if (user.role === "admin") return true;
7915
7988
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -8075,7 +8148,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
8075
8148
  function createAlertsDigestHandler() {
8076
8149
  return async (req) => {
8077
8150
  try {
8078
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8151
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8079
8152
  const cfg = getAlertConfig();
8080
8153
  const digest = await buildAlertDigest(req.payload, cfg);
8081
8154
  return Response.json(
@@ -8101,7 +8174,7 @@ function createAlertsDigestHandler() {
8101
8174
  function createAlertsRunHandler(siteUrl) {
8102
8175
  return async (req) => {
8103
8176
  try {
8104
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8177
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8105
8178
  const cfg = getAlertConfig();
8106
8179
  const digest = await buildAlertDigest(req.payload, cfg);
8107
8180
  const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
@@ -9154,6 +9227,8 @@ var fr = {
9154
9227
  bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
9155
9228
  bulkOptimizing: "Analyse\u2026",
9156
9229
  bulkConfirm: "Confirmer ?",
9230
+ optimizeSite: "Optimiser le site",
9231
+ optimizeSiteHint: "Cible automatiquement les pages \xE0 probl\xE8me (m\xE9ta manquante, sans mot-cl\xE9, score faible) \u2192 aper\xE7u \u2192 appliquer",
9157
9232
  bulkPreviewTitle: "Corrections m\xE9ta propos\xE9es",
9158
9233
  bulkApply: "Appliquer",
9159
9234
  bulkApplying: "Application\u2026",
@@ -9756,6 +9831,8 @@ var en = {
9756
9831
  bulkOptimizeMeta: "Optimize meta (AI)",
9757
9832
  bulkOptimizing: "Analyzing\u2026",
9758
9833
  bulkConfirm: "Confirm?",
9834
+ optimizeSite: "Optimize site",
9835
+ optimizeSiteHint: "Auto-targets pages that need work (missing meta, no keyword, low score) \u2192 preview \u2192 apply",
9759
9836
  bulkPreviewTitle: "Proposed meta corrections",
9760
9837
  bulkApply: "Apply",
9761
9838
  bulkApplying: "Applying\u2026",
@@ -10694,6 +10771,11 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10694
10771
  handler: withRateLimit(createDuplicateContentHandler(targetCollections))
10695
10772
  });
10696
10773
  }
10774
+ pluginEndpoints.push({
10775
+ path: `${basePath}/health`,
10776
+ method: "get",
10777
+ handler: createSeoHealthHandler(basePath, seoConfig)
10778
+ });
10697
10779
  pluginEndpoints.push(
10698
10780
  {
10699
10781
  path: `${basePath}/robots.txt`,
package/dist/index.d.cts CHANGED
@@ -470,6 +470,8 @@ interface DashboardTranslations {
470
470
  bulkOptimizeMeta: string;
471
471
  bulkOptimizing: string;
472
472
  bulkConfirm: string;
473
+ optimizeSite: string;
474
+ optimizeSiteHint: string;
473
475
  bulkPreviewTitle: string;
474
476
  bulkApply: string;
475
477
  bulkApplying: string;
package/dist/index.d.ts CHANGED
@@ -470,6 +470,8 @@ interface DashboardTranslations {
470
470
  bulkOptimizeMeta: string;
471
471
  bulkOptimizing: string;
472
472
  bulkConfirm: string;
473
+ optimizeSite: string;
474
+ optimizeSiteHint: string;
473
475
  bulkPreviewTitle: string;
474
476
  bulkApply: string;
475
477
  bulkApplying: string;
package/dist/index.js CHANGED
@@ -1681,9 +1681,9 @@ function analyzeDoc(doc, collection, seoConfig) {
1681
1681
  };
1682
1682
  }
1683
1683
  var CACHE_KEY = "audit";
1684
- var auditBuildInFlight = false;
1685
- async function buildAuditCache(payload, collections, globals, seoConfig) {
1686
- const { config: mergedConfig, ignoredSlugs } = await loadMergedConfig(payload, seoConfig);
1684
+ var auditBuildsInFlight = /* @__PURE__ */ new Set();
1685
+ async function buildAuditCache(payload, collections, globals, seoConfig, reqLocale) {
1686
+ const { config: mergedConfig, ignoredSlugs } = await loadMergedConfig(payload, seoConfig, { reqLocale });
1687
1687
  const BATCH_SIZE = Math.min(100, Math.max(1, parseInt(process.env.SEO_AUDIT_BATCH_SIZE || "15", 10) || 15));
1688
1688
  const MAX_DOCS2 = Math.max(1, parseInt(process.env.SEO_AUDIT_MAX_DOCS || "1500", 10) || 1500);
1689
1689
  const allResults = [];
@@ -1791,15 +1791,15 @@ async function buildAuditCache(payload, collections, globals, seoConfig) {
1791
1791
  };
1792
1792
  return { enrichedResults, stats, capped };
1793
1793
  }
1794
- function ensureAuditBuild(payload, collections, globals, seoConfig) {
1795
- if (auditBuildInFlight) return;
1796
- auditBuildInFlight = true;
1797
- void buildAuditCache(payload, collections, globals, seoConfig).then((result) => {
1798
- seoCache.set(CACHE_KEY, result);
1794
+ function ensureAuditBuild(payload, collections, globals, seoConfig, cacheKey, reqLocale) {
1795
+ if (auditBuildsInFlight.has(cacheKey)) return;
1796
+ auditBuildsInFlight.add(cacheKey);
1797
+ void buildAuditCache(payload, collections, globals, seoConfig, reqLocale).then((result) => {
1798
+ seoCache.set(cacheKey, result);
1799
1799
  }).catch((e) => {
1800
1800
  payload.logger.error(`[seo] audit build failed: ${e instanceof Error ? e.message : "unknown"}`);
1801
1801
  }).finally(() => {
1802
- auditBuildInFlight = false;
1802
+ auditBuildsInFlight.delete(cacheKey);
1803
1803
  });
1804
1804
  }
1805
1805
  function createAuditHandler(collections, seoConfig, globals = []) {
@@ -1812,12 +1812,14 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1812
1812
  const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
1813
1813
  const limit = Math.min(500, Math.max(1, parseInt(url.searchParams.get("limit") || "300", 10)));
1814
1814
  const noCache = url.searchParams.get("nocache") === "1";
1815
- if (noCache && !auditBuildInFlight) {
1816
- seoCache.invalidateKey(CACHE_KEY);
1815
+ const reqLocale = typeof req.locale === "string" && req.locale ? req.locale : void 0;
1816
+ const cacheKey = reqLocale ? `${CACHE_KEY}:${reqLocale}` : CACHE_KEY;
1817
+ if (noCache && !auditBuildsInFlight.has(cacheKey)) {
1818
+ seoCache.invalidateKey(cacheKey);
1817
1819
  }
1818
- const cached = seoCache.get(CACHE_KEY);
1820
+ const cached = seoCache.get(cacheKey);
1819
1821
  if (!cached) {
1820
- ensureAuditBuild(req.payload, collections, globals, seoConfig);
1822
+ ensureAuditBuild(req.payload, collections, globals, seoConfig, cacheKey, reqLocale);
1821
1823
  return Response.json(
1822
1824
  { building: true, results: [], stats: null },
1823
1825
  { status: 202, headers: { "Cache-Control": "no-store" } }
@@ -7521,6 +7523,77 @@ function createCtrOpportunitiesHandler(basePath, targetCollections, seoConfig) {
7521
7523
  };
7522
7524
  }
7523
7525
 
7526
+ // src/endpoints/health.ts
7527
+ function isAdmin8(user) {
7528
+ if (!user) return false;
7529
+ if (user.role === "admin") return true;
7530
+ if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
7531
+ return false;
7532
+ }
7533
+ function createSeoHealthHandler(basePath, seoConfig) {
7534
+ return async (req) => {
7535
+ try {
7536
+ if (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7537
+ const config = {
7538
+ aiKey: !!process.env.ANTHROPIC_API_KEY,
7539
+ aiModel: process.env.SEO_AI_MODEL || "claude-opus-4-8",
7540
+ pageSpeedKey: !!(process.env.PAGESPEED_API_KEY || process.env.GOOGLE_PAGESPEED_API_KEY),
7541
+ gscConfigured: !!getGscOAuthConfig(basePath, seoConfig),
7542
+ gscEncryptionKey: !!process.env.SEO_GSC_ENCRYPTION_KEY,
7543
+ alertWebhook: !!process.env.SEO_ALERT_WEBHOOK_URL,
7544
+ alertEmail: !!process.env.SEO_ALERT_EMAIL,
7545
+ siteUrl: seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || null
7546
+ };
7547
+ const cacheStats = seoCache.stats();
7548
+ const auditCached = cacheStats.keys.some((k) => k === "audit" || k.startsWith("audit:"));
7549
+ let gscConnected = false;
7550
+ let gscEmail = null;
7551
+ let lastRankSnapshot = null;
7552
+ try {
7553
+ const authDoc = await getOrCreateGscAuthDoc(req.payload);
7554
+ gscConnected = !!authDoc.refreshTokenEnc;
7555
+ gscEmail = authDoc.connectedEmail || null;
7556
+ } catch {
7557
+ }
7558
+ try {
7559
+ const latest = await req.payload.find({
7560
+ collection: "seo-rank-history",
7561
+ sort: "-snapshotDate",
7562
+ limit: 1,
7563
+ depth: 0,
7564
+ overrideAccess: true
7565
+ });
7566
+ lastRankSnapshot = latest.docs[0]?.snapshotDate || null;
7567
+ } catch {
7568
+ }
7569
+ const warnings = [];
7570
+ if (!config.aiKey) warnings.push("ANTHROPIC_API_KEY not set \u2014 AI features fall back to heuristics.");
7571
+ if (config.gscConfigured && !gscConnected) warnings.push("GSC configured but not connected \u2014 rank tracking & CTR opportunities inactive.");
7572
+ if (config.gscConfigured && !config.gscEncryptionKey) warnings.push("SEO_GSC_ENCRYPTION_KEY not set \u2014 GSC token encrypted with a derived key (set an explicit key for stability).");
7573
+ if ((config.alertWebhook || config.alertEmail) === false) warnings.push("No alert channel configured (SEO_ALERT_WEBHOOK_URL / SEO_ALERT_EMAIL) \u2014 monitoring digest will not be delivered.");
7574
+ return Response.json(
7575
+ {
7576
+ ok: warnings.length === 0,
7577
+ config,
7578
+ runtime: {
7579
+ auditCached,
7580
+ cacheKeys: cacheStats.size,
7581
+ gscConnected,
7582
+ gscEmail,
7583
+ lastRankSnapshot
7584
+ },
7585
+ warnings
7586
+ },
7587
+ { headers: { "Cache-Control": "no-store" } }
7588
+ );
7589
+ } catch (error) {
7590
+ const message = error instanceof Error ? error.message : "Internal server error";
7591
+ req.payload.logger.error(`[seo] health error: ${message}`);
7592
+ return Response.json({ error: message }, { status: 500 });
7593
+ }
7594
+ };
7595
+ }
7596
+
7524
7597
  // src/rateLimiter.ts
7525
7598
  function createRateLimiter(maxRequests, windowMs) {
7526
7599
  const store = /* @__PURE__ */ new Map();
@@ -7568,7 +7641,7 @@ function getClientIp(req) {
7568
7641
 
7569
7642
  // src/endpoints/seoLogs.ts
7570
7643
  var VALID_LOG_TYPES = ["404", "redirect", "error"];
7571
- function isAdmin8(user) {
7644
+ function isAdmin9(user) {
7572
7645
  if (!user) return false;
7573
7646
  if (user.role === "admin") return true;
7574
7647
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7665,7 +7738,7 @@ function createSeoLogsHandler(seoLogsSecret) {
7665
7738
  return Response.json({ error: "Unauthorized" }, { status: 401 });
7666
7739
  }
7667
7740
  if (method === "DELETE") {
7668
- if (!isAdmin8(req.user)) {
7741
+ if (!isAdmin9(req.user)) {
7669
7742
  return Response.json({ error: "Admin access required" }, { status: 403 });
7670
7743
  }
7671
7744
  try {
@@ -7907,7 +7980,7 @@ function stopRankTracker() {
7907
7980
  }
7908
7981
 
7909
7982
  // src/endpoints/alerts.ts
7910
- function isAdmin9(user) {
7983
+ function isAdmin10(user) {
7911
7984
  if (!user) return false;
7912
7985
  if (user.role === "admin") return true;
7913
7986
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -8073,7 +8146,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
8073
8146
  function createAlertsDigestHandler() {
8074
8147
  return async (req) => {
8075
8148
  try {
8076
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8149
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8077
8150
  const cfg = getAlertConfig();
8078
8151
  const digest = await buildAlertDigest(req.payload, cfg);
8079
8152
  return Response.json(
@@ -8099,7 +8172,7 @@ function createAlertsDigestHandler() {
8099
8172
  function createAlertsRunHandler(siteUrl) {
8100
8173
  return async (req) => {
8101
8174
  try {
8102
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8175
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8103
8176
  const cfg = getAlertConfig();
8104
8177
  const digest = await buildAlertDigest(req.payload, cfg);
8105
8178
  const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
@@ -9152,6 +9225,8 @@ var fr = {
9152
9225
  bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
9153
9226
  bulkOptimizing: "Analyse\u2026",
9154
9227
  bulkConfirm: "Confirmer ?",
9228
+ optimizeSite: "Optimiser le site",
9229
+ optimizeSiteHint: "Cible automatiquement les pages \xE0 probl\xE8me (m\xE9ta manquante, sans mot-cl\xE9, score faible) \u2192 aper\xE7u \u2192 appliquer",
9155
9230
  bulkPreviewTitle: "Corrections m\xE9ta propos\xE9es",
9156
9231
  bulkApply: "Appliquer",
9157
9232
  bulkApplying: "Application\u2026",
@@ -9754,6 +9829,8 @@ var en = {
9754
9829
  bulkOptimizeMeta: "Optimize meta (AI)",
9755
9830
  bulkOptimizing: "Analyzing\u2026",
9756
9831
  bulkConfirm: "Confirm?",
9832
+ optimizeSite: "Optimize site",
9833
+ optimizeSiteHint: "Auto-targets pages that need work (missing meta, no keyword, low score) \u2192 preview \u2192 apply",
9757
9834
  bulkPreviewTitle: "Proposed meta corrections",
9758
9835
  bulkApply: "Apply",
9759
9836
  bulkApplying: "Applying\u2026",
@@ -10692,6 +10769,11 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10692
10769
  handler: withRateLimit(createDuplicateContentHandler(targetCollections))
10693
10770
  });
10694
10771
  }
10772
+ pluginEndpoints.push({
10773
+ path: `${basePath}/health`,
10774
+ method: "get",
10775
+ handler: createSeoHealthHandler(basePath, seoConfig)
10776
+ });
10695
10777
  pluginEndpoints.push(
10696
10778
  {
10697
10779
  path: `${basePath}/robots.txt`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-seo-analyzer",
3
- "version": "1.13.0",
3
+ "version": "1.15.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",