Skip to content
DeutschBook an intro call

How-to · Websites & technical SEO

JavaScript and AI crawlers: how to check what ChatGPT sees

In short

According to measurements published so far, ChatGPT reads only the HTML your server delivers when it fetches a page. Anything JavaScript creates in the browser is missing. Three checks show whether that affects you: Is your key sentence in the raw HTML, which text exists only in the rendered DOM, and do robots.txt and your firewall let crawlers through?

Key takeaways

  • A log analysis from September 2026 found no scripts in 37,045 ChatGPT-User page requests, on a single news site.
  • OpenAI’s and Perplexity’s crawler docs don’t mention JavaScript. Conflicting reports on OAI-SearchBot and GPTBot date from September 2026.
  • Count a key sentence in the raw HTML: 0 means it’s missing or the request failed. On Windows, Select-String trips over non-ASCII text.
  • Rendering, robots.txt and firewall problems look alike. A request as a crawler and robots.txt separate them; only logs show IP blocks.
  • On 16 pages of datafalke.de on Nov 9, 2026, all text was in the raw HTML, and GPTBot’s user agent got the same response as a browser.

Which AI systems run JavaScript?

Googlebot and Applebot render JavaScript. The crawlers from OpenAI, Anthropic and Perplexity almost never ran it in measurements through September 2026; in one analysis of 37,045 page requests, ChatGPT-User fetched not a single script. OpenAI and Perplexity don’t address this in their crawler documentation.

A crawler is a program that fetches pages automatically. Rendering here means that it runs a page’s scripts and reads the result. The evidence is thin and varies in reliability, so the table gives the type of source and its date for each system. The numbers in the “Source” column refer to the list of sources at the end of this article.

Do AI systems run JavaScript? Evidence by type and date, as of Nov 9, 2026
SystemJavaScriptEvidence: type and dateSource
ChatGPT-User (fetch triggered by a question)noMeasurement, Dec 17, 2024; logs of a news site through Sep 24, 2026: 37,045 page requests, 0 scripts1, 2
OAI-SearchBot, GPTBotmostly noMeasurement, Dec 17, 2024; logs through Sep 24, 2026: OAI-SearchBot loaded 1 of 1,738 pages the way a browser does; report by a site operator, Sep 30, 2026: both have rendered that site’s pages since Sep 25, 2026, IP addresses checked1, 2
ClaudeBotnoMeasurement, Dec 17, 2024; logs through Sep 24, 2026: 915 page requests, 0 scripts1, 2
Claude-User (fetch triggered by a question)unclearLogs through Sep 24, 2026: 10,028 page requests, 0 scripts; Anthropic’s developer documentation for its web fetch tool: no support for pages rendered with JavaScript; overview, as of Nov 3, 2026: not measured; crawler documentation: no statement2, 3, 4, 5
PerplexityBotmostly noMeasurement, Dec 17, 2024; logs through Sep 24, 2026: 3,193 page requests, 0 scripts; test from October 2025: a price inserted by JavaScript appeared in an answer1, 2, 3
Googlebot, AI Overviews, AI ModeyesGoogle’s documentation, Mar 4, 2026: Chromium renders the page, sometimes with a delay; the AI features require an indexed page (Dec 10, 2025)6, 7
Gemini when fetching a URLunclearMeasurement, Dec 17, 2024: Gemini uses Googlebot’s infrastructure; test on one page, Sep 25, 2026: only the text from the HTML1, 2
Applebotyes, in a browserApple’s documentation, Sep 4, 2026; logs through Sep 24, 2026: 4,838 page requests, 3,133 scripts2, 8
Bingbotcapable, rarely observedBing announcement, Oct 9, 2019: rendering with Edge; logs through Sep 24, 2026: scripts on about one in 800 pages2, 9

The server logs come from a single news site. Requests there were counted by user agent, the name a program uses to identify itself to the server, and the analysis comes from a vendor of log tools (Forge AI Bot Log, Sep 24, 2026). The 2024 measurement was published by the cloud platform Vercel with MERJ; Microsoft Copilot is missing from it because it doesn’t use a user agent of its own (Vercel and MERJ). The overview comes from searchVIU, a vendor of SEO tools (searchVIU, Nov 3, 2026). We haven’t found a newer measurement of comparable size. JavaScript isn’t mentioned in the crawler documentation from OpenAI or Perplexity. We also found no statement from OpenAI on the reports that have appeared since September 2026.

Why it matters: Whatever only appears in the browser is missing for systems that don’t run JavaScript. On the news site, ChatGPT-User fetched a page with fuel prices 157 times in 30 days, and Claude-User 80 times. Neither saw a price, because scripts inserted the prices. Googlebot does render JavaScript, so the gap often goes unnoticed in Google Search. How Google handles this is covered in our guide to technical SEO, in the section Can Google see content that JavaScript generates?

Agents that drive a real browser do run scripts. In your logs, they often look like ordinary visitors using Chrome (Forge AI Bot Log). This how-to doesn’t apply to such agents.

Done when you know which of these systems matter for your customers.

Is your key sentence in the raw HTML?

Count a key sentence from your page in the raw HTML, meaning the text the server delivers before any JavaScript runs. On macOS and Linux, you can do this with curl and grep; on Windows, with curl.exe and Select-String. A 1 or higher means the sentence is in the HTML. A 0 means it isn’t there or the request failed.

Why: A crawler without JavaScript gets exactly this raw HTML. Your browser shows the finished page after all scripts have run, which is why you won’t spot a gap while browsing. The check has three steps: this request first, then the comparison with the browser, and finally finding the cause.

How to do it:

  1. In your browser, copy a sentence your customers should read, ideally one that names your service and location. If you can, pick a stretch without umlauts, accented letters or typographic apostrophes.
  2. Put the URL and the sentence into the command for your system.
  3. Read the output: 1 or more means the sentence is in the raw HTML. grep counts lines, not matches. If you get 0, first check whether the request worked (the third step of this check).
Terminal on macOS and Linux: count the key sentence in the raw HTML
curl -sL https://www.example.com/services/ | grep -c "Your key sentence"
PowerShell on Windows: count the key sentence in the raw HTML
(curl.exe -sL https://www.example.com/services/ | Select-String -SimpleMatch "Your key sentence").Count
(Invoke-WebRequest -UseBasicParsing -Uri https://www.example.com/services/).Content.Contains("Your key sentence")

The first PowerShell line prints a number, the second True or False. Windows 10 and 11 ship with curl (curl project). Type curl.exe there, because curl alone is an alias for Invoke-WebRequest in PowerShell 5.1 (Microsoft on Invoke-WebRequest). That cmdlet’s -UseBasicParsing switch has been necessary since a security update on December 9, 2025. Without it, PowerShell asks on every request whether script code from the page may run.

Common mistakes:

  • Non-ASCII characters on Windows. With curl.exe and Select-String, the search found nothing in our test on November 9, 2026, as soon as the sentence contained a German umlaut, an accented letter or a typographic apostrophe. The console reads the response with an older character set (code page 850 on our German Windows machine). The Invoke-WebRequest line found the text. If you want to stick with curl.exe, first set [Console]::OutputEncoding = [Text.Encoding]::UTF8.
  • Line break in the source. If the sentence is split across two lines in the HTML, grep won’t find it. Use three or four words, or the script in the next section.
  • Text only in data. If the sentence appears only in a data block, such as JSON for the application, grep counts it even though it isn’t on the page without JavaScript. Vercel and MERJ consider it possible that AI models still make use of such data. Don’t rely on it.

Example: On a test page of our own built entirely on the client side, grep -c returned 0. On the static version of the same page, it returned 1.

Done when you know for every important page whether its key sentence is in the raw HTML.

Which text exists only in the rendered DOM?

Text that exists only in the rendered DOM is created by JavaScript in the browser. The DOM is the structure of a page after all its scripts have run. Compare it with the raw HTML: The difference shows the text blocks that a crawler without JavaScript doesn’t get. Chrome and Edge print the DOM with the --dump-dom flag.

Our script compares both versions for the whole page and lists every text block that exists only in the DOM. The --dump-dom flag prints the page after Chrome has run its scripts (Chrome documentation). With --virtual-time-budget, content that loads later gets eight seconds of virtual time.

How to do it: Save the script as raw-vs-dom.py. You need Python 3 and Chrome or Edge. Run it with python on Windows and python3 on macOS and Linux, followed by the URL and your key sentence. Our code examples are available under the MIT-0 license.

raw-vs-dom.py: compare raw HTML and rendered DOM
#!/usr/bin/env python3
"""Compares the text in the delivered HTML with the text after JavaScript has run.

Usage: python3 raw-vs-dom.py https://www.example.com/page/ "a sentence from your page"

Requires Python 3 and Chrome or Edge. License: MIT-0 (datafalke.de).
"""
import html.parser
import shutil
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request

BROWSERS = [
    r"C:\Program Files\Google\Chrome\Application\chrome.exe",
    r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
    "google-chrome", "chromium", "chromium-browser",
]
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
NO_TEXT = {"title", "script", "style", "template"}
BLOCK_TAGS = {"p", "li", "h1", "h2", "h3", "h4", "h5", "h6", "td", "th", "div", "section", "main",
              "article", "header", "footer", "nav", "aside", "form", "tr", "pre", "blockquote", "figcaption"}


class Text(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.skipping, self.blocks, self.scripts, self.data_blocks = 0, [[]], 0, 0

    def handle_starttag(self, tag, attrs):
        if tag == "script":
            kind = dict(attrs).get("type") or ""
            if kind in ("", "text/javascript", "application/javascript", "module"):
                self.scripts += 1
            else:
                self.data_blocks += 1
        if tag in NO_TEXT:
            self.skipping += 1
        if tag in BLOCK_TAGS:
            self.blocks.append([])

    def handle_endtag(self, tag):
        if tag in NO_TEXT and self.skipping:
            self.skipping -= 1

    def handle_data(self, data):
        if not self.skipping:
            self.blocks[-1].append(data)


def normalize(text):
    return " ".join(text.replace("\u00ad", "").split())


def parse(source):
    text = Text()
    text.feed(source)
    blocks = [normalize("".join(block)) for block in text.blocks]
    return [block for block in blocks if block], text.scripts, text.data_blocks


def raw(url):
    request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
    try:
        response = urllib.request.urlopen(request, timeout=30)
    except urllib.error.HTTPError as error:
        response = error
    charset = response.headers.get_content_charset() or "utf-8"
    return response.getcode(), response.read().decode(charset, "replace")


def dom(url):
    browser = next(filter(None, map(shutil.which, BROWSERS)), None)
    if browser is None:
        sys.exit("Chrome or Edge not found: add the path to BROWSERS.")
    profile = tempfile.mkdtemp()
    try:
        result = subprocess.run(
            [browser, "--headless", f"--user-data-dir={profile}", "--virtual-time-budget=8000", "--dump-dom", url],
            capture_output=True, timeout=120)
    finally:
        shutil.rmtree(profile, ignore_errors=True)
    return result.stdout.decode("utf-8", "replace")


def main():
    url = sys.argv[1]
    sentence = normalize(sys.argv[2]) if len(sys.argv) > 2 else None
    status, source = raw(url)
    raw_blocks, scripts, data_blocks = parse(source)
    dom_blocks, _, _ = parse(dom(url))
    raw_text, dom_text = " ".join(raw_blocks), " ".join(dom_blocks)
    only_in_dom = [block for block in dom_blocks if block not in raw_text]
    print("Status:      ", status)
    print("Scripts:     ", scripts, "executable,", data_blocks, "data block" if data_blocks == 1 else "data blocks", "(such as JSON-LD)")
    print("Words:       ", len(raw_text.split()), "in raw HTML,", len(dom_text.split()), "in DOM")
    if sentence:
        print("Key sentence:", "raw HTML", "yes" if sentence in raw_text else "no",
              "| DOM", "yes" if sentence in dom_text else "no")
    print("Only in DOM: ", len(only_in_dom), "text block" if len(only_in_dom) == 1 else "text blocks")
    for block in only_in_dom[:5]:
        print("  -", block[:90])


if __name__ == "__main__":
    main()

What to expect: The table shows the output for five test pages of our own on our test machine. Static means prebuilt files; server-side rendering (SSR) means the server builds the HTML on every request; client-side rendering (CSR) means JavaScript builds the page in the browser (web.dev on rendering types). On real websites, the numbers are larger, but the pattern holds.

Expected output by page type, measured on five test pages of our own: key sentence and words in raw HTML and in the DOM, plus text blocks only in the DOM
Page typegrep -cIn raw, DOMWords raw, DOMDOM only
static: prebuilt HTML1yes, yes25, 250
server-side rendering (SSR)1yes, yes29, 290, more with changing values
mixed: text from the server, one paragraph via script1yes, yes25, 351
client-side rendering (CSR): empty shell0no, yes3, 283
data only: sentence in a JSON block, displayed via script1no, yes0, 91

Measured  · curl 8.21.0, GNU grep 3.0, Python 3.12, Chrome 156, fetches of five test pages of our own with Python and curl, output of the comparison script

How to read the rows:

  • Static: The text is readable by every crawler.
  • Server-side: The text is readable by every crawler; scripts only make the page interactive.
  • Mixed: The paragraph added by the script is missing without JavaScript.
  • Client-side: The text is missing without JavaScript; the 3 words are a notice in the noscript element.
  • Data only: grep counts the sentence, but it only appears on the page after the script runs.

To a crawler, static and server-rendered pages look the same: Both pass the check. If the server rebuilds the page on every request, a block with changing values such as a time can show up as “only in DOM,” because the script fetches the page twice. That isn’t a rendering error. This is what the output looked like for the mixed test page:

Output of raw-vs-dom.py for the mixed test page
Status:       200
Scripts:      1 executable, 0 data blocks (such as JSON-LD)
Words:        25 in raw HTML, 35 in DOM
Key sentence: raw HTML yes | DOM yes
Only in DOM:  1 text block
- Maintenance contracts are available for systems up to 100 kWp.

Without Python, you can count the key sentence in the DOM directly. Point Chrome to a separate profile folder so that an open Chrome window doesn’t get in the way:

Terminal on Linux (first line) and macOS (second line): count the key sentence in the DOM
google-chrome --headless --user-data-dir=/tmp/df-probe --dump-dom https://www.example.com/services/ | grep -c "Your key sentence"
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless --user-data-dir=/tmp/df-probe --dump-dom https://www.example.com/services/ | grep -c "Your key sentence"
PowerShell on Windows: count the key sentence in the DOM
$chrome = "C:\Program Files\Google\Chrome\Application\chrome.exe"
Start-Process $chrome -ArgumentList "--headless", "--user-data-dir=$env:TEMP\df-probe", "--dump-dom", "https://www.example.com/services/" -NoNewWindow -Wait -RedirectStandardOutput dom.html
(Select-String -Path dom.html -SimpleMatch "Your key sentence" -Encoding utf8).Count

On Windows, write the DOM to a file first, because in our test on November 9, 2026, piping it directly with | returned nothing.

Common mistake: The Elements tab in the browser’s developer tools shows the DOM, meaning the page after the scripts have run. The source the server sent is in the “View page source” view (Ctrl+U, on a Mac Command+Option+U). Our how-to on getting recommended by ChatGPT describes this quick check without a terminal.

Done when the “Only in DOM” line shows 0 for your most important pages or lists only text that means nothing to your customers.

Is it rendering, robots.txt or the firewall?

Two requests with different user agents and a look at robots.txt tell rendering, robots.txt and firewall problems apart. A firewall gives itself away with a different status or less content when you send a crawler’s user agent. A robots.txt block shows only in the file itself, because curl doesn’t honor it. Only your server or CDN logs reveal IP blocks.

Request the page once normally and once with a crawler’s user agent, then read your robots.txt:

Terminal on macOS and Linux: requests with two user agents and robots.txt
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" https://www.example.com/services/
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot" https://www.example.com/services/
curl -s https://www.example.com/robots.txt | grep -i -A1 "gptbot"
PowerShell on Windows: requests with two user agents and robots.txt
curl.exe -s -o NUL -w "%{http_code} %{size_download}\n" https://www.example.com/services/
curl.exe -s -o NUL -w "%{http_code} %{size_download}\n" -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot" https://www.example.com/services/
curl.exe -s https://www.example.com/robots.txt | Select-String -Pattern "gptbot" -Context 0,1

Each of the two request lines prints the response status and size in bytes. The user agent in the example is the one OpenAI specifies for its GPTBot crawler (OpenAI’s crawler documentation). The curl manual describes the -A and -w options. If even the first request returns 403, your CDN probably blocks curl in general. In that case, use -A to send a browser’s user agent.

How to recognize the cause
CauseKey sentence on a normal requestRequest with the crawler’s user agentEntry in robots.txt
Text only appears via JavaScript0same status and size as the normal requestnone
Block in robots.txt1 or moresame status, same sizeDisallow under the crawler’s user agent or under User-agent: *
Firewall or CDN blocks by user agent1 or more403 or a much smaller challenge page with status 200; key sentence 0none
Block by IP address1 or moresame status, same size; the block stays invisible in this requestnone

On our test page with a block by user agent, the normal request returned 200 317 and the request with GPTBot’s user agent 403 92. The test page with a block in robots.txt answered both requests with 200 317. So the request with the user agent doesn’t show the block, but the file does.

Why it matters: From the outside, all of these causes look the same, because AI services don’t have your text. They get fixed in different places: in the website’s code, in robots.txt, or in the settings of your firewall and CDN (the network that delivers your pages). Our article AI crawlers and robots.txt lists which user agents exist and what you should allow. How to check your firewall and CDN is covered there in the section on firewalls and CDNs.

Common mistake: A status 200 with GPTBot’s user agent gets read as proof that GPTBot may read the page. But curl doesn’t check robots.txt.

Done when you know the cause for every page that lacks its key sentence.

What should you do if important text exists only in the DOM?

Get the text into the HTML your server delivers: as a static page, rendered on the server or generated at build time. Google recommends these approaches and calls separate versions just for crawlers a workaround. Microsoft advises against hiding important content behind client-side rendering. Interactive elements can stay, as long as the text is in the HTML without them.

  1. Change the rendering. For applications built in the browser, server-side rendering or generating pages at build time helps. Google considers this worthwhile because it makes websites faster and because not all bots can run JavaScript (Google’s JavaScript SEO basics). Our comparison Static site vs. WordPress compares which approach suits your website.
  2. Move late-loading parts into the HTML. Prices, services, business hours and answers to common questions belong in the initial HTML. If a widget has to stay, state the essentials next to it as text. Microsoft says that content that can’t be rendered reliably may not be indexed or used as a source for answers (Bing’s guidelines).
  3. Don’t build separate pages just for crawlers. Google calls a separate version for recognized bots dynamic rendering. It describes that as a workaround that adds complexity and recommends the approaches in point 1 (Google on dynamic rendering).
  4. Check again. Repeat the three steps of the check for every page you changed.

That’s why, on the websites we build, the text is in the HTML from the start.

Common mistake: The home page gets switched over while prices and services on subpages keep loading via script.

Done when the “Only in DOM” line no longer lists any important content on the pages you changed.

What doesn’t this check show?

This check shows what a request without JavaScript gets from your own computer. It doesn’t show whether an AI service actually fetches your page or whether it mentions you. You’ll only see the crawlers’ real access in your server or CDN logs, and you measure mentions with a fixed set of questions for AI assistants.

  • Blocks by IP address or country. Your request with a crawler’s user agent comes from your own network. You can identify genuine crawlers by the operator’s IP address. How to count fetches is covered in our how-to on tracking AI traffic.
  • Timing. Crawlers can change without anyone announcing it, as the reports from September 2026 suggest. Repeat the check after every redesign and every six months.
  • The effect. Once your text is in the HTML, one hurdle is gone. Whether AI answers mention you more often as a result is something this check can’t show. You measure that with a fixed set of questions, as our how-to on measuring AI visibility describes.

Done when your report states what it doesn’t cover.

From our work: what the check found on datafalke.de

On November 9, 2026, we checked 16 pages of datafalke.de with the comparison script above and an additional request using GPTBot’s user agent. All 16 pages returned status 200. None contained an executable script; the only script element on each page carries structured data in JSON-LD format. The DOM had the same number of words as the raw HTML, and no text block was added only in the browser. That matches how the site is built: datafalke.de is a static website (technology in our colophon).

Check of 16 pages on datafalke.de on Nov 9, 2026 (German and English)
MetricResult
Status on a normal request and with GPTBot’s user agent200 on all 16 pages
Executable scripts per page0
Script elements with JSON-LD per page1
Words in raw HTML2,064 to 4,473; the same number in the DOM on each page
Text blocks that exist only in the DOM0
Response with GPTBot’s user agent vs. normal responsebyte-for-byte identical, 77,984 to 118,120 bytes per page

Measured  · Python 3.12 and Chrome 156, 16 pages, GET requests only; comparison of raw HTML and DOM, request with GPTBot’s user agent

The error cases in the tables above all come from a minimal page of our own on our test machine. We didn’t test any other websites. The Windows commands ran in PowerShell 5.1 on Windows 11, and the terminal commands ran in Git Bash with curl and GNU grep. We didn’t run them on macOS or Linux ourselves.

To try this on one of our pages:

Terminal: count a key sentence on a page of datafalke.de, result on Nov 9, 2026: 1
curl -sL https://datafalke.de/en/insights/get-recommended-by-chatgpt/ | grep -c "also how we deliver"

This only proves that our text is readable without JavaScript and that GPTBot’s user agent isn’t blocked. It says nothing about whether AI answers mention us as a result. Our methodology for measuring AI visibility describes how we’ll measure that; there’s no measurement log yet.

Sources

  1. Vercel: The rise of the AI crawler. . Accessed .
  2. Forge AI Bot Log: Can AI crawlers run JavaScript? What ChatGPT-User really sees on your page. . Accessed .
  3. searchVIU: AI crawlers & JavaScript rendering: 39 bots compared. , updated . Accessed .
  4. Anthropic (Claude Platform Docs): Web fetch tool. Accessed .
  5. Anthropic: Does Anthropic crawl data from the web, and how can site owners block the crawler? . Accessed .
  6. Google Search Central: Understand the JavaScript SEO basics. . Accessed .
  7. Google Search Central: AI features and your website. . Accessed .
  8. Apple: About Applebot. . Accessed .
  9. Microsoft Bing: The new evergreen Bingbot simplifying SEO by leveraging Microsoft Edge. . Accessed .
  10. OpenAI: Overview of OpenAI Crawlers. Accessed .
  11. Perplexity: Perplexity Crawlers. Accessed .
  12. Microsoft Bing: Bing Webmaster Guidelines. Accessed .
  13. web.dev (Google): Rendering on the web. . Accessed .
  14. Chrome for Developers: Chrome Headless command-line reference. . Accessed .
  15. curl project: curl man page. Accessed .
  16. curl project: curl shipped by Microsoft. Accessed .
  17. Microsoft Learn: Invoke-WebRequest (PowerShell 5.1). , updated . Accessed .
  18. Google Search Central: Dynamic rendering as a workaround. . Accessed .

Editorial note

Researched and drafted with AI assistance. Mohammad Alboush has not yet checked the sources, figures and claims.

Content not yet reviewed

Changes to this article

DateTypeChange
First publicationArticle published
CorrectionThe next step at the end now points to the AI visibility audit, and the inquiry there is filed under “GEO and AI visibility (audit or retainer).” The statements remain the same.
CorrectionThe related service at the end is now the AI visibility audit. The statements remain the same.

Spotted a mistake? Email us at kontakt@datafalke.de. We’ll fix it and log the change.

Your next step

Not showing up on Google yet?

Send us your website address. For existing sites, we start with a fixed-price audit: indexing, load time, structured data and AI visibility.

Menu

Book an intro callDeutsch