Fixing JavaScript-Only Content for AI Agents
Many pages look complete in a browser but arrive from the server almost empty: the price, the stock message, the event date or the main description is added a moment later by JavaScript. People rarely notice. AI tools that fetch the HTML without running scripts see blanks, and browser-based agents can read the page before the script has finished.
This guide shows how to find exactly which facts depend on JavaScript, how to move them into the HTML your server sends, and how to keep scripts for the extras rather than the essentials.
What "JavaScript-only" means
Every page starts as a response from your server: a block of HTML. A browser then runs any scripts on the page, and those scripts can add, change or remove content. Content is JavaScript-only when it is absent from that first HTML response and only appears once a script has run.
That can happen in a few different ways:
- The whole page is built in the browser. The server sends an almost empty shell,
often just
<div id="root"></div>and a script tag. This is typical of single-page apps without server rendering, covered in detail in the single-page app guide. - The page is mostly there but key facts are fetched afterwards. The template prints the product name and photos, then a script asks an API for the current price and stock. This is very common on otherwise traditional sites and is the main subject of this guide.
- A third-party widget supplies the content. Reviews, booking calendars, menus, store finders and event listings are often loaded from another company's servers into a placeholder.
- The data is present but not as text. Some frameworks embed the page data as a large JSON object in a script tag and build the visible text from it in the browser. The facts are technically in the response, but not as readable page content.
Who sees what
Different visitors see different versions of the same page, which is why the problem hides so well.
| Visitor | Runs JavaScript? | What it gets from a JavaScript-only page |
|---|---|---|
| A person in a normal browser | Yes | The full page, perhaps after a short flicker or spinner. |
| An AI tool that fetches the page | Usually not | The raw HTML only. Prices, dates and text added by scripts are missing. |
| An agent driving a browser | Yes | Usually the full page, but it may read or screenshot the page before slow scripts finish, or give up on a page still showing placeholders. |
| Link preview bots and many SEO tools | Usually not | The raw HTML only. |
| Googlebot | Yes, in a later rendering step | The rendered page eventually, though the raw HTML is processed first. |
The safe assumption is that anything essential to a customer's decision should be readable in the raw HTML. Scripts can then improve the experience for visitors who run them, without anyone losing the basic facts.
How to find the missing facts
The AI Agent Compatibility audit flags content hidden behind JavaScript. To pin down exactly what is missing on a given page, use three quick checks.
View source, not Inspect
Right-click the page and choose View Page Source, or press Ctrl+U (Cmd+Option+U on a Mac). This shows the HTML exactly as the server sent it. Use the browser's find box to search for the price, the date or a sentence from the main description. The Inspect or Elements panel is misleading here, because it shows the page after scripts have changed it.
Fetch it from the command line
curl -s -A "Mozilla/5.0" https://www.example.co.uk/courses/pottery-taster \
| grep -o -i -E "£[0-9]+(\.[0-9]{2})?|in stock|sold out|[0-9]{1,2} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*" \
| sort -u
This prints any prices, stock words or dates found in the raw HTML. If the page shows "£45, 3 places left, Saturday 14 November" but the command prints nothing, those facts are JavaScript-only. Adapt the pattern to the facts that matter on your page.
Switch JavaScript off
In Chrome or Edge, open developer tools, press Ctrl+Shift+P (Cmd+Shift+P on a Mac), type "Disable JavaScript" and press Enter, then reload. Firefox has a similar setting in about:config. What remains is roughly what a fetcher sees. Pay attention to empty boxes, permanent spinners, "Loading..." text and buttons that do nothing.
The fix, step by step
Step 1
Compare the raw HTML with the rendered page
Pick one page from each important template, such as a product, a service, an event and a location page. For each, save the view-source HTML and a copy of the page with JavaScript switched off, then compare them with what a normal visitor sees. Note every difference, however small, including blank spaces where a price, a badge, a date, a map or a block of text should be.
Step 2
List the facts missing from the raw HTML
Turn the differences into a short list of facts, not page areas: current price, stock or places left, delivery estimate, opening hours, event date and time, main description, reviews summary and contact details. Rank them by how much a customer needs each one to decide. Price, availability and dates almost always come first, while carousels of related products and recently viewed items can safely stay in JavaScript.
Step 3
Print those facts in the server template
Change the page template so the server reads the price, stock, dates and main text from
your database or platform and writes them into the HTML as ordinary text, inside elements such as <p>,
<dl> or <time>. If a script currently fetches them from an API
after load, call the same data source on the server instead. Keep the element IDs so existing scripts
can still update values later.
Step 4
Give third-party widgets a server-side fallback
For reviews, booking calendars, menus and store finders loaded from another service, find out whether the provider offers server-side output, a static embed or an API you can call when building the page. If not, print a plain summary yourself, such as the average rating and review count, the next available dates or a link to the booking page, so the essential fact exists before the widget loads.
Step 5
Keep collapsed content in the page
Tabs,
accordions and "read more" sections are fine as long as their content is already in the HTML and simply
hidden with CSS or the hidden attribute until opened. Change any component that only fetches
or creates the content when clicked. The native <details> and <summary>
elements are a good option, because they open and close without any script at all.
Step 6
Let JavaScript enhance rather than supply
Once the facts are in the HTML, scripts can still refresh them, for example updating stock every minute or switching price when a size is chosen. Write those scripts so they replace the server value rather than filling an empty box, and so a failed request leaves the server value in place. Remove spinners and placeholder text from the initial HTML wherever the real value is now available.
Step 7
Retest with scripts switched off
Repeat
the view-source, curl and JavaScript-off checks on each template and confirm every fact
on your list now appears as readable text. Check that the values match the page with JavaScript on, including
after picking a variant. Run the AI Agent Compatibility audit again, and add a simple automated check
that fetches key pages and looks for the price or date text.
Common culprits and what to do about each
| Pattern | What fetchers see | Better approach |
|---|---|---|
| Price fetched from a pricing API on load | An empty price box or "£--" | Print the current price in the template; let the script refresh it. |
| Stock badge filled by a stock app | No stock information, or a default "In stock" | Print the stock status from the platform's own inventory. |
| Reviews widget in a placeholder div | Nothing | Print the average and count server-side, and link to a page of reviews. |
| Booking calendar in an iframe | A frame address, not the dates | Show the next available dates or a clear "Book at" link in the page itself. |
| Description loaded when a tab is clicked | The tab labels only | Put all tab panels in the HTML, hidden until opened. |
| "Load more" button for events or products | The first batch only | Give each batch
a real paginated address, such as ?page=2, and link to it. |
| Currency or tax converter rewriting prices | The base price, then a different one in the browser | Generate localised prices on the server, or show the charging currency plainly. |
| Text drawn in a canvas or baked into images | No text at all | Use real HTML text; keep images for decoration. |
Contact details that exist only as an image or behind a click-to-reveal button are a special case with their own guide: fixing unreachable contact details.
Before and after: a price that depends on a script
Here is a typical pattern. The template prints an empty element and a script fills it in.
<!-- Before: the server sends an empty price -->
<p class="price" id="price"><span class="spinner"></span></p>
<script>
fetch('/api/price?sku=PT-TASTER')
.then(r => r.json())
.then(d => { document.getElementById('price').textContent = '£' + d.price; });
</script>
After the fix, the server writes the price and the script only updates it if something has changed since the page was built.
<!-- After: the server sends the price; the script refreshes it -->
<p class="price" id="price" data-sku="PT-TASTER">£45.00</p>
<p id="places">3 places left on Saturday 14 November</p>
<script>
const el = document.getElementById('price');
fetch('/api/price?sku=' + el.dataset.sku)
.then(r => r.ok ? r.json() : null)
.then(d => { if (d) el.textContent = '£' + d.price; })
.catch(() => {});
</script>
If the request fails, the server value stays. If the page is cached for a few minutes, the script corrects any change. Every visitor, including a fetcher that never runs the script, gets a real price.
Choosing how to render
How you get facts into the first HTML depends on how the site is built.
- Traditional server templates (WordPress, Shopify Liquid, Django, Rails, Laravel, PHP): the template already runs on the server, so the fix is usually to print the value in the template instead of fetching it in the browser.
- Static site generators (Eleventy, Hugo, Astro and similar): facts that change rarely, such as descriptions and hours, can be written at build time. For facts that change often, such as stock, rebuild on change or print a sensible default and refresh it with a script.
- JavaScript frameworks (React, Vue, Svelte, Angular): use the server rendering or static generation built into Next.js, Nuxt, SvelteKit, Angular's server rendering or similar, so each route is sent as HTML. The single-page app guide covers this in depth.
Speed matters too. Even when an agent does run scripts, a page that takes many seconds to show its facts may be abandoned. If your pages render correctly but slowly, see fixing slow rendering for agents.
Edge cases
- Personalised prices. If logged-in trade customers see different prices, print the public price for anonymous visitors in the HTML and switch after sign-in.
- Location-based content. Store finders that need the visitor's location can still print a list of all locations, or the nearest ones by postcode, on a normal page.
- Cookie banners. A consent banner that covers the page is not the same as missing content, but a banner that blocks rendering until accepted has the same effect. Content should be in the HTML behind the banner.
- Structured data added by script. JSON-LD injected by a tag manager has the same problem as visible text. See fixing structured data for agents.
- Very large pages. Printing hundreds of products in one HTML response is slow. Paginate with real addresses instead of loading everything into one page.
Stopping it from coming back
JavaScript-only content tends to creep back with each new app, widget or redesign. A few habits keep it out:
- Make "the key facts are in view-source" part of the definition of done for any new template or component.
- Before installing a widget or app that shows prices, stock, reviews or dates, ask the supplier how the content appears to visitors who do not run JavaScript.
- Keep a small list of important pages and the text each must contain, and fetch them automatically after every release.
- Re-run the AI Agent Compatibility audit after theme updates and major plugin or app changes.
1. Why does my price show in the browser but not in view source?
Because a script adds it after the page loads. The server sends HTML with an empty space for the price, and JavaScript fetches the value from an API or a third-party app and writes it in. View source shows the server's original HTML, while the Inspect panel shows the page after scripts have run. Any tool that reads the HTML without running JavaScript sees the empty space, so the price needs to be printed by the server template.
2. Don't AI agents run JavaScript like a normal browser?
Some do and some do not. Agents that drive a real browser run scripts, but they may read the page before slow scripts finish, and they cannot see content that only loads after a click they did not make. Many lighter AI tools just download the HTML and never run scripts at all. Putting key facts in the first HTML works for both kinds, so it is the safer choice whichever tools your customers use.
3. Is content in tabs and accordions hidden from agents?
Not if it is already in the HTML. A tab or accordion that hides its content with CSS or the hidden attribute until clicked is fine, because the text is there for any tool that reads the page. The problem is a component that only fetches or builds its content when someone clicks it. Check by viewing the source and searching for a sentence from inside a closed tab. If you cannot find it, change the component.
4. Is dynamic rendering a good way to fix this?
It works as a temporary measure but is not a good long-term fix. Dynamic rendering serves a pre-rendered snapshot to visitors that identify as bots and the normal JavaScript page to everyone else. Snapshots go stale, the setup adds cost and complexity, and agents that use an ordinary browser identity still get the JavaScript-only version. Google itself calls it a workaround. Server rendering or printing the facts in the template fixes the page for everyone.
5. Will putting content in the HTML slow my page down?
Usually the opposite. When the server sends the facts in the HTML, visitors see them as soon as the page arrives instead of waiting for a script to download, run and make another request. The HTML gets a little larger, but a price, a date or a few paragraphs of text add very little. The exception is printing very long lists in one response, which is best handled with pagination and real page addresses.
6. My reviews come from a third-party app. What can I do?
Check whether the review provider offers server-side output, such as a snippet your template can include or an API your server can call when building the page. Many do, often on higher plans. If not, store the average rating and review count in your own system and print them in the template, with a link to a page listing the reviews. Keep any review markup consistent with what is shown, and only use reviews collected for that product.
7. How can I check a whole site for JavaScript-only content?
Start with your templates rather than every page, because pages built from the same template usually share the problem. Run the AI Agent Compatibility audit on one page of each type, then use view source or curl to confirm what is missing. For ongoing monitoring, keep a list of important addresses and the text each must contain, such as a price or a date, and fetch them automatically after each release to catch regressions early.
The AI Agent Compatibility audit tests your page for bot blocks, content hidden behind JavaScript, unlabelled forms and controls, navigation agents cannot follow and missing structured data, and links each finding to the guide that fixes it.
Open AI Agent Compatibility Fixes โ