agentleFS
carlosuhartcarlosuhart/seo-audit-skills/skills/cache-headers/SKILL.md

cache-headers

Skill6 starsChanged 12 days agoMITView on GitHub

Cache architecture for SEO: Cache-Control, CDN vs browser cache, ETag and Last-Modified, and CMS setups (WP Rocket, LiteSpeed, PrestaShop, Nginx/Apache). Use for TTFB issues, caching layers or cache header audits.

  • Sends data out

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Verify this is working: publish a post and check the response headers for the page — X-LiteSpeed-Cache: miss confirms a fresh request was …

    line 163
  2. Distinctive:

    When you publish new content, the CDN must serve the new version.

    line 328
  3. Distinctive:

    Check CDN rules — HTML may need an explicit "Cache Everything" rule.

    line 454

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Cache Headers & Caching Strategy — Technical SEO

Correct caching is the single highest-ROI performance optimization for most sites. A cold origin TTFB of 800ms becomes < 50ms with a CDN cache hit.


Cache-Control directives reference

DirectiveMeaningUse case
max-age=NBrowser can cache for N secondsStatic assets
s-maxage=NCDN/shared cache can cache for N secondsCDN-specific TTL
no-cacheMust revalidate with server before using cacheHTML pages
no-storeDo not cache at allAuthenticated/transactional pages
must-revalidateOnce stale, must revalidate (do not serve stale)HTML pages
publicShared caches (CDN) can store this responseStatic assets, HTML
privateOnly browser cache, not CDNLogged-in user pages
immutableContent will not change — no revalidation neededVersioned static assets
stale-while-revalidate=NServe stale while fetching fresh in backgroundHTML for fast TTFB

Combinations by content type

Static assets (CSS, JS, images) — versioned with hash in filename:
Cache-Control: public, max-age=31536000, immutable

HTML pages (anonymous visitors):
Cache-Control: public, max-age=0, must-revalidate
  or with CDN:
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400

HTML pages (logged-in users):
Cache-Control: private, no-cache

Checkout / cart / account:
Cache-Control: no-store

API responses (public):
Cache-Control: public, max-age=300, s-maxage=600

Sitemaps and robots.txt:
Cache-Control: public, max-age=3600

Cache layers architecture

User browser
    ↓
Browser cache (private, per user)
    ↓
CDN edge (shared, per PoP — Cloudflare, Fastly, CloudFront, etc.)
    ↓
Reverse proxy / load balancer (Nginx, Varnish)
    ↓
Application cache (WP Rocket page cache, LiteSpeed Cache, PrestaShop CCC)
    ↓
Object cache (Redis, Memcached — DB query results)
    ↓
PHP runtime (OPcache — compiled PHP bytecode)
    ↓
Database (MySQL / MariaDB)

Each layer reduces load on the next. A CDN hit eliminates all layers below it. A page cache hit eliminates the PHP and database layers.


ETag and Last-Modified

Both are validation mechanisms — they allow a client with a cached response to ask "has this changed?" without downloading the full response again.

Last-Modified:

Response: Last-Modified: Wed, 16 Apr 2026 10:00:00 GMT
Next request: If-Modified-Since: Wed, 16 Apr 2026 10:00:00 GMT
Server: 304 Not Modified (if unchanged) or 200 + new content

ETag:

Response: ETag: "abc123"
Next request: If-None-Match: "abc123"
Server: 304 Not Modified (if same) or 200 + new content

SEO relevance: ETags and Last-Modified enable Googlebot to efficiently re-crawl pages — if the content has not changed, Googlebot receives 304 and skips re-processing. This conserves crawl budget on large sites.

When ETags are a problem: Nginx and Apache can generate ETags that include inode numbers. On multi-server setups, different servers generate different ETags for the same file → cache validation fails → unnecessary full responses.

Fix for Nginx:

etag off;  # or use FileETag MTime Size in Apache

WordPress caching

Page cache — plugin options

PluginMechanismRecommended stack
WP RocketFile-based page cacheNginx or Apache + LiteSpeed
LiteSpeed CacheLiteSpeed server-nativeHostinger, LiteSpeed servers
W3 Total CacheMultiple backendsVPS with Redis/Memcached
WP Super CacheFile-basedBasic shared hosting

WP Rocket configuration

Page Cache:

  • Settings > Cache > Enable caching for mobile devices: on
  • Separate cache for mobile: on (if theme is responsive, keep off)
  • Cache lifespan: 10 hours (adjust to content update frequency)

Browser cache:

  • Settings > Browser Caching > on
  • WP Rocket handles Cache-Control headers for static assets

CDN integration:

  • Settings > CDN > CDN hostname: your CDN URL
  • This rewrites asset URLs to the CDN hostname

Exclusions (important for e-commerce):

URLs to never cache:
/cart/
/checkout/
/my-account/
/order-received/

Cookies that bypass cache:
woocommerce_cart_hash
woocommerce_items_in_cart
wp_woocommerce_session_

LiteSpeed Cache

Common misconfiguration — X-Robots-Tag on XML files: LiteSpeed may apply X-Robots-Tag: noindex to all XML responses including sitemaps. Fix: LiteSpeed Cache > Advanced > Excludes > Do Not Cache URIs > /sitemap*.xml

Cache purge on publish: LiteSpeed Cache automatically purges page cache when a post is published or updated. Verify this is working: publish a post and check the response headers for the page — X-LiteSpeed-Cache: miss confirms a fresh request was served.

Separate mobile cache — WP Customizer CSS stripped: LiteSpeed Cache can maintain separate cached versions for desktop and mobile (detected by User-Agent). When CSS is added via WP Customizer (Additional CSS), LiteSpeed's CSS optimization strips the <style id="wp-custom-css"> block when building the mobile cache. Result: CSS works for logged-in admins (who bypass cache) and in DevTools mobile simulation (which uses the desktop cache), but not on real mobile devices.

Diagnosis:

# Fetch with mobile UA and check for wp-custom-css block
curl -s -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1" \
  https://domain.com/page/ | grep -i "wp-custom-css\|font-size"
# If wp-custom-css is absent from the HTML, LiteSpeed is stripping it

Fix — use Code Snippets plugin (scope site-css) instead of WP Customizer: Code Snippets generates a dedicated external stylesheet (?code-snippets-css=1) that LiteSpeed includes correctly in the HTML for all devices. The WP Customizer inline <style> block is unreliable with LiteSpeed CSS optimization enabled.

# Create CSS snippet via Code Snippets REST API
curl -X POST https://domain.com/wp-json/code-snippets/v1/snippets \
  -H "Authorization: Basic <base64(user:app-password)>" \
  -H "Content-Type: application/json" \
  -d '{"title":"my-css","code":".selector { property: value !important; }","scope":"site-css","active":true}'
# Then purge LiteSpeed cache manually from WP Admin

Note: WP Admin may be blocked by Cloudflare for scripted requests. In that case the cache purge must be triggered manually from the WordPress dashboard.

Redis object cache

Object cache stores database query results in memory. Eliminates repeated identical queries to MySQL.

// wp-config.php
define('WP_CACHE_KEY_SALT', 'unique-site-key');
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);

Object cache reduces TTFB on pages with complex WP_Query calls (WooCommerce shop, custom post type archives, widgets pulling recent posts).


PrestaShop caching

CCC (Combine, Compress, Cache)

Path: Advanced Parameters > Performance > CCC

OptionEffectRecommended
Smart cache for CSSCombines and compresses CSSOn
Smart cache for JavaScriptCombines and compresses JSOn
Minify HTMLRemoves whitespace from HTMLOn
Compress inline JavaScriptMinifies inline JSOn
Move JavaScript to endDefers JS to before </body>On

Warning: CCC can break custom themes. Test on staging first.

Smarty cache

Path: Advanced Parameters > Performance > Smarty

SettingProduction value
Cache compilationNever recompile template files
CacheEnabled
Caching typeFile system

Full-page cache for PrestaShop

PrestaShop does not include a built-in full-page cache. Options:

  1. Varnish in front of the application server — caches full HTML responses
  2. Nginx FastCGI cache — caches PHP-FPM responses at the web server level
  3. Third-party module — available on PrestaShop Addons marketplace

Configure cache bypass for:

  • /order/ (checkout)
  • /cart/
  • Any URL with PHPSESSID cookie set

Nginx caching configuration

FastCGI page cache

# In http block
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m
                   inactive=60m max_size=1g;

# In server block
set $skip_cache 0;

# Bypass cache for logged-in users and WooCommerce
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|woocommerce_cart_hash|woocommerce_items_in_cart") {
    set $skip_cache 1;
}
if ($request_uri ~* "/wp-admin/|/wp-login.php|/cart/|/checkout/|/my-account/") {
    set $skip_cache 1;
}

fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 302 1h;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status;

Static asset cache headers

# CSS, JS, fonts — long cache with versioning assumption
location ~* \.(css|js|woff2|woff|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# Images
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000";
}

# Sitemaps and feeds — shorter TTL
location ~* \.(xml|txt)$ {
    expires 1h;
    add_header Cache-Control "public, max-age=3600";
}

CDN caching

What CDNs cache by default

Most CDNs (Cloudflare, Fastly, CloudFront) cache:

  • CSS, JS, images, fonts — yes, by default
  • HTML — no, not by default (treated as dynamic)

To cache HTML, you must configure a cache rule:

Cloudflare — cache HTML with short TTL:

Rule: Cache Everything
Edge TTL: 1 hour
Bypass cache: Cookie contains "wordpress_logged_in" OR URI contains "/wp-admin/"

Effect: TTFB drops from 400-800ms (origin) to 10-50ms (CDN edge).

Cache invalidation

When you publish new content, the CDN must serve the new version. Options:

  1. Short TTL (1h): cache expires automatically. Simple but means stale content for up to 1 hour after a change.
  2. Cache purge on publish: WordPress plugins (WP Rocket, LiteSpeed) can purge the CDN cache for specific URLs when content is updated.
  3. Cache-Control: stale-while-revalidate: serve stale while fetching fresh in background — best user experience.

LiteSpeed + Cloudflare — respect_origin no tiene nada que respetar

Combinación muy común (WordPress + LiteSpeed Cache + Cloudflare) con un fallo silencioso que no aparece en ningún panel: el HTML apenas se cachea en el edge y el TTFB de campo se dispara a 2 s o más, mientras que cada petición individual que se prueba a mano parece rápida.

La causa: LiteSpeed Cache emite x-litespeed-cache-control: public,max-age=604800, una cabecera propietaria que Cloudflare no interpreta, y no emite ningún Cache-Control estándar. Si la Cache Rule de Cloudflare usa Edge TTL: respect origin, Cloudflare no encuentra ninguna directiva que respetar y cae a sus heurísticas por defecto, reteniendo el HTML unos pocos segundos. Cada expiración cuesta un render PHP completo.

Cómo confirmarlo — comparar la respuesta con y sin que aplique la Cache Rule. Enviar una cookie de sesión de WordPress hace que la regla no aplique y devuelve la respuesta del origen tal cual:

# Origen directo (la regla de CF no aplica por la cookie)
curl -sI -H 'Cookie: wordpress_logged_in_x=1' "https://example.com/post/?x=$RANDOM" \
  | grep -iE 'cache-control|x-litespeed|cf-cache-status'

# Con la regla de CF aplicando
curl -sI "https://example.com/post/?x=$RANDOM" \
  | grep -iE 'cache-control|x-litespeed|cf-cache-status'

Diagnóstico según lo que aparezca:

ObservaciónSignificado
El origen no devuelve Cache-Control, solo x-litespeed-cache-controlrespect_origin es inservible → usar override_origin con TTL explícito
Cache-Control: no-store aparece solo cuando la regla aplicaLo genera browser_ttl: bypass de la propia regla. Instruye al navegador, no al edge. No es la causa del problema de caché
x-litespeed-cache: miss siempre, incluso repitiendo la misma URLLa caché de página de LiteSpeed no sirve nunca (ver abajo)

El fix es dejar de depender del origen para el TTL del edge:

Cloudflare → Caching → Cache Rules → [tu regla de HTML]
  Edge TTL:    Override origin → 4 hours
  Browser TTL: Bypass   (correcto para HTML: el visitante siempre recibe contenido fresco)

Vía API, conservando el resto de la regla intacta:

curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE/rulesets/$RULESET" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"rules":[{"action":"set_cache_settings","description":"Cache HTML anon",
       "expression":"<tu expresion original>","enabled":true,
       "action_parameters":{"cache":true,"browser_ttl":{"mode":"bypass"},
       "edge_ttl":{"mode":"override_origin","default":14400}}}]}'

Exporta primero el ruleset actual (GET .../rulesets/phases/http_request_cache_settings/entrypoint) como respaldo: el PUT reemplaza el conjunto completo de reglas, no hace merge.

x-litespeed-cache: miss permanente es un problema aparte. Si el servidor responde miss en todas las peticiones —incluso pidiendo dos veces seguidas la misma URL contra el origen y en todos los tipos de página— la caché de página de LiteSpeed no está sirviendo, aunque el plugin esté activo y marque la página como cacheable. La causa habitual es que falten las reglas del servidor en el .htaccess:

<IfModule LiteSpeed>
    CacheLookup on
</IfModule>

Requiere acceso al servidor o al hosting; no es diagnosticable ni corregible desde la API de WordPress. Con Cloudflare bien configurado el impacto en usuarios queda absorbido, pero cada purga del CDN vuelve a costar un render PHP completo y el origen queda expuesto si el CDN falla.


Diagnosing cache issues

Check headers directly

# Check what cache headers a URL is returning
curl -I https://domain.com/page/ 2>&1 | grep -i "cache\|x-cache\|age\|etag\|last-modified"

Key headers to read:

HeaderValueMeaning
Cache-Control: no-store—Response will never be cached
X-Cache: HIT—CDN served from cache
X-Cache: MISS—CDN fetched from origin
Age: 3600secondsResponse has been cached for 3600s
CF-Cache-Status: DYNAMICCloudflareHTML not being cached
X-LiteSpeed-Cache: hitLiteSpeedServed from LiteSpeed cache

Common issues

Cache-Control: no-store on all HTML (PrestaShop default)

PrestaShop ships with no-store on all HTML responses to prevent caching of session-aware pages. Fix: enable CCC and Smarty cache (see PrestaShop section).

Cache-Control: private on anonymous pages

WordPress plugins or themes that set private prevent CDNs from caching pages that could be safely cached for anonymous visitors.

No cache headers at all

Old PHP applications or misconfigured servers may emit no Cache-Control header. Browsers default to heuristic caching (usually 10% of the Last-Modified age). Explicit headers are always better.

Cache headers correct but TTFB still high

CDN is configured but HTML is bypassing it (CF-Cache-Status: DYNAMIC). Check CDN rules — HTML may need an explicit "Cache Everything" rule.


Audit checklist

CRITICAL
[ ] HTML pages return Cache-Control header (not absent)
[ ] Cart, checkout, and account pages return Cache-Control: no-store
[ ] No Cache-Control: no-store on anonymous HTML pages
[ ] Sitemap does not return X-Robots-Tag: noindex (LiteSpeed issue)

HIGH
[ ] TTFB < 800ms on primary pages (PSI field data or curl)
[ ] Page caching active (WP Rocket, LiteSpeed, Smarty)
[ ] Static assets (CSS, JS, images) have long max-age (>1 year)
[ ] CDN configured and serving static assets (X-Cache: HIT or CF-Cache-Status: HIT)

MEDIUM
[ ] CDN caching HTML for anonymous visitors (not just assets)
[ ] OPcache active for PHP (check php.ini or phpinfo())
[ ] Redis/Memcached for WordPress object cache (high-traffic sites)
[ ] ETag configuration on multi-server setups (no inode-based ETags)
[ ] PrestaShop: CCC active with all options enabled

LOW
[ ] Cache-Control: immutable on versioned static assets
[ ] stale-while-revalidate configured for HTML responses
[ ] Cache purge on content publish (CDN invalidation)
[ ] Age header present on CDN responses (confirms caching is active)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.