Popcorn

Free site checks: audit, citations, AI crawlers

Three checks that call no model and cost nothing, the site audit of what AI crawlers can read, the citation check of which cited pages name you, and AI crawler counts from your access logs.

Can AI assistants read my site? popcorn audit checks what an AI crawler sees on each of your sites: whether robots.txt or a firewall blocks it, whether there’s an llms.txt and a fresh sitemap, and whether each page has its content in the HTML or is empty without JavaScript. Two more checks sit beside it: the citation check, of which pages the answers cite name you and which name only competitors, and AI crawler counts from your own access logs.

None of the three calls a model. They’re plain HTTP requests and your own logs, so they cost nothing, on any setup, and don’t count against the daily budget.

Site audit

popcorn audit                 # every site in [[brand.sites]] (and brand.domains)
popcorn audit example.com     # just one, which needn't be yours (not kept)
popcorn audit -pages 20 -info # more sitemap pages; list info findings too
popcorn audit -json           # the findings as JSON

Also Site audit in the web UI (/audit, “Audit now”), POST /api/audit to run one and GET /api/audit for the latest, and the MCP tool audit_sites.

For each site it fetches robots.txt, the home page, /llms.txt, the sitemap and up to 10 pages from the sitemap, spread through it. It fetches as Mozilla/5.0 (compatible; Popcorn-Audit/<version>; +https://popcorn.zone), 4 requests at a time, with a 15-second timeout and a 2 MB cap per page, and reads the raw HTML, as a crawler that runs no JavaScript does.

  • A host/path site (github.com/example) gets its robots.txt rules checked for that path, but no llms.txt or sitemap check: those belong to the host.
  • Sites of kind code or community don’t get their markup checked. It isn’t yours to fix.
  • The newest audit of your own sites is kept. If it’s less than 30 days old, its errors and warnings go to the analyst with the next fix list, so a fix can say “this page is empty without JavaScript” rather than only “this page wasn’t cited”.

Each finding has a severity, a check id, what it found and a fix.

Crawling rules

checkseveritywhat it meansfix
robots.blocks-ai-searcherrorrobots.txt blocks an assistant’s search or user fetcher (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User)allow it: User-agent: <bot> and Allow: /
robots.blocks-search-engineerrorrobots.txt blocks Googlebot or Bingbot, whose indexes Gemini, ChatGPT and Copilot searchallow it the same way
robots.blocks-traininginforobots.txt blocks a training crawler (GPTBot, ClaudeBot, anthropic-ai, Google-Extended, Applebot-Extended, CCBot, Meta-ExternalAgent, Bytespider)nothing, unless you want future models to learn from these pages. It keeps pages out of training data, not out of search answers
robots.server-errorerrorrobots.txt answers 5xx, which crawlers read as “fetch nothing”serve it with a 200, or a 404 if you don’t want one
robots.unreachablewarningrobots.txt couldn’t be fetched at allmake sure /robots.txt answers quickly over HTTPS
robots.missinginfono robots.txt: everything is allowedoptional; add one to point crawlers at your sitemap
waf.blocks-ai-boterrorthe home page answers 401, 403 or 429, or a challenge page, to an AI fetcher’s user agent while a browser’s gets 200allow the bot in your firewall or CDN bot settings (on Cloudflare, Security > Bots, AI Crawl Control)

The firewall check only sends each bot’s user agent, from Popcorn’s address. A firewall that lets verified bots through by IP may block this check and still admit the real crawler. Confirm in the firewall’s log, or with the crawler counts below, before changing rules.

llms.txt and the sitemap

checkseveritywhat it meansfix
llms.missingwarningno /llms.txt, or a catch-all route serving HTML therepublish one: # <site name>, a one-line summary in a > quote, then sections of [title](url): description links to your key pages
llms.content-typewarningllms.txt isn’t served as text/plain or text/markdownserve it as one of those
llms.headingwarningllms.txt doesn’t start with #start it with # <site name>
llms.no-linkswarningllms.txt has no markdown linkslist your key pages as [title](url) links
sitemap.missingwarningno sitemap in robots.txt and none at /sitemap.xmlpublish /sitemap.xml with each page’s <lastmod>, and add Sitemap: <url> to robots.txt
sitemap.invaliderrorthe sitemap doesn’t parseserve valid sitemap XML: a <urlset> of <url><loc> entries, or a <sitemapindex>
sitemap.emptywarningit lists no pageslist every public page
sitemap.stalewarningits newest <lastmod> is over 180 days oldregenerate it when pages change, with each page’s real <lastmod>
sitemap.no-lastmodinfoit gives no <lastmod> datesadd <lastmod> to each <url>, from the page’s real last change

Pages

checkseveritywhat it meansfix
page.unreachableerrorthe page couldn’t be fetchedmake sure it answers over HTTPS within a few seconds
page.statuserrorthe page didn’t answer 200serve it with a 200, or drop it from the sitemap if it’s gone
page.challengeerrorthe page served a bot challenge instead of its contentlet crawlers and plain HTTP clients through to public pages
page.needs-jserrorunder 200 characters of text in the HTML and an app mount point (#root, #app, #__next…): empty without JavaScriptrender on the server or pre-render at build time (SSR or SSG)
page.thinwarningunder 200 characters of text in the HTMLput the content in the HTML, not loaded by JavaScript afterwards
page.noindexerrora robots meta tag or X-Robots-Tag says noindexremove noindex from public pages
page.jsonld-invaliderrora JSON-LD block doesn’t parsefix the JSON in each <script type="application/ld+json">
page.jsonld-missingwarningno JSON-LDadd schema.org JSON-LD: Organization and WebSite on the home page, SoftwareApplication or Product for the product, Article or TechArticle for guides, FAQPage for FAQs
page.title-missingwarningno <title>give it a title in the words buyers search with
page.title-longwarninga <title> over 60 charactersshorten it, key words first
page.description-missingwarningno meta descriptionadd a 50–160 character summary
page.description-lengthwarninga meta description outside 50–160 charactersrewrite it to 50–160 characters
page.h1warningnot exactly one <h1>give it exactly one
page.langwarningno lang on <html>add lang="en", or the page’s language
page.canonicalwarningno canonical linkadd <link rel="canonical" href="<its URL>">
page.open-graphwarningno og:title or og:descriptionadd both

A site listed in a form Popcorn can’t read gets site.invalid (error): list it as a host (example.com) or a host and path (github.com/example).

Citation check

popcorn check-citations <run-id>         # the 25 most-cited third-party pages
popcorn check-citations -n 50 <run-id>

Also on the Site audit page (“Check citations”, for any finished run), POST /api/runs/{id}/citation-check (?n=, default 25) to run one and GET for the run’s latest, and the MCP tool check_citations.

It takes the run’s third-party pages, most cited first, then most surfaced in searches, and fetches each one as Popcorn, where its robots.txt allows Popcorn-Audit. Each page is fetched at most once a day and kept for a month. It reads the page’s visible text and says whether it names your brand or an alias, and which of the set’s competitors.

A page that names competitors but not you is marked outreach: the answers read it, and it leaves you out. That’s the same idea as the run page’s Cited without your brand list, checked against the page itself rather than the answers.

Matching is by whole word, ignoring case. That’s stricter than the grader’s own check, so a short alias doesn’t match inside another word.

AI crawlers

popcorn crawl import access.log access.log.1.gz        # nginx, Apache
popcorn crawl import -host docs.example.com docs.log   # the site, for lines that don't record one
popcorn crawl import cloudflare-*.json caddy.json -    # - reads stdin
popcorn crawl report -days 30

Also AI crawlers in the web UI (/crawlers), GET /api/crawlers?days=30, and the MCP tool get_crawlers.

It reads:

  • common and combined log format, the nginx and Apache defaults, with or without a leading virtual host;
  • Caddy’s JSON access log;
  • Cloudflare Logpush JSON (ClientRequestUserAgent, ClientRequestURI, ClientRequestHost, EdgeStartTimestamp, ClientIP, EdgeResponseStatus, ClientRequestReferer).

Lines with no host (common and combined) count under -host, or your first site. Requests for CSS, scripts, images and fonts are skipped. Gzipped logs are read as they are.

Each request is classed by its user agent:

classwhatbots
assistantfetched because someone askedChatGPT-User, Claude-User, Perplexity-User, MistralAI-User
AI searchbuilding an assistant’s search indexOAI-SearchBot, Claude-SearchBot, PerplexityBot, DuckAssistBot
trainingcollecting training dataGPTBot, ClaudeBot, anthropic-ai, CCBot, Meta-ExternalAgent, Bytespider, Amazonbot, cohere-ai
enginea web search engine some assistants search throughGooglebot, Bingbot
AI referrala person, arriving from an assistant’s answera Referer from chatgpt.com, chat.openai.com, claude.ai, perplexity.ai, copilot.microsoft.com or gemini.google.com, or utm_source= one of them

Unverified: user agent only. Anyone can send a crawler’s user agent, and nothing here checks the request’s IP against the operator’s published ranges. Read the counts as what the logs claim.

Popcorn keeps only daily counts per host, page, class and bot, not log lines, plus a hash of each counted request, so importing the same log twice, or one that overlaps it, counts nothing twice.

The page lists each of your pages’ fetches by class, and AI referrals, beside how often the latest finished run of each set cited and surfaced it, and flags:

  • crawled, never cited: AI crawlers fetched it, but no answer in the latest runs cited it;
  • cited, never crawled by search bots: answers cite it, but no assistant or AI search crawler fetched it in the window. Only for hosts your logs cover.

Where to start

Run popcorn audit before your first paid run. It’s free, it takes a minute, and an error there, such as a firewall that blocks ChatGPT-User or a docs site that’s empty without JavaScript, explains a lot of “never cited” before you’ve asked a single question. Then read Reading results for what a run adds.