|
| 1 | +--- |
| 2 | +title: Browser Rendering |
| 3 | +navigation.title: Browser |
| 4 | +description: Control and interact with a headless browser instance in your Nuxt application using Puppeteer. |
| 5 | +--- |
| 6 | + |
| 7 | +## Getting Started |
| 8 | + |
| 9 | +Enable browser rendering in your Nuxt project by enabling the `hub.browser` option: |
| 10 | + |
| 11 | +```ts [nuxt.config.ts] |
| 12 | +export default defineNuxtConfig({ |
| 13 | + hub: { |
| 14 | + browser: true |
| 15 | + }, |
| 16 | +}) |
| 17 | +``` |
| 18 | + |
| 19 | +Lastly, install the required dependencies by running the following command: |
| 20 | + |
| 21 | +```bash [Terminal] |
| 22 | +npx ni @cloudflare/puppeteer puppeteer |
| 23 | +``` |
| 24 | + |
| 25 | +::note |
| 26 | +[ni](https://github.com/antfu/ni) will automatically detect the package manager you are using and install the dependencies. |
| 27 | +:: |
| 28 | + |
| 29 | +## Usage |
| 30 | + |
| 31 | +In your server API routes, you can use the `hubBrowser` function to get a [Puppeteer browser instance](https://github.com/puppeteer/puppeteer): |
| 32 | + |
| 33 | +```ts |
| 34 | +const { page, browser } = await hubBrowser() |
| 35 | +``` |
| 36 | + |
| 37 | +In production, the instance will be from [`@cloudflare/puppeteer`](https://developers.cloudflare.com/browser-rendering/platform/puppeteer/) which is a fork of Puppeteer with version specialized for working within Cloudflare workers. |
| 38 | + |
| 39 | +::tip |
| 40 | +NuxtHub will automatically close the `page` instance when the response is sent as well as closing or disconnecting the `browser` instance when needed. |
| 41 | +:: |
| 42 | + |
| 43 | +## Use Cases |
| 44 | + |
| 45 | +Here are some use cases for using a headless browser like Puppeteer in your Nuxt application: |
| 46 | +- **Web scraping:** Extract data from websites, especially those with dynamic content that requires JavaScript execution. |
| 47 | +- **Generating PDFs or screenshots:** Create snapshots or PDF versions of web pages. |
| 48 | +- **Performance monitoring:** Measure load times, resource usage, and other performance metrics of web applications. |
| 49 | +- **Automating interactions or testing:** Simulating user actions on websites for tasks like form filling, clicking buttons, or navigating through multi-step processes. |
| 50 | + |
| 51 | +## Limits |
| 52 | + |
| 53 | +::important |
| 54 | +Browser rendering is only available on the [Workers Paid](https://www.cloudflare.com/plans/developer-platform/) plan for now. |
| 55 | +:: |
| 56 | + |
| 57 | +To improve the performance in production, NuxtHub will reuse browser sessions. This means that the browser will stay open after each request (for 60 seconds), a new request will reuse the same browser session if available or open a new one. |
| 58 | + |
| 59 | +The Cloudflare limits are: |
| 60 | +- 2 new browsers per minute per Cloudflare account |
| 61 | +- 2 concurrent browser sessions per account |
| 62 | +- a browser instance gets killed if no activity is detected for 60 seconds (idle timeout) |
| 63 | + |
| 64 | +You can extend the idle timeout by giving the `keepAlive` option when creating the browser instance: |
| 65 | + |
| 66 | +```ts |
| 67 | +// keep the browser instance alive for 120 seconds |
| 68 | +const { page, browser } = await hubBrowser({ keepAlive: 120 }) |
| 69 | +``` |
| 70 | + |
| 71 | +The maximum idle timeout is 600 seconds (10 minutes). |
| 72 | + |
| 73 | +::tip |
| 74 | +Once NuxtHub supports [Durable Objects](https://github.com/nuxt-hub/core/issues/50), you will be able to create a single browser instance that will stay open for a long time, and you will be able to reuse it across requests. |
| 75 | +:: |
| 76 | + |
| 77 | +## Screenshot Capture |
| 78 | + |
| 79 | +Taking a screenshot of a website is a common use case for a headless browser. Let's create an API route to capture a screenshot of a website: |
| 80 | + |
| 81 | +```ts [server/api/screenshot.ts] |
| 82 | +import { z } from 'zod' |
| 83 | + |
| 84 | +export default eventHandler(async (event) => { |
| 85 | + // Get the URL and theme from the query parameters |
| 86 | + const { url, theme } = await getValidatedQuery(event, z.object({ |
| 87 | + url: z.string().url(), |
| 88 | + theme: z.enum(['light', 'dark']).optional().default('light') |
| 89 | + }).parse) |
| 90 | + |
| 91 | + // Get a browser session and open a new page |
| 92 | + const { page } = await hubBrowser() |
| 93 | + |
| 94 | + // Set the viewport to full HD & set the color-scheme |
| 95 | + await page.setViewport({ width: 1920, height: 1080 }) |
| 96 | + await page.emulateMediaFeatures([{ |
| 97 | + name: 'prefers-color-scheme', |
| 98 | + value: theme |
| 99 | + }]) |
| 100 | + |
| 101 | + // Go to the URL and wait for the page to load |
| 102 | + await page.goto(url, { waitUntil: 'domcontentloaded' }) |
| 103 | + |
| 104 | + // Return the screenshot as response |
| 105 | + setHeader(event, 'content-type', 'image/jpeg') |
| 106 | + return page.screenshot() |
| 107 | +}) |
| 108 | +``` |
| 109 | + |
| 110 | +On the application side, we can create a simple form to call our API endpoint: |
| 111 | + |
| 112 | +```vue [pages/capture.vue] |
| 113 | +<script setup> |
| 114 | +const url = ref('https://hub.nuxt.com') |
| 115 | +const image = ref('') |
| 116 | +const theme = ref('light') |
| 117 | +const loading = ref(false) |
| 118 | +
|
| 119 | +async function capture { |
| 120 | + if (loading.value) return |
| 121 | + loading.value = true |
| 122 | + const blob = await $fetch('/api/browser/capture', { |
| 123 | + query: { |
| 124 | + url: url.value, |
| 125 | + theme: theme.value |
| 126 | + } |
| 127 | + }) |
| 128 | + image.value = URL.createObjectURL(blob) |
| 129 | + loading.value = false |
| 130 | +} |
| 131 | +</script> |
| 132 | +
|
| 133 | +<template> |
| 134 | + <form @submit.prevent="capture"> |
| 135 | + <input v-model="url" type="url" /> |
| 136 | + <select v-model="theme"> |
| 137 | + <option value="light">Light</option> |
| 138 | + <option value="dark">Dark</option> |
| 139 | + </select> |
| 140 | + <button type="submit" :disabled="loading"> |
| 141 | + {{ loading ? 'Capturing...' : 'Capture' }} |
| 142 | + </button> |
| 143 | + <img v-if="image && !loading" :src="image" style="aspect-ratio: 16/9;" /> |
| 144 | + </form> |
| 145 | +</template> |
| 146 | +``` |
| 147 | + |
| 148 | +That's it! You can now capture screenshots of websites using Puppeteer in your Nuxt application. |
| 149 | + |
| 150 | +### Storing the screenshots |
| 151 | + |
| 152 | +You can store the screenshots in the Blob storage: |
| 153 | + |
| 154 | +```ts |
| 155 | +const screenshot = await page.screenshot() |
| 156 | + |
| 157 | +// Upload the screenshot to the Blob storage |
| 158 | +const filename = `screenshots/${url.value.replace(/[^a-zA-Z0-9]/g, '-')}.jpg` |
| 159 | +const blob = await hubBlob().put(filename, screenshot) |
| 160 | +``` |
| 161 | + |
| 162 | +::note{to="/docs/features/blob"} |
| 163 | +Learn more about the Blob storage. |
| 164 | +:: |
| 165 | + |
| 166 | +## Metadata Extraction |
| 167 | + |
| 168 | +Another common use case is to extract metadata from a website. |
| 169 | + |
| 170 | +```ts [server/api/metadata.ts] |
| 171 | +import { z } from 'zod' |
| 172 | + |
| 173 | +export default eventHandler(async (event) => { |
| 174 | + // Get the URL from the query parameters |
| 175 | + const { url } = await getValidatedQuery(event, z.object({ |
| 176 | + url: z.string().url() |
| 177 | + }).parse) |
| 178 | + |
| 179 | + // Get a browser instance and navigate to the url |
| 180 | + const { page } = await hubBrowser() |
| 181 | + await page.goto(url, { waitUntil: 'networkidle0' }) |
| 182 | + |
| 183 | + // Extract metadata from the page |
| 184 | + const metadata = await page.evaluate(() => { |
| 185 | + const getMetaContent = (name) => { |
| 186 | + const element = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`) |
| 187 | + return element ? element.getAttribute('content') : null |
| 188 | + } |
| 189 | + |
| 190 | + return { |
| 191 | + title: document.title, |
| 192 | + description: getMetaContent('description') || getMetaContent('og:description'), |
| 193 | + favicon: document.querySelector('link[rel="shortcut icon"]')?.href |
| 194 | + || document.querySelector('link[rel="icon"]')?.href, |
| 195 | + ogImage: getMetaContent('og:image'), |
| 196 | + origin: document.location.origin |
| 197 | + } |
| 198 | + }) |
| 199 | + |
| 200 | + return metadata |
| 201 | +}) |
| 202 | +``` |
| 203 | + |
| 204 | +Visiting `/api/metadata?url=https://cloudflare.com` will return the metadata of the website: |
| 205 | + |
| 206 | +```json |
| 207 | +{ |
| 208 | + "title": "Connect, Protect and Build Everywhere | Cloudflare", |
| 209 | + "description": "Make employees, applications and networks faster and more secure everywhere, while reducing complexity and cost.", |
| 210 | + "favicon": "https://www.cloudflare.com/favicon.ico", |
| 211 | + "ogImage": "https://cf-assets.www.cloudflare.com/slt3lc6tev37/2FNnxFZOBEha1W2MhF44EN/e9438de558c983ccce8129ddc20e1b8b/CF_MetaImage_1200x628.png", |
| 212 | + "origin": "https://www.cloudflare.com" |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +To store the metadata of a website, you can use the [Key Value Storage](/docs/features/kv). |
| 217 | + |
| 218 | +Or directly leverage [Caching](/docs/features/cache) on this API route: |
| 219 | + |
| 220 | +```ts [server/api/metadata.ts] |
| 221 | +export default cachedEventHandler(async (event) => { |
| 222 | + // ... |
| 223 | +}, { |
| 224 | + maxAge: 60 * 60 * 24 * 7, // 1 week |
| 225 | + swr: true, |
| 226 | + // Use the URL as key to invalidate the cache when the URL changes |
| 227 | + // We use btoa to transform the URL to a base64 string |
| 228 | + getKey: (event) => btoa(getQuery(event).url), |
| 229 | +}) |
| 230 | +``` |
0 commit comments