# On-Demand and Mixed Rendering

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Rendering
Source: https://www.devtutorials.ai/learn/astro/rendering/on-demand-rendering

> How on-demand rendering works, server adapters, Astro.request, Astro.url, Astro.response, Astro.redirect(), returning a Response, dynamic routes without getStaticPaths(), the prerender export, mixing static and on-demand pages, a decision guide, request security, the 500 page and streaming.

## On-Demand Rendering

- **On-demand rendering** = a server makes a page's HTML when a visitor asks for it.
- Each request can give a different page: a search result, a cart, a "Hello, Asha".
- Astro does this with an **adapter** and one line per page.
- In this lesson, Asha adds pages that react to each visitor, while the rest of her site stays static.

**Simple lesson:** on-demand pages are made per request, so they can use the request.

### A Real-World Example

- Asha adds a search box to her blog.
- The results depend on what the reader types, so they cannot be built ahead of time.
- She adds the Node adapter and marks only the search page as on-demand.
- Her posts stay static and fast. Only search runs on the server.

Press **▶ Run**. Type a word in the box and press **Search**.

<form method="get">
    <input name="q" value={query} placeholder="Try: astro" />
    <button>Search</button>
</form>

{query && <p>{results.length} result(s) for "{query}"</p>}
<ul>{results.map((title) => <li>{title}</li>)}</ul>
`,
  }}
/>

What happened:

- `export const prerender = false` made the page on-demand.
- The form sent `?q=...` in the address. The page read it with `Astro.url.searchParams`.
- Every search is a new request. The **Terminal** shows each one.
- Lines tagged `[server]` come from the server, while it handles a request. A `console.log` in an on-demand page prints there too, not in the browser.

### What You Will Learn

In this lesson, you will learn:

1. How a request becomes a response.
2. What adapters do, and how to add one.
3. How to read the request and change the response.
4. How to redirect, or return a `Response` yourself.
5. How dynamic routes work without `getStaticPaths()`.
6. How to mix static and on-demand pages with `prerender`.
7. How to choose a mode for each page.
8. How Astro protects forms, and how to show a 500 page.

## How On-Demand Rendering Works

### Request, Render and Response

1. A visitor's browser sends a **request**: the address, the method (GET or POST), headers and cookies.
2. The server runs the page's component script with that request.
3. The server sends back a **response**: a status code, headers and the HTML.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-req" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-req">Animation: a request travels from the browser to the server, the server runs the page, and a response with HTML travels back.</title>
  <defs><marker id="rq-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="20" y="50" width="150" height="100" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="95" y="96" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Browser</text>
  <text x="95" y="116" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 11 }}>/search?q=astro</text>
  <rect x="470" y="50" width="150" height="100" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} />
  <text x="545" y="90" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Server</text>
  <text x="545" y="110" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 11 }}>runs search.astro</text>
  <path d="M170 80 H466" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#rq-arrow)" />
  <path d="M470 125 H174" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, fill: "none" }} markerEnd="url(#rq-arrow)" />
  <text x="320" y="72" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>1. Request: GET, address, cookies</text>
  <text x="320" y="146" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>3. Response: 200, HTML</text>
  <circle r="7" cy="80" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="170;466;466" keyTimes="0;0.4;1" dur="4s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.4;0.41;1" dur="4s" repeatCount="indefinite" /></circle>
  <circle r="7" cy="125" style={{ fill: "var(--ngc-green)" }}><animate attributeName="cx" values="470;470;174" keyTimes="0;0.6;1" dur="4s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.59;0.6;1" dur="4s" repeatCount="indefinite" /></circle>
  <text x="545" y="178" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12 }}>2. Render</text>
</svg>
<figcaption>Each visit is one request and one response.</figcaption>
</figure>

### Static Files vs a Running Server

- A static page is a file. The host sends it without running anything.
- An on-demand page needs a **running server** that executes your code.
- That server costs more and may be a little slower. Use it only where a page needs it.

**Simple lesson:** the server runs your page once per request, and sends back a response.

## Server Adapters

### What Adapters Do

- **Adapter** = a package that lets Astro's server code run on a specific host.
- Each host runs servers differently. The adapter does the translation.

### Official Adapters

- `@astrojs/node` = your own server, or any host that runs Node.js.
- `@astrojs/vercel`, `@astrojs/netlify`, `@astrojs/cloudflare` = those hosts' serverless platforms.

### Choosing an Adapter

- Pick the adapter of the host you will deploy to.
- Not sure yet? Start with `@astrojs/node`. You can switch later with one line.

Your pages do not change when you switch adapters. Only the `adapter` line in the config changes, so the choice is easy to undo.

### Adding an Adapter with astro add

```bash
npx astro add node
```

- It installs the package and sets `adapter` in the config:

```js
import node from "@astrojs/node";

export default defineConfig({
  adapter: node({ mode: "standalone" }),
});
```

- `mode: "standalone"` builds a server you start with `node ./dist/server/entry.mjs`.

Press **▶ Run**. This page asks to be on demand, but there is no adapter. Read the **Terminal**, then add the adapter lines from above.

`,
  }}
/>

**Simple lesson:** one adapter per host. `npx astro add <host>` sets it up.

## Working with Requests and Responses

### Astro.request

- `Astro.request` is a standard web **Request** object.
- `Astro.request.method` = `"GET"`, `"POST"` and so on.
- `Astro.request.headers.get("user-agent")` = one request header.
- `await Astro.request.formData()` = the fields of a submitted form.

### Astro.url

- `Astro.url` is a **URL** object for the current address.
- `Astro.url.pathname` = the path, such as `/search`.
- `Astro.url.searchParams.get("q")` = one value from the query string.

### Astro.clientAddress

- The visitor's IP address, as the server sees it. Some adapters do not provide it.

### Astro.response

- Change the response before it is sent:
  - `Astro.response.status = 404;`
  - `Astro.response.headers.set("Cache-Control", "max-age=60");`

### Astro.redirect()

- `return Astro.redirect("/login");` sends the visitor somewhere else.
- The default status is `302` (temporary). Pass `301` for a permanent move.
- Use `return`, so the rest of the page does not run.

Press **▶ Run**. Fill the form and send it. Then click the "old" link.

<p>Method: {Astro.request.method} · Path: {Astro.url.pathname}</p>
<form method="post">
    <input name="name" placeholder="Your name" />
    <button>Send</button>
</form>
{message && <p><strong>{message}</strong></p>}
<p><a href="/contact-old">An old link to this page</a></p>
`,
    "src/pages/contact-old.astro": `---
return Astro.redirect("/contact", 301);
---
`,
  }}
/>

- Send the form empty: the status becomes `400`. The **Terminal** shows it.
- The old link answers with a `301` redirect to `/contact`.

### Returning a Response from a Page

- A page can `return new Response(...)` instead of HTML.
- Use it for a plain-text or JSON answer, or a custom status.
- Example: `return new Response("Not found", { status: 404 });`

### Fetching Fresh Data on Every Request

- In an on-demand page, `fetch()` runs on each visit.
- Readers always see the latest data. Each visit also waits for the API.

### HTML Streaming

- **Streaming** = the server sends the HTML in pieces, as each piece is ready.
- Astro streams on-demand pages by default. The top of the page can show while a slow part below is still loading.
- Put slow `fetch()` calls in components lower on the page, not all at the top.

**Simple lesson:** read `Astro.request` and `Astro.url`, change `Astro.response`, and `return` a redirect or a `Response` when needed.

## On-Demand Dynamic Routes

### Dynamic Routes Without getStaticPaths()

- In a static page, `getStaticPaths()` must list every page ahead of time.
- In an on-demand page, there is no list. Any value in the address is accepted.
- The page reads `Astro.params` and looks up the data on each request.
- If nothing is found, return a 404.

<p>♥ {post.likes} likes, fetched just now.</p>
<p>
    <a href="/posts/props-made-simple">Another post</a> ·
    <a href="/posts/does-not-exist">A missing post</a>
</p>
`,
  }}
/>

On-demand dynamic routes have no `getStaticPaths()`. Instead of a list, you must handle "not found" yourself.

**Simple lesson:** on-demand `[slug]` pages accept any value. Look it up, and return a 404 when it is missing.

## Mixed Rendering

### Why Mix

- Most pages of a site are the same for everyone: static is fastest.
- A few pages need the request: on-demand is needed.
- **Mixed rendering** = both in one project. Static speed where possible, on-demand where needed.

### The prerender Export

- `export const prerender = false;` = render this page on demand.
- `export const prerender = true;` = build this page ahead of time.
- The `output` setting decides the default. `prerender` changes it for one page.

### Two Ways to Mix

- **A static project with some on-demand pages** = the default `output: "static"`, plus `prerender = false` on those pages. Most sites look like this.
- **An on-demand project with some static pages** = `output: "server"`, plus `prerender = true` on pages that never change.

Press **▶ Run**. Read the **Terminal**: `▶` pages were built now, `λ` pages run on each request.

<ul>
    <li><a href="/about">About (static)</a></li>
    <li><a href="/stats">Live stats (on demand)</a></li>
</ul>
`,
    "src/pages/about.astro": `<h1>About</h1>
<p>Same for everyone. Built once.</p>
<a href="/">Home</a>
`,
    "src/pages/stats.astro": `---
const res = await fetch("https://api.devjournal.dev/posts");
const posts = await res.json();
const total = posts.reduce((sum, p) => sum + p.likes, 0);
---

<h1>Live stats</h1>
<p>{total} likes across {posts.length} posts, as of this request.</p>
<a href="/">Home</a>
`,
  }}
/>

## Choosing the Right Mode

### A Decision Guide

Ask one question for each page: **when does its content change?**

1. **Per visitor** (their name, their cart, their settings) → on-demand.
2. **Per request** (search results, query strings, form answers) → on-demand.
3. **Every few minutes** (live scores, stock levels) → on-demand, perhaps with caching.
4. **Rarely** (posts, docs, about pages) → static.

### A Comparison Table

<div className="tscroll">

| Point | Static | On-demand | Mixed |
|---|---|---|---|
| Hosting | Any static host | A server or serverless host with an adapter | A host with an adapter |
| Speed | Fastest | Depends on the server and APIs | Fast where static, fresh where needed |
| Data freshness | As of the last build | Every request | Each page chooses |
| Cost | Lowest | Higher: code runs per visit | Low: only some pages run code |
| Request data | No | Yes | Only on on-demand pages |

</div>

**Simple lesson:** start static. Make a page on-demand only when its content depends on the visitor or the request.

## Request Security and Error Pages

### Cross-Site Request Forgery Protection

- **CSRF** (cross-site request forgery) = a bad website tricks a visitor's browser into sending a form to **your** site.
- Astro checks this for on-demand pages. It is on by default: `security.checkOrigin`.
- A form `POST` that comes from a different website is refused.

### What the 403 Response Means

- **403 Forbidden** = "I understood the request, but I refuse it".
- If a real form gets a 403, check that it is sent from your own site's address.

### The 500 Page

- **500** = "the server had an error".
- Make `src/pages/500.astro`. Astro shows it when an on-demand page throws an error.
- It gets an `error` prop. Do **not** show error details to visitors on a live site. Log them instead.

Press **▶ Run**. The broken page throws an error, so the 500 page is shown.

<p>Sorry, this page is having trouble. Please try again soon.</p>
<a href="/">Go home</a>
`,
    "src/pages/index.astro": `<h1>Home</h1>
`,
  }}
  expectWarning
/>

On a live site, never print `error.message` on the 500 page. It can reveal details about your server. Show a friendly message and log the error for yourself.

**Simple lesson:** Astro blocks cross-site form posts with a 403. A `500.astro` page catches server errors kindly.

## TL;DR

1. **On-demand rendering** = HTML made per request on a server.
2. **Adapter** = runs Astro on your host. `npx astro add node` (or vercel, netlify, cloudflare).
3. **Astro.request** = method, headers, `formData()`. **Astro.url** = pathname, `searchParams`.
4. **Astro.response** = set `status` and `headers`.
5. **Astro.redirect(path, status)** = `return` it. 302 by default.
6. **return new Response(...)** = a custom answer, such as a 404.
7. **On-demand [slug]** = no `getStaticPaths()`. Handle "not found" yourself.
8. **prerender** = `false` for on-demand, `true` for static, per page.
9. **Choose** = static unless the content depends on the visitor or the request.
10. **Security** = `checkOrigin` refuses cross-site form posts with a 403. **500.astro** = the error page.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `Astro.redirect()` without `return` | Calling it stops the page | Write `return Astro.redirect(...)` | The page keeps running and may fail |
| No adapter | Astro has a built-in server | Add one with `astro add` | The build stops |
| `getStaticPaths()` in an on-demand page | Every dynamic page needs it | On-demand pages read `Astro.params` | It is ignored, with a warning |
| Not handling a missing item | Every address has data | Return a 404 when nothing is found | The page crashes with a 500 |
| Making the whole site on demand | It is more modern | Mix: on-demand only where needed | Slower pages, higher bills |
| Showing `error.message` to visitors | It helps users | Log it, show a friendly message | Server details leak |

</div>

## Check Yourself

## Exercises

## What to Remember

- On-demand pages run on a server, once per request. They need an adapter.
- Read the request with `Astro.request` and `Astro.url`. Change the answer with `Astro.response`.
- `return Astro.redirect()` or `return new Response()` to answer differently.
- Mix modes with `prerender`. Start static, opt in where needed.
- Astro blocks cross-site form posts. A `500.astro` page handles server errors.

Next: **Astro Islands**, where Asha adds interactive components from Preact, with JavaScript only where they live.
