NotebookLM ran our JavaScript
Table of contents
I was wrong about NotebookLM before lunch.
That is the cleanest way to start this post.
The first signal said NotebookLM was not executing JavaScript. It saw content that looked post-hydration, but our Layer 3 JavaScript-execution beacon did not fire. The tempting interpretation was simple: NotebookLM must be scraping URLs and text out of static HTML or script payloads, not running the page.
That interpretation was wrong.
NotebookLM ran our JavaScript. The beacon was the thing that failed.
The False Trail
The test bed at next.jsseo.dev measures which JavaScript content patterns survive in bot responses. Every cell has a marker. The tracker sees page requests. A separate Layer 3 beacon records whether the requester executed enough JavaScript to fire a client-side signal.
NotebookLM complicated that model.
When a ssr/mixed/article URL was added to NotebookLM as a source, NotebookLM had access to content that should not exist in the initial HTML:
| Content observed by NotebookLM | Why it mattered |
|---|---|
/api/content body text |
This endpoint is called by the JsFetched React effect. |
| Late-loaded related content | This appears after client-side rendering, not in the initial static body. |
| Cloudflare RUM request | This is injected and fired by JavaScript. |
At the same time, the legacy Layer 3 beacon did not produce a google-notebooklm row.
That contradiction created the wrong hypothesis:
NotebookLM is not executing JavaScript. It is aggressively scraping static resources and extracting content that looks like it required JavaScript.
There were several weak signals pointing that way. No beacon row. No visible /_next/ chunk fetches. Some hits to beacon-like endpoints without the payload we expected.
The problem was that those signals were not independent. They were mostly artifacts of our own measurement design.
The tracker skips /_next/* paths, so “we did not log chunk fetches” never meant “NotebookLM did not fetch chunks”. The url_path field does not store query strings, so SQL checks for beacon payloads using LIKE '/api/js-executed?p=%' were asking the wrong question. And the beacon itself was an inline script plus an image-shaped request, which turned out to be exactly the kind of signal NotebookLM’s sandbox could miss or block.
So we built a falsifier.
The Falsifier
The probe had one job: create a URL that cannot exist unless JavaScript runs.
The component does this:
const uuid = crypto.randomUUID();
fetch(`/api/probe/${uuid}`);
The literal string /api/probe/ exists in the bundle. The concrete URL does not.
A static scanner can find the text /api/probe/. It cannot know the UUID. The UUID is generated at runtime. If the tracker sees a request to /api/probe/<real-uuid>, that request came from a renderer that evaluated JavaScript.
We deployed that probe at:
/probe/runtime-entropy-2026-05-22
Then we added the URL to NotebookLM as a source.
What NotebookLM Did
NotebookLM generated its own runtime UUID and fetched the probe endpoint.
Tracker sequence from the test:
| Signal | Observation | Meaning |
|---|---|---|
| Runtime probe | NotebookLM fetched /api/probe/a9a34ae7-... |
It evaluated crypto.randomUUID() and built a URL at runtime. |
| Client content | NotebookLM fetched /api/content |
The JsFetched component’s useEffect ran. |
| Cloudflare RUM | NotebookLM fetched /cdn-cgi/rum |
Auto-injected JavaScript executed. |
| DOM state | NotebookLM displayed its generated UUID | React mounted, updated state, and rendered the result. |
That killed the static-scrape hypothesis.
NotebookLM did not merely discover static URLs. It executed the React bundle.
This is the important distinction: the bot did not just access content that normally appears after JavaScript. It proved JavaScript execution by creating a value that could not exist before runtime.
Then Why Did The Beacon Miss It?
The original Layer 3 beacon was an inline script at the end of the page body. It read the page marker and fired an image-shaped request to /api/js-executed.
That worked for normal browsers. It worked for Google Search Console URL Inspection. It worked for Bing’s renderer.
It did not work for NotebookLM.
The runtime probe made the reason clearer. NotebookLM executes bundled React code, but it appears to skip or suppress some inline script boilerplate. That is plausible for an AI ingestion renderer. The semantic page content lives in the React tree and the bundled app code, not in inline analytics-style scripts.
So we moved the beacon into a React client component.
The first version of that V2 beacon still used new Image().src, like the old beacon. NotebookLM still did not fire it, even though the same NotebookLM visit fetched /api/content.
Then we changed one thing: V2 stopped using an image request and used fetch() POST instead.
That worked.
Fresh NotebookLM visit, fresh row:
| Bot | V1 inline + image | V2 client component + fetch |
|---|---|---|
| human Chrome | yes | yes |
| Google URL Inspection | yes | yes |
| Bingbot renderer | yes | yes |
google-notebooklm |
no | yes |
The difference was not “JavaScript vs no JavaScript”. The difference was the delivery mechanism.
NotebookLM ran the code. It just did not tolerate the old beacon shape.
What This Changes
The common shortcut “AI bots do not execute JavaScript” is too broad.
The better taxonomy is:
| Class | Examples | JavaScript behavior |
|---|---|---|
| No-JS batch crawlers | GPTBot, ClaudeBot, PerplexityBot |
No beacon, no runtime probe, initial HTML is the main evidence surface. |
| Robots/compliance probes | OAI-SearchBot in our first week |
Reads rules, did not fetch content cells in the observed window. |
| JS-capable AI surfaces | Google-NotebookLM |
Executes bundled JS and client-side fetches, but may block some analytics-shaped beacon mechanisms. |
| Search renderers | Google URL Inspection, some Bingbot visits | Execute JavaScript in a browser-like pipeline. |
NotebookLM belongs in the JS-capable group.
That does not mean GPTBot renders JavaScript. It does not mean ClaudeBot renders JavaScript. It does not mean every AI product sees the same DOM a human sees.
It means the AI-bot landscape has split.
Some AI bots are still plain HTML fetchers. Some are rule probes. Some are user-triggered fetchers. Some are full or partial renderers. If your measurement collapses all of those into “AI bot traffic”, you will get nonsense.
What This Means For JS Content Patterns
For NotebookLM specifically, js-fetched content can survive.
That is not a theoretical statement. NotebookLM fetched /api/content, the endpoint used by the test bed’s JsFetched pattern after mount.
That makes NotebookLM very different from the batch crawlers in the same experiment. In the week-one data, GPTBot, ClaudeBot, and PerplexityBot did not produce JavaScript-execution signals. For them, content that is absent from the initial HTML remains at risk.
For NotebookLM, the risk is different. The question is no longer “does it execute JavaScript at all?” It does. The question becomes:
- Which JavaScript patterns complete inside its sandbox?
- Which network primitives are blocked?
- Does it scroll enough to trigger late-loaded content?
- Does it click anything? Almost certainly not.
- Does it treat hash-routed content as navigable?
That is a better research question than the blanket claim we started with.
The Method Lesson
This finding is partly about NotebookLM, but it is also about measurement.
Our original beacon was not wrong when it fired. A beacon row was still a real execution signal. The mistake was treating “no beacon row” as “no JavaScript execution” without first testing whether the renderer could see that beacon mechanism.
Three lessons came out of the failure:
- A missing record can be caused by your tracker, not by the bot. We did not log
/_next/*, so we could not infer anything about chunk fetching. - Know the exact shape of stored data before writing SQL around it. Our
url_pathdid not include query strings. - One beacon is not a universal truth serum. Bot sandboxes block different APIs and request shapes.
The runtime-entropy probe is the cleaner test. It does not ask “did this exact analytics-style beacon fire?” It asks “did the renderer create a value that only JavaScript can create?”
That is why it changed the conclusion.
What I Am Not Claiming
I am not claiming NotebookLM is equivalent to a normal Chrome browser.
I am not claiming all Google AI surfaces use the same renderer.
I am not claiming batch AI crawlers execute JavaScript. The evidence still points the other way for GPTBot, ClaudeBot, and PerplexityBot.
I am not claiming every JS content pattern survives in NotebookLM. We have proven execution and client fetches. We have not yet mapped every pattern.
The narrow claim is this:
NotebookLM executed JavaScript on our test bed. Our first beacon missed it because the beacon shape was wrong for that renderer.
That is enough.
What We Will Test Next
The next step is pattern-level NotebookLM testing.
The test is simple: feed NotebookLM one URL from each major pattern and watch the tracker:
ssr/clean/articlessr/js-fetched/articlessr/click-reveal/articlessr/late-loaded/articlessr/hash-routing/articlessr/js-images/articlessr/mixed/articlecsr/clean/article
For each URL, check:
- Did
google-notebooklmfetch the page? - Did it fire V2 beacon?
- Did it fetch
/api/content? - Did it hit any runtime probe endpoint?
- Could NotebookLM summarize the actual content, or only the placeholder?
That will tell us whether NotebookLM is merely JS-capable or actually robust across the content patterns that break simpler crawlers.
For now, the headline is already useful:
NotebookLM ran the JavaScript. The measurement had to catch up.
Data availability: JS SEO Lab publishes methodology and tracker notes in the public repository at github.com/Qbeczek1/jsseo-dev. The live aggregate dashboard is available at /dashboard/.
Bias disclosure: I run JS SEO Lab as an independent technical SEO research project. I also do paid technical SEO and AI visibility audits through FratreSEO. No framework vendor, crawler vendor, search engine, or AI company funds this work.