=== Acid AEO — AI Readiness, Citations & Rich Data ===
Contributors: allexsava
Tags: ai, llms-txt, schema, seo, indexnow
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 8.0
Stable tag: 1.0.3
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Help ChatGPT, Perplexity and Google AI cite you: llms.txt, one AI crawler policy, rich data, IndexNow and a readiness score, set up in two minutes.

== Description ==

Search is turning into answering. ChatGPT, Perplexity, Claude, Gemini and Google AI Overviews read your site, summarise it and decide whether to cite you. Acid AEO gives them exactly what they need to understand and quote you, and gives you the controls, the score and the proof.

**Two minutes, no code.** Install, answer a short wizard, read your AI readiness score. Watch the whole run, from the directory install to the first audit:

https://www.youtube.com/watch?v=lrg1Qtqww4s

Documentation, from install to the developer reference: https://acidstudios.ro/acid-aeo/docs/

= What you get in two minutes =

* **A site AI assistants can read in one request.** `llms.txt` with your key pages, offerings and FAQ, plus `llms-full.txt` with the full text: the files answer engines look for first.
* **One AI crawler policy, applied everywhere.** Pick a preset from "citable" to "closed" or decide per crawler; the same policy is written to `robots.txt`, `ai.txt`, the `Content-Signal` header and `llms.txt`, so they can never disagree.
* **Rich data that does not fight your SEO plugin.** One JSON-LD graph (Organization, WebSite, WebPage, BreadcrumbList, Article, Person, FAQPage, Speakable). Yoast, Rank Math, SEOPress, AIOSEO, The SEO Framework and Slim SEO are detected, and the plugin steps aside for what they already emit.
* **Found faster.** IndexNow tells Bing and its partners the moment you publish.
* **Proof it works.** A readiness score from 0 to 100, the fixes that matter (most of them one click), and AI citation tracking: which crawlers fetched what, and which answer engines sent you visitors. No cookies, no stored IP addresses.

= Who it is for =

Site owners and agencies who want the AI-readiness stack done right without reading the specs. Developers who want every output filterable, a REST API and WP-CLI commands for the same operations.

= Why people trust it =

* One source of truth: identity and policy are set once in the wizard, and every file and every schema node is generated from it.
* Nothing leaves your server except IndexNow pings (on by default, one switch to turn off). No analytics, no phone-home, no external fonts.
* Honest by design: the audit reports what is actually served, not what is configured, and never overwrites a file you placed yourself.
* Free and open source (GPL), listed in the WordPress.org directory, with public documentation.

= Everything it generates =

**AI readiness files**

* `llms.txt` — a curated map of your site for language models, plus `llms-full.txt` with the full text.
* `humans.txt` — the people behind the site, with roles and profiles.
* `ai.txt` — machine readable usage terms for AI crawlers.
* `ai-context.json` — a compact JSON brief: who you are, what you offer, how to cite you.
* `.well-known/security.txt` — the RFC 9116 security contact.

**One AI crawler policy**

* Presets from "citable" to "closed", or per-bot control over every known AI crawler.
* Emitted consistently to `robots.txt`, `ai.txt`, the `Content-Signal` header and the policy section of `llms.txt`.
* Optional licence and permissions contact so publishers can find you.

**Rich data**

* A single JSON-LD `@graph`: Organization, WebSite, WebPage, BreadcrumbList, Article, Person, FAQPage, Speakable and image metadata.
* Detects Yoast, Rank Math, SEOPress, AIOSEO, The SEO Framework and Slim SEO, and steps aside for the pieces they already emit.

**Reach and measurement**

* IndexNow: a key, the key endpoint and automatic submission when you publish.
* AI citation tracking: which AI crawlers fetched what, and which answer engines sent you visitors — without cookies and without storing personal data.
* Content tools: TL;DR, FAQ and key-takeaway blocks, per-post summaries, "last updated" and Markdown for agents.
* An audit score that tells you exactly what is missing and fixes most of it for you.

== Installation ==

1. Upload the `acid-aeo` folder to `/wp-content/plugins/`, or install the ZIP through **Plugins → Add New → Upload Plugin**.
2. Activate the plugin through the **Plugins** screen.
3. The setup wizard opens automatically. It takes about three minutes.
4. Visit `/llms.txt` on your site to confirm the files are being served.

If your files return a 404, go to **Settings → Permalinks** and press **Save Changes** once to refresh the rewrite rules.

== Frequently Asked Questions ==

= Does it conflict with Yoast, Rank Math or another SEO plugin? =

No. Acid AEO detects the SEO plugins listed above and turns off the structured data pieces they already emit (Organization, WebSite, Breadcrumb, Open Graph and Twitter tags). What stays on is what SEO plugins do not do: FAQPage, Speakable, extended Person data, AI-specific files and the crawler policy. You can force any piece on or off yourself.

= Where are the files served from? =

They are virtual by default: `llms.txt`, `humans.txt`, `ai.txt`, `ai-context.json` and `security.txt` are produced on request through WordPress rewrite rules, so they are always current. If a real file with the same name already exists in your webroot, that file wins and the plugin tells you about it in the audit.

= There is already a robots.txt, llms.txt or ai.txt file on my server. What happens? =

The web server answers with the file on disk before WordPress runs, so the copy this plugin generates is never seen for that path. The audit reports it ("No robots.txt on disk", "No conflicting llms.txt on disk") rather than overwriting anything: a file the plugin did not write is never touched. Delete or rename the file and the generated one takes over immediately. `robots.txt` is a special case — the plugin never writes it to disk; it only filters the virtual one WordPress serves.

= I use a page cache. Will the citation tracking still work? =

Server-side tracking sees only the requests that reach PHP, so a page served from a full-page cache is invisible to it. The wizard detects the common cache plugins and offers the browser beacon: one small first-party script that reports a visit arriving from an AI answer to your own site's REST endpoint (`/wp-json/acid-aeo/v1/beacon`). It sends the page URL, the referrer and the `utm_source` parameter, once per page per browser session, and nothing else. No third party is involved.

= Does it slow the site down? =

No. Generated files are cached in transients and invalidated only when content or settings change. Front-end output is a single JSON-LD block plus a few header tags; there is no JavaScript on the front end unless you switch the optional citation beacon on.

= Is the citation tracking GDPR-safe? =

It is designed to be. No cookies are set, no personal data is stored: visitor IPs are hashed with a salt that rotates daily and truncated, and user agents are reduced to a bot name. Rows are deleted automatically after the retention period you choose.

= Can I write physical files instead of virtual ones? =

Yes. Switch **Static files** on and pick which of `llms.txt`, `llms-full.txt`, `humans.txt`, `ai.txt`, `ai-context.json` and `.well-known/security.txt` are written into your webroot through the WordPress filesystem API. You need this on hosts that answer `/*.txt` and `/.well-known/*` from disk and only fall through to PHP when the file is missing — a CDN, an edge cache or a static export in front of WordPress.

The writer is deliberately conservative. It never writes `robots.txt`, which belongs to WordPress core and to your SEO plugin. Every file it generates carries a marker (`generated by Acid AEO`, or the `"generator"` field in `ai-context.json`), and a file without that marker is never overwritten and never deleted — so an `llms.txt` that was already in your webroot stays exactly as it is. Writing needs the `direct` filesystem method: if WordPress would have to ask for FTP credentials, the audit says so instead of asking. The files are rewritten when the settings change, when you publish, and once a day. Each write also prunes: a file the plugin wrote that you have since unticked — or whose module you switched off — is deleted by the same pass, so your server stops answering with a copy the settings no longer stand behind. Switching the module off removes the ones the plugin wrote, and so does uninstalling it. See `docs/hosting-notes.md` for server and cache configuration.

= What is removed on uninstall? =

Deleting the plugin removes its settings, the events table, its cron events, the post and user meta it added, its transients and any file in the webroot that carries the plugin's own marker. Files it did not write are left alone. Tick **Keep my data on uninstall** under **Settings → Advanced** before deleting if you want to keep everything for a later reinstall.

= Does it work on multisite? =

Multisite: activate per site or network-wide; each site keeps its own settings, table and cron. A network activation prepares every site that already exists, and a site created afterwards is prepared as it is created.

= Is there a WP-CLI interface? =

Yes, every screen has a command behind it:

* `wp acid-aeo audit [--fresh] [--format=<table|json>]` — runs the readiness audit and exits with status 1 when the score is below 50, so a deployment pipeline can gate on it.
* `wp acid-aeo regenerate` — drops every cached file and rebuilds the rewrite rules.
* `wp acid-aeo static write|remove` — writes the generated files to disk, or removes the ones the plugin wrote.
* `wp acid-aeo indexnow submit [<url>...] [--all]` and `wp acid-aeo indexnow log`.
* `wp acid-aeo export [--file=<path>]` and `wp acid-aeo import <file>` — move settings between staging and production.
* `wp acid-aeo citations stats [--days=<n>]` and `wp acid-aeo citations purge [--older-than=<days>]`.
* `wp acid-aeo preview <llms|llms_full|robots|humans|ai_txt|ai_context|security>` — prints what a file would say right now, even when its module is switched off.

= How do I stop the “Updated on” line appearing on my posts? =

It is off until you switch it on. **Acid AEO -> Settings -> Content tools -> Show a “last updated” line on posts** controls it for the whole site; the setup wizard
offers the same switch in its writing tools step.

To keep the line but drop it from one page, tick **Hide the “last updated” line on this
post** in the AI readiness box in the editor sidebar. A theme can take it over entirely
with the `acid_aeo_last_updated_html` filter: return an empty string to remove the line,
or your own markup to replace it. `acid_aeo_last_updated_threshold` changes how long after
publishing a revision has to come before the line appears, in seconds; the default is one day.

= Are there any known limitations? =

Two worth knowing about in 1.0.0:

* **IndexNow does not read your SEO plugin's per-post noindex setting.** It stops at the site-wide **Discourage search engines** option, so a public site that has marked one individual post `noindex` in Yoast, Rank Math or another SEO plugin will still submit that post's URL to IndexNow. Until a later version reads the per-post flag, the way round it is to untick that post type under IndexNow, or to turn automatic submission off and submit by hand with `wp acid-aeo indexnow submit <url>`.
* **Static files need the `direct` filesystem method.** If your host makes WordPress ask for FTP or SSH credentials before writing, nothing can write the files — a scheduled task has nobody to ask. The audit reports it as a finding rather than prompting, and the virtual files keep working exactly as before; only the physical copies are unavailable.

== External services ==

The plugin talks to one third-party service. IndexNow is on by default; switch it off in the setup wizard's Discovery step or on the settings screen, and nothing is sent automatically (an explicit `wp acid-aeo indexnow submit` or `POST /indexnow/submit` still sends).

**IndexNow** (https://www.indexnow.org/). When the IndexNow module is enabled, the plugin sends an HTTPS POST to `https://api.indexnow.org/indexnow` containing your site's host name, the IndexNow key the plugin generated for your site, the URL of the key file and the list of URLs that were just published, updated, deleted, or that you chose to submit. It is sent when content changes (if automatic submission is on), when you press **Submit** in the admin or the CLI, and when you finish the setup wizard with **Submit every URL** ticked. Nothing else is transmitted — no visitor data, no content. IndexNow terms of use: https://www.indexnow.org/terms. IndexNow is operated by Microsoft; privacy statement: https://privacy.microsoft.com/privacystatement. The endpoint can be changed with the `acid_aeo_indexnow_endpoint` filter. Turn the IndexNow module off and no request is made automatically.

Everything else stays on your own server:

* The **readiness audit** fetches your own site's URLs (home page, `robots.txt`, `llms.txt` and the other generated files) with `wp_remote_get()`, as a crawler would, to check they are served correctly. These are loopback requests to your own domain.
* The optional **citation beacon** is a first-party script that posts to your own site's REST API, as described in the FAQ.
* The plugin does not contact Acid Studios, does not phone home, does not check for updates outside wordpress.org and does not load any remote script, font or image.

Two kinds of URL appear in the source without ever being requested. They are named here explicitly, because an automated scan reads the constant and not the paragraph:

* **`AiContextGenerator::SCHEMA_URL`** = `https://acidstudios.ro/schemas/ai-context/1.0.json`. This string is written into the `$schema` key of the `ai-context.json` the plugin generates, the way any JSON document names the schema it follows. It is never fetched, at generation time or later.
* **The `docs` URLs in `includes/Bots/Registry.php`** (23 of them, for example `https://developer.amazon.com/amazonbot` and `https://platform.openai.com/docs/bots`). These are link targets printed in the per-crawler settings table, for you to open in your own browser if you want to read what a crawler does. The plugin never requests them, and no crawler documentation is fetched, parsed or cached.

== Fonts ==

The admin screens use Inter (https://github.com/rsms/inter, v4.1) and Space Mono (https://github.com/googlefonts/spacemono), both under the SIL Open Font License 1.1. The files and their licences are in `assets/fonts/`. They are served from your own site and load only on Acid AEO's admin screens.

== Screenshots ==

1. Your AI readiness score right after setup: 93, and the handful of fixes that actually matter.
2. Install from the WordPress.org directory and activate: the setup wizard opens by itself.
3. The wizard: who you are, which AI crawlers may read you, what goes into llms.txt. Sensible defaults, most people press Next.
4. What AI engines get: llms.txt and a robots.txt with every AI crawler named, served from your own domain.
5. Setup wizard, step 1: what the plugin already found on the site — site type, language, SEO plugin, page cache and permalinks.
6. Setup wizard, step 3: the AI crawler policy, chosen from four presets or per crawler, with a live preview of the robots.txt it produces.
7. Setup wizard, last step: the files are published, the site is measured and every check is listed with its result.
8. The dashboard: readiness score, the fixes worth making, every module as a switch, AI citations for the last 30 days and links to each generated file.
9. Settings, Identity tab: the single source of truth every file is generated from — site type, language, schema type, name, description and tagline, with contact and location further down.
10. AI citations: totals and a timeline, crawlers and answer engines by name, the most cited pages and the recent events.
11. The setup wizard from start to finish.

== Changelog ==


= 1.0.3 =
Released 2026-10-08.

* Changed: the admin screens and the setup wizard are redrawn in the Acid Studios light style: Inter and Space Mono, black on white, one green used as a fill under black text. Nothing was moved and no setting changed.
* Changed: two typefaces now ship with the plugin (Inter and Space Mono, SIL Open Font License 1.1, in `assets/fonts/` with their licences). They load on the plugin's own admin screens only; nothing is requested from another server.
* Fixed: text that was white on green (the mark, the step numbers, the yes/no buttons, the success message) did not meet WCAG AA contrast. It is black on green now.
* Fixed: a switch that is off was drawn too faintly to see against the page.
* Fixed: in a right-to-left language the wizard's switches slid the wrong way.
* Fixed: the wizard's yes/no buttons showed their radio circles on top of the labels.
* Fixed: at phone width the settings screen, the crawler table and the Tools screen scrolled sideways.
* Fixed: the "AI readiness" widget on the WordPress dashboard showed its score as plain text.

= 1.0.2 =
Released 2026-09-18.

* Changed: the line crediting Acid Studios in humans.txt is **off by default**. It was on, which put a link to the plugin author's site in a public file on every install that did not go looking for the switch. The switch is still in the wizard and in Settings for anyone who wants to leave it on.
* Fixed: on a site given its own directory for WordPress — core's supported layout, where `siteurl` is `home` plus a folder — the optional static files were written into the WordPress folder while every address the plugin publishes pointed at the folder above it. The files were invisible, and the audit then reported them missing. They are now written where the site is actually served from, and an uninstall removes them from there.
* Fixed: on a multisite network, switching the static files on from a site other than the main one overwrote the main site's llms.txt, humans.txt and ai-context.json with that site's content, published at the network's address. Network sites now decline to write to the shared directory and say why; virtual delivery is unchanged, so every site still answers its own files.
* Fixed: the `Allow:` lines in robots.txt named `/llms.txt` and its neighbours even on sites where those addresses do not answer — with plain permalinks the files are served through a query variable, and on a site in a subdirectory every path carries that directory. The lines are now taken from the addresses the files are actually served at.
* Fixed: the audit's loopback requests called the core `https_local_ssl_verify` filter with one argument where core passes two. A site with a callback written to core's documented signature took a fatal error on every audit.
* Fixed: the sitemap probe accepted any HTTP 200 as a sitemap, so a host that answers unknown paths with the home page had an HTML page stored and then advertised to every crawler as its sitemap. The answer now has to read as a sitemap.
* Fixed: a fresh audit settled which sitemap answers *before* dropping its cached responses, so the probe read answers up to ten minutes old and then fetched every address a second time. It now happens after, once, inside the run.
* Fixed: a summary taken from a post's excerpt was published with the page builder's shortcodes still in it, while the same text taken from the content had them removed.
* Fixed: the headings the AI blocks print — "In short", "Frequently asked questions", "Key takeaways" — were English on every site, whatever its language, because the defaults were never translated. They now follow the site language like the rest of the interface.
* Added: the `acid_aeo_llms_full_documents` filter. llms-full.txt considered at most two hundred documents with no way to change it and nothing saying so; the byte budget beside it was already a setting.
* Fixed: the audit read authors from `post` alone, so a site whose content is entirely in a custom post type was told every author profile was empty. It now reads the post types the plugin publishes; the same goes for the key pages fallback, which now recognises every page-like post type, and for the published counts behind the wizard's guess at what kind of site this is.
* Fixed: the **Documentation** link on the Plugins screen pointed at a page that did not exist; it now opens the plugin's page on acidstudios.ro.
* Changed: the readme now names, one by one, the URLs that appear in the source without ever being requested, so an automated scan can match the constant it reads against a sentence about that constant.
* Changed: `Contributors` now lists the wordpress.org account that owns the plugin.
* Fixed: uninstalling now also removes the per-post "hide the last updated line" flag introduced in 1.0.1, instead of leaving it behind in the database.
* Changed: the readiness audit's loopback requests now respect the `https_local_ssl_verify` filter, the same switch WordPress core uses for its own loopbacks.
* Fixed: on a site running Yoast SEO, robots.txt named the same sitemap twice. The AI crawler block is now the last thing added to the file, so the line another plugin contributes is seen and not repeated.
* Fixed: on a site built with a page builder — WPBakery, Divi, Elementor — llms.txt, llms-full.txt and the Markdown endpoint printed the builder's own shortcodes instead of the text inside them. The tags are now removed whether or not the builder registered them for the request, and the prose they wrap is kept.
* Fixed: a page kept out of search by the site's SEO plugin was still advertised in llms.txt and ai-context.json. The `noindex` set in Yoast SEO, Rank Math, SEOPress, All in One SEO, The SEO Framework or Slim SEO is now read, each in that plugin's own vocabulary, and the page is left out. The `acid_aeo_is_noindex` filter answers for anything else.
* Fixed: on a WooCommerce store the generated files led with Cart, Checkout and My account — three pages that read as a form to anyone who fetches them. They are left out; the shop page, which is the catalogue, stays.
* Fixed: a page built with Elementor, Bricks or Oxygen was published as a bare link, because those builders keep the content in post meta and leave `post_content` empty. The rendered content is read instead, so the page gets a summary like any other.
* Fixed: the audit reported llms.txt as "written by something else" on most sites — it looked for the signature in the first two kilobytes, and llms.txt signs its last line. It now asks the same question, of both ends of the file, as the module that writes it.
* Fixed: the sitemap address the files point at was worked out from which SEO plugin is installed, which is not the same question — Rank Math, SEOPress and All in One SEO ship the sitemap as a module that can be off, and The SEO Framework and Slim SEO answer at their own address while core's stops. The audit now settles it by asking the site, and robots.txt adds no sitemap line when another plugin has already written one.

= 1.0.1 =
Released 2026-09-17.

* Fixed: the “Updated on” line gave its `<time datetime>` attribute in UTC while printing the visible date in the site's own timezone, so the two could name different days on any site that is not on UTC. Both halves are now the site's local time, and the visible date is localised rather than always English.
* Added: **Hide the “last updated” line on this post**, a per-post checkbox in the AI readiness box, for the pages the site-wide line does not suit.
* Added: the `acid_aeo_last_updated_html` and `acid_aeo_last_updated_threshold` filters, so a theme can move, restyle, replace or remove the line and choose how old a revision has to be.

= 1.0.0 =
Released 2026-09-15.

* Initial release: AI crawler policy (robots.txt, ai.txt, Content-Signal), llms.txt and llms-full.txt, humans.txt, ai-context.json, security.txt, JSON-LD rich data, AI head tags and HTTP headers, IndexNow, AI citation tracking, content tools with Markdown for agents, optional static files, the readiness audit, the setup wizard, a REST API and WP-CLI commands.

== Upgrade Notice ==

= 1.0.3 =
A visual refresh of the admin screens. No settings change.

= 1.0.2 =
Compatibility with page builders, WooCommerce, SEO plugins and multisite. The humans.txt credit is off by default (a saved choice is kept). With WordPress in its own directory, re-run the audit: static files went to the wrong folder.

= 1.0.1 =
Fixes the “Updated on” line, whose machine-readable date could be a day behind the one readers saw. Adds a per-post way to hide it and two filters for themes.

= 1.0.0 =
First release. After activating, run the setup wizard once; it takes about three minutes and publishes every file.
