Skip to content

Does it need a browser?

"You need to enable JavaScript to run this app" in a fetch result

Why a static fetch returns that one sentence, which template put it there, why Readability returns nothing for the same page, and how to route it.

Published

This article was drafted with AI assistance from the sources listed below and edited and published by Teneo Labs. Disclosure: this site is run by Teneo Labs, the company behind Live Web Data API, which this article mentions.

The sentence showed up. Now what?
  • Confirm the shell: the noscript line next to an empty div id root
  • Expect nothing from Readability: it removes noscript elements
  • Do not rely on the string: the Vite React template ships no noscript at all
  • Search the raw HTML for embedded data before you start a browser
  • Nothing there: render the page or drop it, but never pass the sentence on as content

Your fetch result is one line long, and the line is "You need to enable JavaScript to run this app." The page did not fail. It answered with an app shell, and that sentence is the only text the shell contains.

Where the sentence comes from

It is a template default. Create React App's public/index.html puts it in the body, directly above the mount point:

<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

A comment in the same template says the build step places the bundled scripts into the body. So a site built from it and left unchanged sends this exact sentence, an empty #root and script tags. The text you came for is built in the browser.

Check your copy:

URL=https://example.com/
curl -sL "$URL" -o raw.html
grep -oi '<noscript>[^<]*' raw.html
grep -oE '<div id="(root|app)"></div>' raw.html

The noscript line plus an empty mount point means you have an app shell.

Why one extractor shows it and another shows nothing

Two tools can read the same shell and disagree.

A parser with the scripting flag on reads <noscript> children as plain text. parse5 does this by default: its scriptingEnabled option defaults to true, and with it set, noscript content is parsed as text. Any step that then collects every text node in the body returns the sentence as the page.

Mozilla's Readability goes the other way. Before it looks for the article, it pulls single images out of noscript fallbacks, then removes every script and noscript element. On a Create React App shell nothing readable is left.

Neither result is a bug. One gives you a misleading sentence, the other gives you nothing. Your pipeline has to treat both as "this page has no content yet".

Why matching the string is not enough

Create React App is no longer the default way to start a React app. The React team announced on February 14, 2025 that it is deprecated for new apps. It still works in maintenance mode, but it has no active maintainers. The team points new projects to frameworks such as Next.js, React Router and Expo, or to build tools such as Vite, Parcel and Rsbuild.

That matters for detection. The create-vite React TypeScript template has no <noscript> element. Its body is an empty root div and one module script. A shell built from it gives you an empty string, not a helpful sentence.

So use the sentence as a strong hint, not as the test. Check for the shell itself:

import re

NOSCRIPT_HINT = "you need to enable javascript to run this app"

def shell_reason(html: str) -> str | None:
    mount = re.search(r'<div id="(root|app)"[^>]*>\s*</div>', html)
    paragraphs = len(re.findall(r"<p[\s>]", html, re.I))
    if mount and NOSCRIPT_HINT in html.lower():
        return "app shell (template noscript text)"
    if mount and paragraphs < 2:
        return "app shell (empty mount point)"
    return None

with open("raw.html", encoding="utf-8", errors="replace") as f:
    print(shell_reason(f.read()))

The thresholds are a starting point. The important part is the return value: a reason you can log and route on, not a document you pass on.

What to do with the page

The order is the same as in the one-minute headless browser test: cheap checks first.

  1. Look for data in the HTML. Search raw.html for application/ld+json, og: tags or a feed link. Some shells still carry a summary there.
  2. Render only if nothing is there. A browser runs the bundle and builds the DOM. Wait for an element that holds the text, not for #root, which is there from the start.
  3. Or drop it. If the page is not worth a browser, record the reason and move on.

What you should never do is store the sentence as the page's text. A model asked to summarize it will summarize a noscript fallback. If the shell is one of several empty-response causes in your logs, tell the four causes apart first.

Where a static fetch API fits

Live Web Data API does a static fetch by default and returns the main content as Markdown, text or HTML. A page like this is flagged instead of passed on: a THIN_CONTENT warning, or a CRAWL_NOT_RENDERED error. Failed fetches cost 0 credits. Over REST and the SDKs you can resend that URL with render: true, or set renderFallback: true from the start so only pages that come back empty are rendered in a browser. The MCP tool does not render. To check whether a static fetch is enough for your URL, use the playground; the error codes are in the API reference.

Test it yourself

Take one URL that returned the sentence. Run the two grep lines above, then the Python check on the same raw.html. If both say app shell, search for embedded data next. If that finds nothing either, the page needs a browser.

Frequently asked questions

Where does "You need to enable JavaScript to run this app" come from?

It is the noscript text in the public/index.html template of Create React App. A site built from that template and left unchanged sends it in the first HTML response, next to an empty div with the id root.

Why does my scraper return only that sentence?

The page builds its content with JavaScript, so the server sent an app shell. The only text in that shell is the noscript fallback. A parser that reads noscript content as text, followed by a dump of every text node, returns that sentence as the whole page.

Does every client-rendered page show a noscript message?

No. The Vite React TypeScript template has no noscript element, so its shell holds no text at all. Check for an empty mount point and missing content instead of matching one sentence.

Will waiting longer or retrying the request fix it?

No. The server sends the same shell every time. The content only appears after scripts run, so you need a browser, an embedded data payload or another source for that page.

Sources

  1. cra-template public/index.html (Create React App) (Documentation)
  2. Sunsetting Create React App (react.dev) (Blog)
  3. create-vite template-react-ts index.html (Documentation)
  4. parse5 ParserOptions: scriptingEnabled (Documentation)
  5. Readability.js (Mozilla) (Documentation)