Skip to content

Commit 0f928aa

Browse files
authored
feat: add support for browser rendering (#271)
1 parent bc269e5 commit 0f928aa

File tree

22 files changed

+1480
-573
lines changed

22 files changed

+1480
-573
lines changed

docs/content/0.index.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ hero:
1010
light: '/images/landing/hero-light.svg'
1111
dark: '/images/landing/hero-dark.svg'
1212
headline:
13-
label: Using AI for User Experience
14-
to: /blog/cloudflare-ai-for-user-experience
13+
label: Browser rendering is available
14+
to: /changelog/hub-browser
1515
icon: i-ph-arrow-right
1616
links:
1717
- label: Start reading docs

docs/content/1.docs/2.features/ai.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,11 @@ NuxtHub AI is compatible with some functions of the [Vercel AI SDK](https://sdk
169169
Make sure to install the Vercel AI SDK in your project.
170170

171171
```[Terminal]
172-
npx nypm add ai @ai-sdk/vue
172+
npx ni ai @ai-sdk/vue
173173
```
174174

175175
::note
176-
[`nypm`](https://github.com/unjs/nypm) will detect your package manager and install the dependencies with it.
176+
[`ni`](https://github.com/antfu/ni) will detect your package manager and install the dependencies with it.
177177
::
178178

179179
### `useChat()`
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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+
```

docs/content/1.docs/3.recipes/5.postgres.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The module ensures that you can connect to your PostgreSQL database using [Cloud
3131
2. Install the [`postgres`](https://www.npmjs.com/package/postgres) NPM package in your project.
3232

3333
```bash
34-
npx nypm add postgres
34+
npx ni postgres
3535
```
3636

3737
::tip{icon="i-ph-rocket-launch"}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
title: Browser Rendering is available
3+
description: "Taking screenshots, crawling websites, extracting information has never been easier with `hubBrowser()`."
4+
date: 2024-08-28
5+
image: '/images/changelog/nuxthub-browser.jpg'
6+
authors:
7+
- name: Sebastien Chopin
8+
avatar:
9+
src: https://avatars.githubusercontent.com/u/904724?v=4
10+
to: https://x.com/atinux
11+
username: atinux
12+
---
13+
14+
::tip
15+
This feature is available on both [free and pro plans](/pricing) of NuxtHub but on the [Workers Paid plan](https://www.cloudflare.com/plans/developer-platform/) for your Cloudflare account.
16+
::
17+
18+
We are excited to introduce [`hubBrowser()`](/docs/features/browser). This new method allows you to run a headless browser directly in your Nuxt application using [Puppeteer](https://github.com/puppeteer/puppeteer).
19+
20+
::video{poster="https://res.cloudinary.com/nuxt/video/upload/v1725901706/nuxthub/nuxthub-browser_dsn1m1.jpg" controls class="w-full h-auto rounded"}
21+
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1725901706/nuxthub/nuxthub-browser_dsn1m1.webm" type="video/webm"}
22+
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1725901706/nuxthub/nuxthub-browser_dsn1m1.mov" type="video/mp4"}
23+
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1725901706/nuxthub/nuxthub-browser_dsn1m1.ogg" type="video/ogg"}
24+
::
25+
26+
## How to use hubBrowser()
27+
28+
1. Update `@nuxthub/core` to the latest version (`v0.7.11` or later)
29+
30+
2. Enable `hub.browser` in your `nuxt.config.ts`
31+
32+
```ts [nuxt.config.ts]
33+
export default defineNuxtConfig({
34+
hub: {
35+
browser: true
36+
}
37+
})
38+
```
39+
40+
3. Install the required dependencies
41+
42+
```bash [Terminal]
43+
npx ni @cloudflare/puppeteer puppeteer
44+
```
45+
46+
4. Start using [`hubBrowser()`](/docs/features/browser) in your server routes
47+
48+
```ts [server/api/screenshot.ts]
49+
export default eventHandler(async (event) => {
50+
const { page } = await hubBrowser()
51+
52+
await page.setViewport({ width: 1920, height: 1080 })
53+
await page.goto('https://cloudflare.com')
54+
55+
setHeader(event, 'content-type', 'image/jpeg')
56+
return page.screenshot()
57+
})
58+
```
59+
60+
5. Before deploying, make sure you are subscribed to the [Workers Paid plan](https://www.cloudflare.com/plans/developer-platform/)
61+
62+
6. [Deploy your project with NuxtHub](/docs/getting-started/deploy)
63+
64+
::note{to="/docs/features/browser"}
65+
Read the documentation about `hubBrowser()` with more examples.
66+
::

docs/pages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ onMounted(() => {
7070
</template>
7171

7272
<template #title>
73-
<MDC :value="page?.hero.title" />
73+
<span v-html="page?.hero.title" />
7474
</template>
7575

7676
<template #description>
175 KB
Loading

playground/app/app.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ useSeoMeta({
1818
const links = [
1919
{ label: 'Home', to: '/' },
2020
{ label: 'AI', to: '/ai' },
21+
{ label: 'Browser', to: '/browser' },
22+
{ label: 'Blob', to: '/blob' },
2123
{ label: 'Database', to: '/database' },
22-
{ label: 'KV', to: '/kv' },
23-
{ label: 'Blob', to: '/blob' }
24+
{ label: 'KV', to: '/kv' }
2425
]
2526
</script>
2627

0 commit comments

Comments
 (0)