Skip to content
Barkhausen AI

DataBA-D-2026-05

University homepage JS-rendering census 2026 — raw-versus-rendered visible text

The 429 analyzable university homepages of BA-D-2026-02 (an HTTP 200 with a non-empty body on 2026-07-09), each read twice: the visible text of the stored 2026-07-09 raw HTML (what a non-rendering crawler saw) and document.body.innerText from a headless-Chrome render of the same final URL on 2026-07-10. One row per domain. Of the 429, 400 rendered cleanly (rendered_status = ok) and are the denominator for every rate in BA-D-2026-05; 17 timed out, 11 are suspected challenge/WAF blocks, and 1 hard-errored — those 29 carry an untrustworthy rendered-text count and are excluded from the headline rates.

Collection window: raw HTML 2026-07-09; headless render 2026-07-10 (00:41:04Z–00:51:19Z UTC) · License: CC-BY-4.0 · Published with BA-D-2026-05: The JavaScript wall at the university front door: a raw-versus-rendered census of homepage text

Download

CC-BY-4.0 · cite as a dataset with the parent report.

Schema

ColumnTypeDescription
domainstringThe registrable domain from the BA-D-2026-02 frame, lowercased. One row per domain, 429 rows.
requested_urlstringThe URL the headless browser navigated to: the BA-D-2026-02 final_url for the domain, or https://<domain>/ when that census recorded none.
raw_final_urlstringThe final URL of the stored 2026-07-09 raw fetch (BA-D-2026-02 final_url), i.e. the page from which raw_chars was extracted.
rendered_final_urlstringThe URL the page rested on after the 2026-07-10 headless navigation and any client-side redirect. May differ from raw_final_url when the page redirects in JavaScript; a chrome-error:// value marks a navigation that hard-errored before landing.
http_status_renderinteger or emptyHTTP status of the headless render's final response. Empty when no response was received — a navigation timeout (goto never resolved) or a hard error before any response.
rendered_statusstring (enum)Trustworthiness of the render: ok (navigation resolved, body trustworthy — the rate denominator), timeout (45 s navigation timeout; a best-effort, possibly partial body was still captured), challenge-suspected (an ok navigation whose body is <200 chars and either carries a bot-challenge/WAF-block title or overwrote a raw page that was itself substantial — a render-side block, not a homepage), or error (a navigation or script error). Only ok rows drive the headline rates.
nav_statusstring (enum)The raw navigation outcome from the renderer before the challenge-suspected upgrade: ok, timeout, or error. rendered_status equals nav_status except that an ok navigation returning a near-empty untrustworthy body is upgraded to challenge-suspected.
raw_charsintegerCharacter count of the visible text of the stored 2026-07-09 raw HTML, extracted with the BA-D-2026-02 parser (HomepageParser + decode_html) and whitespace-collapsed. Reproduces that census's visible_text_chars (0 mismatches over all 429). This is what a non-rendering crawler saw.
rendered_charsintegerCharacter count of document.body.innerText from the 2026-07-10 headless render, passed through the identical whitespace-collapse before counting, so raw_chars and rendered_chars share exactly the same counting semantics. On non-ok rows this count is not a trustworthy measurement of the site's rendered text (a challenge/block/partial body).
ratio_raw_over_renderednumber or emptyraw_chars / rendered_chars, rounded to 4 decimals. Empty when rendered_chars = 0 (division undefined). A value ≥ 1 means the raw parse counted at least as much text as the browser rendered — the common case, a definitional difference between the two extractors (see limitations), not JavaScript removing content.
delta_rendered_minus_rawintegerrendered_chars − raw_chars (signed). Negative when the raw parse counted more characters than the browser rendered.
js_lightboolean or emptytrue when ratio_raw_over_rendered ≥ 0.9 — raw and rendered text within 10%, effectively text-complete for a non-rendering crawler. Empty (nulled) on challenge-suspected and error rows, where the rendered body is untrustworthy and, being far below raw, would spuriously read as js_light. Populated on ok and timeout rows.
js_dependentboolean or emptytrue when rendered_chars > 0 AND raw_chars < 0.5 × rendered_chars — the homepage's visible text at least doubles once JavaScript runs, so a non-rendering crawler sees under half of it. Empty (nulled) on challenge-suspected and error rows; populated on ok and timeout rows.
hard_wallboolean or emptytrue when raw_chars < 200 AND rendered_chars ≥ 1000 — the unrendered page is essentially empty while the rendered page is substantial. Every hard_wall is by construction also js_dependent. A near-empty raw page has several causes (genuine client-side rendering, a bot-challenge shell served to the crawler, or a JavaScript redirect), so hard_wall is a ceiling on client-side rendering, not an estimate of it — read it together with raw_incapsula_waf and raw_client_redirect. Empty (nulled) on challenge-suspected and error rows; populated on ok and timeout rows.
raw_incapsula_wafbooleantrue when the stored 2026-07-09 raw HTML carries an Imperva/Incapsula challenge script (_Incapsula_Resource / Incapsula). Explains a near-empty raw page as a WAF challenge served to the crawler's user agent rather than client-side rendering. Recorded for every row; interpret it as a wall only when read together with a near-empty raw_chars.
raw_client_redirectbooleantrue when the stored 2026-07-09 raw HTML carries an inline location.href/replace/assign assignment or a meta refresh. A broad signal — analytics and consent snippets also call location.* — so it marks a redirect wall only when read together with a near-empty raw_chars; the 50 flagged rows are not 50 redirect walls.
raw_bytesintegerByte length of the stored 2026-07-09 raw HTML body. A ~212-byte value on a raw_incapsula_waf row is the size of the Incapsula challenge shell that replaced the page.
render_msintegerWall-clock milliseconds for the render of this domain (navigation + fixed 3 s settle). Timeout rows sit near 48000 (the 45 s navigation cap plus settle).
rendered_sha256stringSHA-256 of the whitespace-collapsed rendered text, pinning the exact rendered string this row counted (the full rendered text is not stored). e3b0c442… is the hash of the empty string, i.e. rendered_chars = 0.
titlestringdocument.title from the headless render, whitespace-collapsed and truncated to 200 chars. A bot-challenge/WAF title here ("403 Forbidden", "Access Blocked", "Request Rejected", "Just a moment…") is what flags a near-empty render as challenge-suspected.
render_errorstring or emptyThe renderer's error message for timeout and error rows (e.g. "Navigation timeout of 45000 ms exceeded", "net::ERR_HTTP2_PROTOCOL_ERROR"). Empty on ok and challenge-suspected rows.

Known limitations

FAQ

Why is ratio_raw_over_rendered ≥ 1 (raw larger than rendered) so often — did JavaScript delete text?
No. The raw parser counts every text node regardless of CSS, while the browser's innerText excludes text that is CSS-hidden at capture time (collapsed menus, off-screen navigation, display:none panels). A page with a large hidden navigation and footer therefore counts more in raw than in rendered. This is a definitional difference between the two extractors, not JavaScript removing content, and it is why the median ratio is above 1. The classifications that flag a JavaScript wall (js_dependent, hard_wall) fire only when raw is much smaller than rendered, so they are unaffected.
A domain is a hard_wall — does that mean the university runs a single-page app?
Not necessarily. hard_wall means the raw page was near-empty (<200 chars) and the rendered page substantial (≥1000 chars), which has three surface-identical causes: genuine client-side rendering, a bot-challenge shell served to the crawler's user agent (raw_incapsula_waf), and a JavaScript redirect (raw_client_redirect). Of the 6 ok hard walls, only 2 (uga.edu, msu.ru) are genuine client-side rendering; 2 are Incapsula shells (msu.edu, cityu.edu.hk) and 2 are redirect stubs (cuhk.edu.hk, uitm.edu.my). Read hard_wall together with the two raw-side signal columns.
What do the empty js_light / js_dependent / hard_wall cells mean?
The row's rendered_status is challenge-suspected or error, so the rendered body is not a trustworthy measurement. Those three booleans are pure functions of the character counts and would fire misleadingly on a block page or a failed render (a near-empty body against a substantial raw page would read as js_light), so they are left empty on those rows. timeout rows keep their booleans (they captured a best-effort body) but are still excluded from the headline rates. Only ok rows drive the rates.
Why are there challenge-suspected rows with an HTTP 200 status?
Some WAFs return their block or challenge page with a 200 status and an innocuous or explicit title (e.g. "Request Rejected", or the site's own name over a near-empty body). The challenge-suspected label fires when an ok navigation returns under 200 characters AND either the title matches a known block/challenge marker OR the stored raw page for that domain was itself substantial — the second arm catches a render-side block whose block page carries an innocuous title. It marks the render as untrustworthy regardless of the HTTP status.

Changelog

How to cite

Barkhausen AI (2026). University homepage JS-rendering census 2026 — raw-versus-rendered visible text (dataset). https://barkhausen.ai/data/js-rendering-2026/

BibTeX
@techreport{BA-D-2026-05,
  author       = {{Barkhausen AI}},
  title        = {University homepage JS-rendering census 2026 — raw-versus-rendered visible text (dataset)},
  institution  = {Barkhausen AI},
  year         = {2026},
  url          = {https://barkhausen.ai/data/js-rendering-2026/}
}

Published under the Creative Commons Attribution 4.0 International (CC-BY-4.0).