# Pages that need a browser

Some sites build their content with scripts; plain requests get an empty shell. A node can ask for NeoDownloader's browser: `"fetch": {"via": "browser", "wait": {"kind": "css", "expr": "..."}}`, and `grow` pages by scrolling and pressing.

**Measure before reaching for it**: a JSON island in the HTML (`outline.islands`) or an API the page calls is cheaper and steadier than a browser. `fetch` and `query` on the island first.

## The agent's page

`open_page(draft, url)` opens one page in NeoDownloader's own browser - the one the person signs in to. There is one page per agent; a new one closes the old. **Its size is fixed at 1280x900 (`viewport` in the answer) - the size of a run's page**, so the layout you read is the one the recipe will meet; `window.resizeTo` does nothing, and nothing needs it. Shown to the person, the page keeps that size and is drawn smaller. These tools need the browser, not the window: on a server without a window they still work. Only `page_show` needs the window; `get_environment` says what there is.

- `page_read(page)` gives a digest: links and images grouped by the shape of their address with a count and an example, JSON islands, buttons by caption, lines that look like a count ("1 of 43", "526 photos"). The DOM after scripts is kept as `dom_visit` - ask it with `query`. An island is asked through its `within`: `query(visit, within={"kind": "css", "expr": "script#__NEXT_DATA__"}, value={"kind": "json", "expr": "props.items[].url"})`. A recipe takes the same two steps: an `extract` of the island into a variable, then a value with `"in": "{that variable}"`.
- `exchanges` in the same answer are the XHR and Fetch the page made since the last read, each kept as a `visit`. **This is how the hidden API is found**: look at them before the markup. `try_step` replays on them like on anything fetched.
- `page_pick(page, hint)` turns a text on the page or a rough selector into selectors for the repeating element, with how many each catches page-wide and how many neighbours share it. `show: true` draws the set on a picture: the right number of the wrong pictures shows there.
- `page_act(page, scroll | press)` does what `grow` does; `grew` says whether the page answered. There is no typing: a recipe could not repeat it.
- `page_eval(page, js)` reads what lives in the page's memory. In a recipe the same is an inject file and a `call` value.
- `shot: true` on `page_read` adds a picture of the top of the page.

**An API only the browser is let into: call it from the page, do not scroll for it.** When the page's own requests pass and `fetch` is refused even with `session: true`, the check holds the browser itself, not its cookies. What worked in `page_eval` - `fetch('/api/...')` inside the page - goes into the recipe whole: a function in the recipe's `inject` file (`write_code(kind="page")`), a `call` value on a `via: browser` node, the answer read with `json`. A few calls to the API's pages are steadier than scrolling a feed and counting what it drew; a feed may scroll an inner box, not the window, and grow nothing.

**A warm page is not a cold recipe.** The page keeps what you did to it; a recipe starts from an address. `reached` says how the page got where it is - and `person` in it, or `touched: true`, means it moved since you left it: the person did something in the window, or the page did by itself. Check the end with `run_recipe`, which starts cold: it walks `via: browser` nodes in the same browser, under the same network rule and budget, and so does `try_step` with `live: true`. While a run is on a browser page, a page tool waits for it.

## Walls

A check, a captcha, a sign-in form: `open_page` and `page_read` name it in `wall`. It is the person's to pass. Call `page_show(page, why)` with a short sentence in the person's language, wait, and read the page again. After that `fetch(..., session: true)` sends the cookies the browser holds for that host - their names come back, their values never do. Never try to solve or route around a wall.

A browser is not the browser of the program when it is a different one: measure a browser path with NeoDownloader's own, not with another browser that announces itself differently.
