Static files, caching & hosting
Virtual files versus files on disk, what a page cache or CDN changes, and the server rules that keep llms.txt, robots.txt and the headers reachable.
Updated 2026-10-08 · applies to Acid AEO 1.0.4
On a stock Apache or Nginx host there is nothing to configure. This page covers the three places where that stops being true: a web server that answers before WordPress does, a page cache, and a CDN.
Virtual files by default
Acid AEO serves /llms.txt, /llms-full.txt, /humans.txt, /ai.txt, /ai-context.json, /.well-known/security.txt (also at /security.txt) and the IndexNow key file through WordPress rewrite rules. Nothing is written to disk, so the content never goes stale. /robots.txt is written by WordPress itself and filtered by the plugin. Files are sent with Cache-Control: public, max-age=3600.
With pretty permalinks off, the files are only reachable as /?acid_aeo_file={slug}. That is a fallback, and the audit fails Pretty permalinks.
When a file on disk wins
A real robots.txt, llms.txt or ai.txt in the site root is answered by the web server before WordPress runs, so the generated version is never seen. For robots.txt and llms.txt the audit reports it as No robots.txt on disk or No conflicting llms.txt on disk. It never overwrites or deletes a file it did not write.
The Static files module goes the other way: it writes real copies of llms.txt, llms-full.txt, humans.txt, ai.txt, ai-context.json and .well-known/security.txt into the web root, through the WordPress filesystem API. It never writes robots.txt. Details are under Static files on the Modules page.
Nginx
The standard WordPress block already sends unknown paths to PHP:
location / {
try_files $uri $uri/ /index.php?$args;
}
Two things can break it. First, a .well-known block that returns 404 on its own, as installed with many Let's Encrypt setups:
location ^~ /.well-known/ {
root /var/www/letsencrypt;
try_files $uri =404;
}
Keep the ACME challenge on its own path and send the rest to WordPress:
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
try_files $uri =404;
}
location ^~ /.well-known/ {
try_files $uri $uri/ /index.php?$args;
}
Second, a static-file location such as location ~* \.(txt|json|xml)$ { expires 30d; } may end in a 404. Drop txt and json from it, or name the plugin's files before it:
location ~ ^/(llms|llms-full|humans|ai)\.txt$ {
try_files $uri /index.php?$args;
}
location = /ai-context.json {
try_files $uri /index.php?$args;
}
Apache
WordPress' own .htaccess already routes anything that is not a real file to index.php:
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
AllowOverride must permit mod_rewrite for the site directory. Some hardening rules deny dot-prefixed paths: if /.well-known/security.txt returns 403, look for a <FilesMatch "^\."> or RedirectMatch 404 /\..*$ rule and exclude .well-known.
Page caches
A page cache may store the generated files, which is fine while it drops them when content changes. If in doubt, exclude these paths. The plugin already caches them internally, so excluding costs almost nothing:
/llms.txt
/llms-full.txt
/humans.txt
/ai.txt
/ai-context.json
/security.txt
/.well-known/security.txt
/robots.txt
Also exclude, or vary on, the query strings ?format=markdown and ?acid_aeo_file=. The first returns a page as Markdown under the page's own URL, and a cache that ignores query strings would show that Markdown to the next human visitor.
The Link and Content-Signal headers are sent by WordPress. A cache that serves pages without running PHP, or a proxy that strips headers, drops them, and the audit warns on Link header points at llms.txt or Content-Signal header. Add them at the server instead (Apache shown):
<IfModule mod_headers.c>
Header add Link "<https://example.com/llms.txt>; rel=\"alternate\"; type=\"text/plain\""
Header set Content-Signal "search=yes, ai-input=yes, ai-train=no"
</IfModule>
Both values are examples: use your own URL and the signal from your policy. Header add keeps the other Link headers WordPress sends.
The same cache hides AI referrals from server-side citation tracking. Switch on the browser beacon (the audit offers it as a fix on Tracking survives the page cache). In WP Rocket, add the paths to Advanced Rules → Never Cache URL(s). With W3 Total Cache, WP Super Cache or LiteSpeed Cache, use their "never cache" fields. LiteSpeed also needs format in its query-string list. Markdown negotiation by Accept header does not work behind a full-page cache. See the Developers page for the setting.
Cloudflare and other CDNs
Cloudflare does not cache text/plain or application/json by default, so the files already pass through. If a rule caches everything, add Cache Rules → Bypass cache with this expression:
(http.request.uri.path in {"/llms.txt" "/llms-full.txt" "/humans.txt" "/ai.txt" "/ai-context.json" "/robots.txt" "/security.txt" "/.well-known/security.txt"}) or (http.request.uri.query contains "format=markdown")
To cache them, use a short edge TTL (an hour matches the plugin's own header) and purge after wp acid-aeo regenerate. Never cache the IndexNow key file (/{key}.txt) blindly: it must answer for the stored key only.
Hosts that serve .well-known from disk
Some managed hosts and static-export setups answer /.well-known/* and /*.txt from the filesystem and fall through to PHP only when the file is missing. There, a virtual file does not exist and the URL returns 404 whatever the rewrite rules say. Switch on Static files, pick the files, and run:
wp acid-aeo static write # publish the files
wp acid-aeo static remove # take them off disk again
The writer is conservative:
- Each file carries a marker (
generated by Acid AEO, or the"generator"field inai-context.json). A file without it is never overwritten or deleted. - It needs the
directfilesystem method. Otherwise the audit failsFilesystem accessand the virtual files keep working. - Saving settings or publishing schedules one write a minute later; the daily task rewrites everything.
- A file the plugin wrote and no longer wants is deleted. Switching the module off or uninstalling removes only marked files.
Real files are answered before WordPress, so a settings change shows only after the next write. wp acid-aeo static write forces one.
Checking it works
curl -sI https://example.com/llms.txt | head -n 1
curl -sI https://example.com/.well-known/security.txt | head -n 1
curl -sI https://example.com/ | grep -iE '^(link|content-signal):'
wp acid-aeo audit --fresh
Expect 200 on the files and both headers on the home page. The audit fetches every published file itself and names the one that did not return 200. See the Readiness audit page for the output, and Troubleshooting if a file does not answer.
Found a mistake or something missing? Write to [email protected].