@consilioweb/payload-seo-analyzer 1.8.0 → 1.8.1

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
@@ -1632,7 +1632,10 @@ function analyzeDoc(doc, collection, seoConfig) {
1632
1632
  if (seoInput.isGlobal) {
1633
1633
  seoInput.slug = "";
1634
1634
  }
1635
- const analysis = analyzeSeo(seoInput, seoConfig);
1635
+ const analysis = analyzeSeo(seoInput, {
1636
+ ...seoConfig,
1637
+ disabledRules: [...seoConfig?.disabledRules ?? [], "geo", "eeat", "hreflang"]
1638
+ });
1636
1639
  const extracted = extractDocContent(doc);
1637
1640
  const fullText = extracted.text;
1638
1641
  const allLinks = extracted.links;
@@ -1693,28 +1696,48 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1693
1696
  let cached = noCache ? null : seoCache.get(CACHE_KEY);
1694
1697
  if (!cached) {
1695
1698
  const { config: mergedConfig, ignoredSlugs } = await loadMergedConfig(req.payload, seoConfig);
1699
+ const BATCH_SIZE = Math.min(100, Math.max(1, parseInt(process.env.SEO_AUDIT_BATCH_SIZE || "25", 10) || 25));
1700
+ const MAX_DOCS2 = Math.max(1, parseInt(process.env.SEO_AUDIT_MAX_DOCS || "1500", 10) || 1500);
1696
1701
  const allResults = [];
1697
- for (const collectionSlug of collections) {
1698
- try {
1699
- let page2 = 1;
1700
- let hasMore = true;
1701
- while (hasMore) {
1702
- const result = await req.payload.find({
1703
- collection: collectionSlug,
1704
- limit: 100,
1705
- page: page2,
1706
- depth: 1,
1707
- overrideAccess: true
1708
- });
1709
- for (const doc of result.docs) {
1710
- if (ignoredSlugs.includes(doc.slug)) continue;
1711
- allResults.push(analyzeDoc(doc, collectionSlug, mergedConfig));
1702
+ let capped2 = false;
1703
+ collectionsLoop:
1704
+ for (const collectionSlug of collections) {
1705
+ try {
1706
+ let page2 = 1;
1707
+ let hasMore = true;
1708
+ while (hasMore) {
1709
+ const result = await req.payload.find({
1710
+ collection: collectionSlug,
1711
+ limit: BATCH_SIZE,
1712
+ page: page2,
1713
+ depth: 1,
1714
+ overrideAccess: true
1715
+ });
1716
+ for (const doc of result.docs) {
1717
+ if (ignoredSlugs.includes(doc.slug)) continue;
1718
+ if (allResults.length >= MAX_DOCS2) {
1719
+ capped2 = true;
1720
+ break collectionsLoop;
1721
+ }
1722
+ try {
1723
+ allResults.push(analyzeDoc(doc, collectionSlug, mergedConfig));
1724
+ } catch (e) {
1725
+ req.payload.logger.warn(
1726
+ `[seo] audit: skipped ${collectionSlug}/${doc.id}: ${e instanceof Error ? e.message : "error"}`
1727
+ );
1728
+ }
1729
+ }
1730
+ hasMore = result.hasNextPage;
1731
+ page2++;
1732
+ await new Promise((resolve) => setImmediate(resolve));
1712
1733
  }
1713
- hasMore = result.hasNextPage;
1714
- page2++;
1734
+ } catch {
1715
1735
  }
1716
- } catch {
1717
1736
  }
1737
+ if (capped2) {
1738
+ req.payload.logger.warn(
1739
+ `[seo] audit: capped at ${MAX_DOCS2} docs (SEO_AUDIT_MAX_DOCS). Lower SEO_AUDIT_BATCH_SIZE on low-memory hosts, or raise the cap.`
1740
+ );
1718
1741
  }
1719
1742
  for (const globalSlug of globals) {
1720
1743
  try {
@@ -1777,10 +1800,10 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1777
1800
  avgWordCount: totalDocs2 > 0 ? Math.round(enrichedResults2.reduce((s, r) => s + r.wordCount, 0) / totalDocs2) : 0,
1778
1801
  avgReadability: totalDocs2 > 0 ? Math.round(enrichedResults2.reduce((s, r) => s + r.readabilityScore, 0) / totalDocs2) : 0
1779
1802
  };
1780
- cached = { enrichedResults: enrichedResults2, stats: stats2 };
1803
+ cached = { enrichedResults: enrichedResults2, stats: stats2, capped: capped2 };
1781
1804
  seoCache.set(CACHE_KEY, cached);
1782
1805
  }
1783
- const { enrichedResults, stats } = cached;
1806
+ const { enrichedResults, stats, capped } = cached;
1784
1807
  const totalDocs = enrichedResults.length;
1785
1808
  const totalPages = Math.ceil(totalDocs / limit);
1786
1809
  const startIdx = (page - 1) * limit;
@@ -1796,7 +1819,8 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1796
1819
  hasNextPage: page < totalPages,
1797
1820
  hasPrevPage: page > 1
1798
1821
  },
1799
- cached: !noCache && seoCache.get(CACHE_KEY) !== null
1822
+ cached: !noCache && seoCache.get(CACHE_KEY) !== null,
1823
+ capped
1800
1824
  }, { headers: { "Cache-Control": "no-store" } });
1801
1825
  } catch (error) {
1802
1826
  const message = error instanceof Error ? error.message : "Internal server error";
@@ -8657,6 +8681,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
8657
8681
  settings: true,
8658
8682
  gscApi: false,
8659
8683
  // opt-in — requires Google Cloud OAuth setup + secrets
8684
+ warmCache: true,
8685
+ // disable on low-memory hosts to skip startup pre-loading
8660
8686
  ...pluginConfig.features
8661
8687
  };
8662
8688
  function hasExistingSeoMeta(fields) {
@@ -9083,7 +9109,9 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
9083
9109
  const existingOnInit = config.onInit;
9084
9110
  config.onInit = async (payload) => {
9085
9111
  if (existingOnInit) await existingOnInit(payload);
9086
- startCacheWarmUp(payload, basePath, targetGlobals, targetCollections);
9112
+ if (features.warmCache) {
9113
+ startCacheWarmUp(payload, basePath, targetGlobals, targetCollections);
9114
+ }
9087
9115
  };
9088
9116
  return config;
9089
9117
  };
package/dist/index.d.cts CHANGED
@@ -168,6 +168,11 @@ interface SeoFeatures {
168
168
  * enabled, the redirect URI registered, and ideally SEO_GSC_ENCRYPTION_KEY for token-at-rest.
169
169
  */
170
170
  gscApi?: boolean;
171
+ /**
172
+ * Background cache warm-up on init + hourly (pre-loads collection data). Enabled by
173
+ * default. Set to `false` on low-memory hosting to avoid loading all documents at startup.
174
+ */
175
+ warmCache?: boolean;
171
176
  /** Duplicate content detection (endpoint) */
172
177
  duplicateContent?: boolean;
173
178
  /** Settings view (/admin/seo-config) */
package/dist/index.d.ts CHANGED
@@ -168,6 +168,11 @@ interface SeoFeatures {
168
168
  * enabled, the redirect URI registered, and ideally SEO_GSC_ENCRYPTION_KEY for token-at-rest.
169
169
  */
170
170
  gscApi?: boolean;
171
+ /**
172
+ * Background cache warm-up on init + hourly (pre-loads collection data). Enabled by
173
+ * default. Set to `false` on low-memory hosting to avoid loading all documents at startup.
174
+ */
175
+ warmCache?: boolean;
171
176
  /** Duplicate content detection (endpoint) */
172
177
  duplicateContent?: boolean;
173
178
  /** Settings view (/admin/seo-config) */
package/dist/index.js CHANGED
@@ -1630,7 +1630,10 @@ function analyzeDoc(doc, collection, seoConfig) {
1630
1630
  if (seoInput.isGlobal) {
1631
1631
  seoInput.slug = "";
1632
1632
  }
1633
- const analysis = analyzeSeo(seoInput, seoConfig);
1633
+ const analysis = analyzeSeo(seoInput, {
1634
+ ...seoConfig,
1635
+ disabledRules: [...seoConfig?.disabledRules ?? [], "geo", "eeat", "hreflang"]
1636
+ });
1634
1637
  const extracted = extractDocContent(doc);
1635
1638
  const fullText = extracted.text;
1636
1639
  const allLinks = extracted.links;
@@ -1691,28 +1694,48 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1691
1694
  let cached = noCache ? null : seoCache.get(CACHE_KEY);
1692
1695
  if (!cached) {
1693
1696
  const { config: mergedConfig, ignoredSlugs } = await loadMergedConfig(req.payload, seoConfig);
1697
+ const BATCH_SIZE = Math.min(100, Math.max(1, parseInt(process.env.SEO_AUDIT_BATCH_SIZE || "25", 10) || 25));
1698
+ const MAX_DOCS2 = Math.max(1, parseInt(process.env.SEO_AUDIT_MAX_DOCS || "1500", 10) || 1500);
1694
1699
  const allResults = [];
1695
- for (const collectionSlug of collections) {
1696
- try {
1697
- let page2 = 1;
1698
- let hasMore = true;
1699
- while (hasMore) {
1700
- const result = await req.payload.find({
1701
- collection: collectionSlug,
1702
- limit: 100,
1703
- page: page2,
1704
- depth: 1,
1705
- overrideAccess: true
1706
- });
1707
- for (const doc of result.docs) {
1708
- if (ignoredSlugs.includes(doc.slug)) continue;
1709
- allResults.push(analyzeDoc(doc, collectionSlug, mergedConfig));
1700
+ let capped2 = false;
1701
+ collectionsLoop:
1702
+ for (const collectionSlug of collections) {
1703
+ try {
1704
+ let page2 = 1;
1705
+ let hasMore = true;
1706
+ while (hasMore) {
1707
+ const result = await req.payload.find({
1708
+ collection: collectionSlug,
1709
+ limit: BATCH_SIZE,
1710
+ page: page2,
1711
+ depth: 1,
1712
+ overrideAccess: true
1713
+ });
1714
+ for (const doc of result.docs) {
1715
+ if (ignoredSlugs.includes(doc.slug)) continue;
1716
+ if (allResults.length >= MAX_DOCS2) {
1717
+ capped2 = true;
1718
+ break collectionsLoop;
1719
+ }
1720
+ try {
1721
+ allResults.push(analyzeDoc(doc, collectionSlug, mergedConfig));
1722
+ } catch (e) {
1723
+ req.payload.logger.warn(
1724
+ `[seo] audit: skipped ${collectionSlug}/${doc.id}: ${e instanceof Error ? e.message : "error"}`
1725
+ );
1726
+ }
1727
+ }
1728
+ hasMore = result.hasNextPage;
1729
+ page2++;
1730
+ await new Promise((resolve) => setImmediate(resolve));
1710
1731
  }
1711
- hasMore = result.hasNextPage;
1712
- page2++;
1732
+ } catch {
1713
1733
  }
1714
- } catch {
1715
1734
  }
1735
+ if (capped2) {
1736
+ req.payload.logger.warn(
1737
+ `[seo] audit: capped at ${MAX_DOCS2} docs (SEO_AUDIT_MAX_DOCS). Lower SEO_AUDIT_BATCH_SIZE on low-memory hosts, or raise the cap.`
1738
+ );
1716
1739
  }
1717
1740
  for (const globalSlug of globals) {
1718
1741
  try {
@@ -1775,10 +1798,10 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1775
1798
  avgWordCount: totalDocs2 > 0 ? Math.round(enrichedResults2.reduce((s, r) => s + r.wordCount, 0) / totalDocs2) : 0,
1776
1799
  avgReadability: totalDocs2 > 0 ? Math.round(enrichedResults2.reduce((s, r) => s + r.readabilityScore, 0) / totalDocs2) : 0
1777
1800
  };
1778
- cached = { enrichedResults: enrichedResults2, stats: stats2 };
1801
+ cached = { enrichedResults: enrichedResults2, stats: stats2, capped: capped2 };
1779
1802
  seoCache.set(CACHE_KEY, cached);
1780
1803
  }
1781
- const { enrichedResults, stats } = cached;
1804
+ const { enrichedResults, stats, capped } = cached;
1782
1805
  const totalDocs = enrichedResults.length;
1783
1806
  const totalPages = Math.ceil(totalDocs / limit);
1784
1807
  const startIdx = (page - 1) * limit;
@@ -1794,7 +1817,8 @@ function createAuditHandler(collections, seoConfig, globals = []) {
1794
1817
  hasNextPage: page < totalPages,
1795
1818
  hasPrevPage: page > 1
1796
1819
  },
1797
- cached: !noCache && seoCache.get(CACHE_KEY) !== null
1820
+ cached: !noCache && seoCache.get(CACHE_KEY) !== null,
1821
+ capped
1798
1822
  }, { headers: { "Cache-Control": "no-store" } });
1799
1823
  } catch (error) {
1800
1824
  const message = error instanceof Error ? error.message : "Internal server error";
@@ -8655,6 +8679,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
8655
8679
  settings: true,
8656
8680
  gscApi: false,
8657
8681
  // opt-in — requires Google Cloud OAuth setup + secrets
8682
+ warmCache: true,
8683
+ // disable on low-memory hosts to skip startup pre-loading
8658
8684
  ...pluginConfig.features
8659
8685
  };
8660
8686
  function hasExistingSeoMeta(fields) {
@@ -9081,7 +9107,9 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
9081
9107
  const existingOnInit = config.onInit;
9082
9108
  config.onInit = async (payload) => {
9083
9109
  if (existingOnInit) await existingOnInit(payload);
9084
- startCacheWarmUp(payload, basePath, targetGlobals, targetCollections);
9110
+ if (features.warmCache) {
9111
+ startCacheWarmUp(payload, basePath, targetGlobals, targetCollections);
9112
+ }
9085
9113
  };
9086
9114
  return config;
9087
9115
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-seo-analyzer",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
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",