Recipe: JavaScript-Rendered Pages¶
Goal¶
Read a page whose content appears only after JavaScript runs in the browser, such as a single-page
app or a dashboard. Use the fast fetch_webpage first, and fall back to fetch_webpage_rendered
only when the page turns out to be an empty shell.
Prerequisites¶
- The
webextra forfetch_webpage(addextractfor main-content extraction). -
The
browserextra and Chromium forfetch_webpage_rendered:pip install "doc-processing-std-tools[web,extract,browser] @ git+https://github.com/rzoeb/doc-processing-std-tools.git@v0.6.1" playwright install chromium # in a container: playwright install --with-deps chromium -
fetch_webpage_renderedbelongs to the browser server, which is off unless enabled. You don't need the rest of the browser tools to use it (step 4).
Steps¶
Tool calls are written as Python calls; the model sends the same arguments as JSON.
1. Fetch the page normally¶
fetch_webpage(url="https://example.com/dashboard")
On a page built by JavaScript, the HTML the server sends holds little more than a title and an
empty <div id="root">, and the result says so with likely_js_rendered:
{
"title": "Results dashboard",
"extractor": "stdlib",
"text": "Results dashboard",
"total_chars": 17,
"likely_js_rendered": true
}
likely_js_rendered is true when the page has little text and several scripts, or an empty app
root such as <div id="root"></div>. fetch_webpage takes milliseconds and runs no JavaScript, so
it stays the first choice: most pages don't need a browser.
2. Fetch it again, rendered¶
fetch_webpage_rendered(url="https://example.com/dashboard")
Chromium loads the page, runs its JavaScript and returns the same JSON object as fetch_webpage,
plus "rendered": true:
{
"url": "https://example.com/dashboard",
"final_url": "https://example.com/dashboard",
"status": 200,
"content_type": "text/html",
"title": "Results dashboard",
"format": "text",
"extractor": "trafilatura",
"text": "Quarterly results\nRevenue grew 12% this quarter.\nDetails",
"truncated": false,
"total_chars": 56,
"offset": 0,
"next_offset": null,
"has_more": false,
"byte_cap_reached": false,
"rendered": true
}
It takes seconds rather than milliseconds. Each call uses a fresh browser context with no cookies or stored sign-ins, which is closed afterwards, so it reads pages as a signed-out visitor. For a page behind a sign-in, drive the browser tools instead (Portal Download to Text).
3. Ask for Markdown, links or the next part¶
The arguments match fetch_webpage's:
fetch_webpage_rendered(url="https://example.com/dashboard", format="markdown", include_assets=True)
fetch_webpage_rendered(url="https://example.com/dashboard", offset=10000) # the next part
# Quarterly results
Revenue grew 12% this quarter.
[Details](https://example.com/details)
With include_assets=True, the result also lists the page's links and images, with absolute
URLs.
4. Give an agent both tools¶
fetch_webpage's description already tells the model to retry with fetch_webpage_rendered when
likely_js_rendered is true and that tool is available. So attach the two tools, and nothing else
from the browser server:
from doc_processing_toolkit import Agent
from doc_processing_std_tools import browser_server, web_search_server
agent = Agent(
model="claude-sonnet-5",
tools=[
{"type": "fastmcp", "server": web_search_server, "allowed_tools": ["fetch_webpage"]},
{"type": "fastmcp", "server": browser_server, "allowed_tools": ["fetch_webpage_rendered"]},
],
)
Through the composed launcher, set DPTTOOLS_ENABLE_BROWSER=true in the server's env. That adds
all 19 browser tools (75 tools in all); use allowed_tools on the session to show the model only
the ones it needs.
Internal sites¶
fetch_webpage_rendered uses the browser's network policy, which refuses private and internal
addresses:
Error: Navigation refused: localhost resolves to a private or internal address; the browser may not reach it (add it to DPTTOOLS_BROWSER_ALLOW_HOSTS if it is a trusted internal host).
To read a trusted internal host, add its exact hostname to both allow-lists:
DPTTOOLS_WEB_FETCH_ALLOW_HOSTS for fetch_webpage and DPTTOOLS_BROWSER_ALLOW_HOSTS for
fetch_webpage_rendered. DPTTOOLS_BROWSER_ALLOWED_DOMAINS and DPTTOOLS_BROWSER_BLOCKED_DOMAINS
apply to the rendered fetch as well.
What to validate¶
fetch_webpageon the shell returnslikely_js_rendered: true, without the content.fetch_webpage_renderedreturnsrendered: true, the sametitle, and the content intext.format="markdown"keeps headings and links, andinclude_assets=Truelists the links with absolute URLs.- Paging works the same as
fetch_webpage's:offset,next_offset,has_more. - No browser session or tab is left open after a call.
- A private address is refused unless its hostname is in
DPTTOOLS_BROWSER_ALLOW_HOSTS, andfile:URLs are refused.
Test sources¶
tests/integration/test_fetch_rendered.py: the shell and rendered fetches of the same page, on real Chromium (skipped when Chromium is missing).tests/unit/test_fetch_webpage_structured.py(TestJsHint): whenlikely_js_renderedis set.
Related pages¶
- Web server: the result's fields, and the fetch's network policy.
- Browser server: the rest of the browser tools, and its settings.