Skip to content

fix(seo): cache SEO results for their TTL in seconds, keyed by every input - #273

Merged
docdyhr merged 2 commits into
mainfrom
fix/seo-cache-ttl-and-keys
Oct 12, 2026
Merged

docdyhr merged 2 commits into
mainfrom
fix/seo-cache-ttl-and-keys

Conversation

@docdyhr

@docdyhr docdyhr commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Pull Request

Description

The SEO tools gave cache TTLs in seconds (SEO_CACHE_*_TTL, SEOCacheManager.DEFAULT_TTL, 21600 // 6 hours in every caller), but the base CacheManager counts milliseconds. As a result:

  • a "6 hour" content analysis expired after 21.6 s;
  • a "1 hour" site audit expired after 3.6 s.

Reproduced against the built code before the fix: an entry set with ttl=21600 had expired 30 s later.

The short TTLs also hid cache keys that never covered their inputs. One SEOCacheManager serves every site:

  • Bulk analysis/metadata keys had no site, so post 5 of one site was answered with post 5 of another. This leak exists today within the 21.6 s window; the new test failed with expected 3 to be 8 before the fix.
  • Schema and metadata keys ignored focusKeywords, and the audit key ignored maxPages.
  • No key changed when a post was edited. With real TTLs, "analyze → edit → re-analyze" would have returned the stale analysis for 6 h, and schema for 24 h.

Found while reviewing #268.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)

Changes Made

  • SEOCacheManager.set() converts seconds to milliseconds in one place. ttlFor() exposes the configured TTLs, so SEO_CACHE_*_TTL now reach the tools; before, SEOTools passed hard-coded literals.
  • seoResultKey() builds every key from all call parameters (sorted, without force), plus the post's modified_gmt for post-based results. Post-based tools fetch the post before the cache lookup.
  • analysisType (previously accepted but ignored) narrows recommendations and detail sections to its area: readability → content, keywords → keyword, structure → structure + technical. Score and metrics stay whole-post.
  • auditType (previously ignored) selects the audit sections; technical includes site architecture.
  • wp_seo_site_audit: maxPages gets minimum: 1, maximum: 100, matching its description. The never-implemented includeExternalLinks is removed (checking external links would also mean fetching arbitrary URLs). API docs regenerated.
  • DOX: src/cache/AGENTS.md (TTL units, key builder) and src/tools/AGENTS.md (contract) updated.

Related Issues

Related to #268 (review follow-up).

Testing

Tests Performed

  • Unit tests pass (npm test): 2778 passed before, 2799 passed after, 0 failed
  • TypeScript compilation (npm run build), typecheck, lint, format:check, security:scan
  • RED first: 18 of the 21 new tests failed on main, each for the reason it names. The other 3 are guards that pass both ways: the full analysis, and auditType omitted or full.

Test Cases

  1. tests/cache/SEOCacheManager.test.js: a 21600 s entry lives 6 h (not 21.6 s); 6 h default; 1 h audit.
  2. tests/tools/seo/SEOToolsCaching.test.js:
    • analysis is cached for hours and re-run after an edit;
    • schema and metadata keyed by focus keywords and edits;
    • audit keyed by maxPages, cached for 1 h, force bypasses (asserted step by step: a total alone let the two bugs cancel out);
    • each analysisType and auditType;
    • schema bounds.
  3. tests/tools/seo/BulkOperations.test.js: the same post ID on two sites stays apart; edits and other focus keywords are re-analysed.
  4. Four existing cache-hit tests now count analyzer/generator calls instead of getPost calls, since every call now fetches the post. Their intent is unchanged.

Breaking Changes

None at runtime. includeExternalLinks is gone from the schema; it never had an effect, and the argument validator strips unknown properties, so callers passing it are unaffected.

Behaviour to know about: SEO results are now cached for their configured lifetimes. Keyword research is derived from site content and lasts 7 days (SEO_CACHE_KEYWORDS_TTL to shorten); SERP estimates last 12 hours. Before this fix both expired within minutes.

Documentation

  • Documentation updated (docs/api regenerated, DOX contracts)
  • CHANGELOG.md (semantic-release writes it)

Additional Notes

Merge order with the performance follow-up PR doesn't matter: the two touch disjoint code and git merge-tree is clean.

🤖 Generated with Claude Code

Summary by Sourcery

Fix SEO caching to honor configured TTLs and isolate results by every input while aligning SEO behavior and API documentation with their contracts.

Bug Fixes:

  • Correct SEO cache TTL handling so configured lifetimes are honored in seconds.
  • Prevent SEO cache collisions and stale results by including all result-shaping inputs, site and post modification state in cache keys.
  • Make SEO analysis and audit type parameters select the corresponding result sections.
  • Repair SEO cache invalidation and statistics so entries can be found and managed correctly.

Enhancements:

  • Enforce the site audit page limit and remove the unused external-links option.
  • Align generated API documentation with tool schemas, including enums, defaults, and numeric bounds.

Documentation:

  • Regenerate SEO API documentation and update cache and tool contracts.

Tests:

  • Add coverage for SEO TTLs, cache key isolation, edit invalidation, parameter-specific results, cache management, and generated documentation.

…input

Root cause: the SEO layer gives TTLs in seconds (SEO_CACHE_*_TTL,
SEOCacheManager.DEFAULT_TTL, "21600 // 6 hours" in every caller) but the
base CacheManager counts milliseconds, so a 6 h analysis expired after
21.6 s and a 1 h audit after 3.6 s. That also hid cache keys that never
covered their inputs: the shared cache answered post 5 of one site with
post 5 of another (bulk keys had no site), schema/metadata keys ignored
focus keywords, the audit key ignored maxPages, and nothing would have
noticed an edited post.

- SEOCacheManager.set() converts seconds to milliseconds, once; ttlFor()
  exposes the configured TTLs, so SEO_CACHE_*_TTL now reach the tools.
- seoResultKey(): every call parameter (sorted, without force) plus the
  post's last modification for post-based results. Post-based tools
  fetch the post before the cache lookup.
- analysisType narrows recommendations/details to its area (score and
  metrics stay whole-post); auditType selects the audit sections.
- wp_seo_site_audit: maxPages gets minimum 1 / maximum 100; the never
  implemented includeExternalLinks is removed. API docs regenerated.

Tests: 21 new (TTL lifetime, edits, focus keywords, maxPages, cross-site
bulk, analysisType, auditType, schema); 4 existing cache-hit tests now
count analyzer/generator calls, since every call fetches the post.
Suite: 2778 passed before, 2799 passed after, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 11, 2026 16:42
@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Sorry @docdyhr, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 19 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T16:45:36.763565Z 25342c9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added cache Cache-related changes docs Documentation changes labels Oct 11, 2026
@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR fixes SEO cache TTL unit conversion, routes all result types through configured lifetimes, and creates input-complete, post-versioned keys so cached results remain isolated and refresh after edits. It also makes analysisType and auditType functional, tightens the site-audit schema, removes an ineffective option, and updates tests and documentation.

Sequence diagram for versioned SEO result caching

sequenceDiagram
    participant Caller
    participant SEOTools
    participant WordPressClient
    participant SEOCacheManager
    participant Analyzer

    Caller->>SEOTools: analyzeContent(params)
    SEOTools->>WordPressClient: getPost(postId)
    WordPressClient-->>SEOTools: post with modified_gmt
    SEOTools->>SEOCacheManager: get(seoResultKey(kind, params, post))
    alt cached result
        SEOCacheManager-->>SEOTools: cached result
    else cache miss or force
        SEOTools->>Analyzer: analyzePost(post, params)
        Analyzer-->>SEOTools: SEO result
        SEOTools->>SEOCacheManager: ttlFor(analysis)
        SEOCacheManager->>SEOCacheManager: set(key, result, ttlSeconds * 1000)
        SEOTools-->>Caller: SEO result
    end
Loading

File-Level Changes

Change Details Files
Correct SEO cache expiration and centralize configured TTL handling.
  • Convert SEO TTLs from seconds to the base cache’s millisecond units in the cache manager override.
  • Expose per-result TTLs and update SEO callers to select configured TTL tiers instead of hard-coded durations.
  • Add regression coverage for default, analysis, and audit lifetimes.
src/cache/SEOCacheManager.ts
src/tools/seo/SEOTools.ts
tests/cache/SEOCacheManager.test.js
tests/tools/seo/SEOToolsCaching.test.js
Make SEO cache entries safe across sites, input variations, and post edits.
  • Build deterministic keys from sorted call parameters while excluding only force-control flags.
  • Include the post modification timestamp for post-based results and fetch posts before cache lookup.
  • Apply the key strategy to single-post and bulk analysis, schema, metadata, audit, SERP, and keyword operations.
  • Add tests covering cross-site isolation, focus keywords, maxPages, edits, and force refresh behavior.
src/cache/SEOCacheManager.ts
src/tools/seo/SEOTools.ts
src/tools/seo/BulkOperations.ts
tests/tools/seo/BulkOperations.test.js
tests/tools/seo/SEOToolsAnalyzeContent.test.js
tests/tools/seo/SEOToolsCaching.test.js
Implement the previously ignored analysis and audit selector parameters.
  • Filter analysis recommendations and optional detail sections by analysisType while retaining whole-post scores and metrics.
  • Map auditType to technical, content, performance, or full auditor sections, including site architecture under technical.
  • Add selector behavior tests.
src/tools/seo/analyzers/ContentAnalyzer.ts
src/tools/seo/SEOTools.ts
src/tools/seo/SEOToolDefinitions.ts
tests/tools/seo/SEOToolsCaching.test.js
Align the site-audit contract and generated documentation with implemented behavior.
  • Constrain maxPages to 1–100.
  • Remove the unused includeExternalLinks parameter from types, handlers, schema, and docs.
  • Regenerate API documentation and update cache/tooling contracts.
src/tools/seo/SEOToolDefinitions.ts
src/tools/seo/SEOHandlers.ts
src/types/seo.ts
docs/api/README.md
docs/api/openapi.json
docs/api/summary.json
docs/api/tools/wp_seo_site_audit.md
docs/api/tools/wp_seo_analyze_content.md
src/cache/AGENTS.md
src/tools/AGENTS.md
tests/tools/seo/SEOToolsCaching.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cache ordering, invalidation, bulk TTL configuration, and generated OpenAPI constraints remain incorrect.

4 open findings
What changed in this PR

Fixes SEO cache TTL units, key isolation, invalidation after edits, and analysis/audit filtering.

Changes:

  • Converts SEO TTLs from seconds to milliseconds centrally.
  • Expands cache keys to include result-shaping inputs and post modification times.
  • Adds filtering behavior, validation, documentation, and regression tests.
File Description
src/​cache/​SEOCacheManager.ts Adds centralized TTL conversion and key generation.
src/​cache/​AGENTS.md Documents SEO cache contracts.
src/​tools/​seo/​SEOTools.ts Uses new keys, configured TTLs, and audit filtering.
src/​tools/​seo/​BulkOperations.ts Adds site-, input-, and edit-aware bulk keys.
src/​tools/​seo/​analyzers/​ContentAnalyzer.ts Filters analysis details by requested area.
src/​tools/​seo/​SEOToolDefinitions.ts Updates schemas and descriptions.
src/​tools/​seo/​SEOHandlers.ts Removes unsupported external-link handling.
src/​types/​seo.ts Removes the unused external-link parameter.
src/​tools/​AGENTS.md Documents tool caching requirements.
tests/​cache/​SEOCacheManager.test.js Tests corrected TTL behavior.
tests/​tools/​seo/​SEOToolsCaching.test.js Covers caching, invalidation, and filtering.
tests/​tools/​seo/​SEOToolsAnalyzeContent.test.js Updates cache-hit assertions.
tests/​tools/​seo/​BulkOperations.test.js Tests bulk cache isolation and edits.
docs/​api/​tools/​wp_seo_site_audit.md Regenerates site-audit documentation.
docs/​api/​tools/​wp_seo_analyze_content.md Documents analysis filtering.
docs/​api/​openapi.json Regenerates SEO OpenAPI definitions.
docs/​api/​README.md Updates generation date.
docs/​api/​summary.json Updates generation timestamp.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/cache/SEOCacheManager.ts Outdated
Comment thread src/tools/seo/BulkOperations.ts
Comment thread src/tools/seo/SEOTools.ts Outdated
Comment thread docs/api/openapi.json Outdated
…nt work

Review follow-ups (Copilot on #273):

- SEO keys are now seo:<kind>:<site>:<postId|->:<version>:<params>, so
  invalidatePostSEO/invalidateSiteSEO/clearSEOCache/getSEOCacheStats can
  find them. They never could: getAllKeys() was a placeholder returning
  [], and getSEOCacheKey() built "seo::type" keys. They now read the base
  cache's keys() and match on segments (site "s1" no longer matches
  "s10" as the old glob would have).
- SERP keys keep keyword order: positions come back in request order.
- Bulk analysis/metadata use the configured TTLs (ttlFor()).
- DocumentationGenerator keeps a schema's minimum/maximum and takes enums
  and defaults from the schema; the name-keyed tables gave comment status
  the post statuses and an invalid default "publish". Docs regenerated.

Tests: 9 new (8 failed before the fix); suite 2808 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@docdyhr
docdyhr merged commit b0711c6 into main Oct 12, 2026
32 checks passed
@docdyhr
docdyhr deleted the fix/seo-cache-ttl-and-keys branch October 12, 2026 01:07
docdyhr pushed a commit that referenced this pull request Oct 12, 2026
## [4.1.5](v4.1.4...v4.1.5) (2026-10-12)

### 🐛 Bug Fixes

* **seo:** cache SEO results for their TTL in seconds, keyed by every input ([#273](#273)) ([b0711c6](b0711c6))
@docdyhr

docdyhr commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 4.1.5 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cache Cache-related changes docs Documentation changes released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants