@consilioweb/payload-seo-analyzer 1.14.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
@@ -7525,6 +7525,77 @@ function createCtrOpportunitiesHandler(basePath, targetCollections, seoConfig) {
7525
7525
  };
7526
7526
  }
7527
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
+
7528
7599
  // src/rateLimiter.ts
7529
7600
  function createRateLimiter(maxRequests, windowMs) {
7530
7601
  const store = /* @__PURE__ */ new Map();
@@ -7572,7 +7643,7 @@ function getClientIp(req) {
7572
7643
 
7573
7644
  // src/endpoints/seoLogs.ts
7574
7645
  var VALID_LOG_TYPES = ["404", "redirect", "error"];
7575
- function isAdmin8(user) {
7646
+ function isAdmin9(user) {
7576
7647
  if (!user) return false;
7577
7648
  if (user.role === "admin") return true;
7578
7649
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7669,7 +7740,7 @@ function createSeoLogsHandler(seoLogsSecret) {
7669
7740
  return Response.json({ error: "Unauthorized" }, { status: 401 });
7670
7741
  }
7671
7742
  if (method === "DELETE") {
7672
- if (!isAdmin8(req.user)) {
7743
+ if (!isAdmin9(req.user)) {
7673
7744
  return Response.json({ error: "Admin access required" }, { status: 403 });
7674
7745
  }
7675
7746
  try {
@@ -7911,7 +7982,7 @@ function stopRankTracker() {
7911
7982
  }
7912
7983
 
7913
7984
  // src/endpoints/alerts.ts
7914
- function isAdmin9(user) {
7985
+ function isAdmin10(user) {
7915
7986
  if (!user) return false;
7916
7987
  if (user.role === "admin") return true;
7917
7988
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -8077,7 +8148,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
8077
8148
  function createAlertsDigestHandler() {
8078
8149
  return async (req) => {
8079
8150
  try {
8080
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8151
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8081
8152
  const cfg = getAlertConfig();
8082
8153
  const digest = await buildAlertDigest(req.payload, cfg);
8083
8154
  return Response.json(
@@ -8103,7 +8174,7 @@ function createAlertsDigestHandler() {
8103
8174
  function createAlertsRunHandler(siteUrl) {
8104
8175
  return async (req) => {
8105
8176
  try {
8106
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8177
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8107
8178
  const cfg = getAlertConfig();
8108
8179
  const digest = await buildAlertDigest(req.payload, cfg);
8109
8180
  const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
@@ -10700,6 +10771,11 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10700
10771
  handler: withRateLimit(createDuplicateContentHandler(targetCollections))
10701
10772
  });
10702
10773
  }
10774
+ pluginEndpoints.push({
10775
+ path: `${basePath}/health`,
10776
+ method: "get",
10777
+ handler: createSeoHealthHandler(basePath, seoConfig)
10778
+ });
10703
10779
  pluginEndpoints.push(
10704
10780
  {
10705
10781
  path: `${basePath}/robots.txt`,
package/dist/index.js CHANGED
@@ -7523,6 +7523,77 @@ function createCtrOpportunitiesHandler(basePath, targetCollections, seoConfig) {
7523
7523
  };
7524
7524
  }
7525
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
+
7526
7597
  // src/rateLimiter.ts
7527
7598
  function createRateLimiter(maxRequests, windowMs) {
7528
7599
  const store = /* @__PURE__ */ new Map();
@@ -7570,7 +7641,7 @@ function getClientIp(req) {
7570
7641
 
7571
7642
  // src/endpoints/seoLogs.ts
7572
7643
  var VALID_LOG_TYPES = ["404", "redirect", "error"];
7573
- function isAdmin8(user) {
7644
+ function isAdmin9(user) {
7574
7645
  if (!user) return false;
7575
7646
  if (user.role === "admin") return true;
7576
7647
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7667,7 +7738,7 @@ function createSeoLogsHandler(seoLogsSecret) {
7667
7738
  return Response.json({ error: "Unauthorized" }, { status: 401 });
7668
7739
  }
7669
7740
  if (method === "DELETE") {
7670
- if (!isAdmin8(req.user)) {
7741
+ if (!isAdmin9(req.user)) {
7671
7742
  return Response.json({ error: "Admin access required" }, { status: 403 });
7672
7743
  }
7673
7744
  try {
@@ -7909,7 +7980,7 @@ function stopRankTracker() {
7909
7980
  }
7910
7981
 
7911
7982
  // src/endpoints/alerts.ts
7912
- function isAdmin9(user) {
7983
+ function isAdmin10(user) {
7913
7984
  if (!user) return false;
7914
7985
  if (user.role === "admin") return true;
7915
7986
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -8075,7 +8146,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
8075
8146
  function createAlertsDigestHandler() {
8076
8147
  return async (req) => {
8077
8148
  try {
8078
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8149
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8079
8150
  const cfg = getAlertConfig();
8080
8151
  const digest = await buildAlertDigest(req.payload, cfg);
8081
8152
  return Response.json(
@@ -8101,7 +8172,7 @@ function createAlertsDigestHandler() {
8101
8172
  function createAlertsRunHandler(siteUrl) {
8102
8173
  return async (req) => {
8103
8174
  try {
8104
- if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8175
+ if (!isAdmin10(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
8105
8176
  const cfg = getAlertConfig();
8106
8177
  const digest = await buildAlertDigest(req.payload, cfg);
8107
8178
  const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
@@ -10698,6 +10769,11 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10698
10769
  handler: withRateLimit(createDuplicateContentHandler(targetCollections))
10699
10770
  });
10700
10771
  }
10772
+ pluginEndpoints.push({
10773
+ path: `${basePath}/health`,
10774
+ method: "get",
10775
+ handler: createSeoHealthHandler(basePath, seoConfig)
10776
+ });
10701
10777
  pluginEndpoints.push(
10702
10778
  {
10703
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.14.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",