We already knew NotebookLM runs JavaScript and Bingbot splits into a JS and a non-JS subsystem. Both findings came from one beacon firing, or not firing. This one comes from the beacon firing through the wrong channel - and it turns “does the bot run JS?” into “how deep does it go?”

Two channels

The Layer 3 beacon has two independent delivery paths:

  • Inline - an inline <script> at the end of <body>, sent as an image request. Fires if the bot executes raw inline script.
  • Client-component - a React client component running in a useEffect, sent as a fetch(). Fires if the bot evaluates the React bundle and runs the component lifecycle.

Both ran in parallel the whole window (the last inline fire was 2026-08-01). So “fired one channel but not the other” is a property of the renderer, not an artifact of timing - in any week where one bot fired the inline channel, another fired the component channel.

Four tiers

applebotgoogle-readaloudgooglebotnotebooklmgptbotgsc-inspectionbingbotheadless-chrome 32 12 7 1 3 11 inline scriptReact client component
Figure 1 - Beacon fires per bot, inline channel (coral) vs React-component channel (blue). The Google and Apple renderers fire only the component channel; GPTBot fires only the inline channel; Bing, GSC inspection and headless Chrome fire both. (Human traffic, 106 inline / 46 component, is omitted for scale.)
tier classes inline component reading
Bundle-evaluators applebot, googlebot (WRS), google-readaloud, google-other, notebooklm 0 54 Evaluate the React tree, skip the inline <body> script
Inline-only gptbot 3 0 Runs a raw inline script, does not evaluate the bundle
Full human, headless-chrome, gsc-inspection, bingbot 122 65 Both channels - full browser-class execution
Zero claudebot, amazonbot, ccbot, meta-external, oai-searchbot, perplexitybot, chatgpt-user, claude-user, perplexity-user, google-extended, bytespider 0 0 No beacon, ever

Two headline numbers

Applebot renders almost everything it crawls. 32 component-channel beacons against 32 verified Applebot cell fetches, spread across all eight content patterns. Applebot belongs with Googlebot-WRS in the full-renderer club, not with the batch AI crawlers.

cleanlate-loadedmixedjs-fetchedjs-linksclick-revealjs-images 15 6 4 3 2 1 1
Figure 2 - Applebot component-channel beacons by content pattern. It rendered across the whole matrix, not just the easy cells.

GPTBot is the only batch AI crawler that runs any JS at all - and only the inline channel (three fires, one cell, one day). Every other batch AI crawler produced exactly zero beacons across 80 days.

Why the beacon population is trustworthy

A spoofer sending a fake user-agent to a static scraper cannot fire the beacon - firing it requires actually executing the page’s JS. So this layer self-selects for genuine renderers; the nine-IP impersonation pool that pollutes the HTTP logs never appears here. The beacon, not any HTTP side-effect, is the authoritative execution signal.

What this means for site owners

The tiers imply a specific rule for JS-gated content:

  • Injected by an inline <script>: seen by GPTBot, skipped by the renderers that matter most (Google WRS, Applebot, Read Aloud, NotebookLM), which treat the inline <body> script as boilerplate.
  • Behind React (useEffect, client fetch, hydration): seen by Applebot, Googlebot-WRS, Read Aloud and NotebookLM; invisible to GPTBot and every zero-JS batch crawler.
  • In the server-rendered HTML: seen by everyone.

If you must hide content behind JavaScript, put it in the React component lifecycle, not an inline script. But the only channel that reaches the zero-JS majority - ClaudeBot, Perplexity, Amazon, CCBot - is server-side rendering.

What this does not prove

  • The mechanism is confounded. The inline channel is both an inline script and an image-beacon, so we cannot fully separate “skips the inline script” from “blocks analytics-shaped image requests.” The effect on content visibility is the same; the cause is not isolated.
  • GPTBot’s inline result is thin (n=3, one cell, one day). Directional, not robust.
  • The beacon is a floor, never a ceiling. A bot can render fully and still drop the beacon (sandbox, keepalive). Every tier is a lower bound on execution.

What would falsify this

If, in a fresh window, Applebot starts firing the inline channel or GPTBot fires the component channel, the tiers are less clean than they look here. And if a claudebot or amazonbot beacon ever appears, the zero tier breaks - that single row would be a bigger finding than this whole post.

Method notes

Beacon rows are stored with a source of inline or client-component; the counts above are direct group-bys over the js_executions table for the full window. Applebot’s render rate compares its 32 component beacons against its 32 verified (reverse-DNS confirmed) content fetches in the canonical HTTP log - aggregate counts, not a per-request join, so read it as “near 1:1,” not exact.


Data availability: methodology, tracker code, classifier and beacon source are public at github.com/Qbeczek1/jsseo-dev.

Bias disclosure: I run JS SEO Lab as an independent technical SEO research project, and do paid technical SEO and AI-visibility audits through FratreSEO. No framework vendor, crawler vendor, search engine, or AI company funds this work.