Skip to content

Commit 2b467f1

Browse files
authored
feat: add support for ALS (#272)
1 parent 116283e commit 2b467f1

File tree

11 files changed

+295
-77
lines changed

11 files changed

+295
-77
lines changed

docs/content/1.docs/1.getting-started/3.deploy.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ You only need to create these resources if you have explicitly enabled them in t
139139
Then, create a [Cloudflare Pages project](https://dash.cloudflare.com/?to=/:account/pages/new/provider/github) and link your GitHub or Gitlab repository and choose the Nuxt Framework preset in the build settings.
140140

141141
Once your project is created, open the `Settings -> Functions` tab and set:
142-
- Placement: Smart
143142
- KV namespace bindings
144143
- Set the variable name as `KV` and select your KV namespace created
145144
- Set the variable name as `CACHE` and select your KV namespace for caching created
@@ -149,7 +148,10 @@ Once your project is created, open the `Settings -> Functions` tab and set:
149148
- Set the variable name as `DB` and select your D1 database created
150149
- AI bindings
151150
- Set the variable name as `AI`
152-
151+
- Browser bindings
152+
- Set the variable name as `BROWSER`
153+
- Compatibility flags
154+
- Add the `nodejs_compat` and `nodejs_als` flags
153155

154156
Go back to the `Deployment` tab and retry the last deployment by clicking on `...` then `Retry deployment`.
155157

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,115 @@ export default cachedEventHandler(async (event) => {
228228
getKey: (event) => btoa(getQuery(event).url),
229229
})
230230
```
231+
232+
## PDF Generation
233+
234+
You can also generate PDF using `hubBrowser()`, this is useful if you want to generate an invoice or a receipt for example.
235+
236+
Let's create a `/_invoice` page with Vue that we will use as template to generate a PDF:
237+
238+
```vue [pages/_invoice.vue]
239+
<script setup>
240+
definePageMeta({
241+
layout: 'blank'
242+
})
243+
244+
// TODO: Fetch data from API instead of hardcoding
245+
const currentDate = ref(new Date().toLocaleDateString())
246+
const items = ref([
247+
{ name: 'Item 1', quantity: 2, price: 10.00 },
248+
{ name: 'Item 2', quantity: 1, price: 15.00 },
249+
{ name: 'Item 3', quantity: 3, price: 7.50 }
250+
])
251+
const total = computed(() => {
252+
return items.value.reduce((sum, item) => sum + item.quantity * item.price, 0)
253+
})
254+
</script>
255+
256+
<template>
257+
<div style="font-family: Arial, sans-serif; padding: 20px;">
258+
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
259+
<div>
260+
<p><strong>Invoice To:</strong></p>
261+
<p>John Doe</p>
262+
<p>123 Main St</p>
263+
<p>Anytown, USA 12345</p>
264+
</div>
265+
<div>
266+
<p><strong>Invoice Number:</strong> INV-001</p>
267+
<p><strong>Date:</strong> {{ currentDate }}</p>
268+
</div>
269+
</div>
270+
<table>
271+
<thead>
272+
<tr>
273+
<th>Item</th>
274+
<th>Qty</th>
275+
<th>Price</th>
276+
<th>Total</th>
277+
</tr>
278+
</thead>
279+
<tbody>
280+
<tr v-for="(item, index) in items" :key="index">
281+
<td>{{ item.name }}</td>
282+
<td>{{ item.quantity }}</td>
283+
<td>${{ item.price.toFixed(2) }}</td>
284+
<td>${{ (item.quantity * item.price).toFixed(2) }}</td>
285+
</tr>
286+
</tbody>
287+
</table>
288+
<div style="text-align: right; margin-top: 20px;">
289+
<p><strong>Total: ${{ total.toFixed(2) }}</strong></p>
290+
</div>
291+
</div>
292+
</template>
293+
294+
<style scoped>
295+
table { width: 100%; border-collapse: collapse; }
296+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
297+
th { background-color: #f2f2f2; }
298+
</style>
299+
```
300+
301+
To avoid having any styling issues, we recommend to keep your `app.vue` as minimal as possible:
302+
303+
```vue [app.vue]
304+
<template>
305+
<NuxtLayout>
306+
<NuxtPage />
307+
</NuxtLayout>
308+
</template>
309+
```
310+
311+
And move most of your head management, style & HTML structure in [`layouts/default.vue`](https://nuxt.com/docs/guide/directory-structure/layouts#default-layout).
312+
313+
Lastly, we need to create a `layouts/blank.vue` to avoid having any layout on our `_invoice` page:
314+
315+
```vue [layouts/blank.vue]
316+
<template>
317+
<slot />
318+
</template>
319+
```
320+
321+
This will ensure that no header, footer or any other layout elements are rendered.
322+
323+
Now, let's create our server route to generate the PDF:
324+
325+
```ts [server/routes/invoice.pdf.ts]
326+
export default eventHandler(async (event) => {
327+
const { page } = await hubBrowser()
328+
await page.goto(`${getRequestURL(event).origin}/_invoice`)
329+
330+
setHeader(event, 'Content-Type', 'application/pdf')
331+
return page.pdf({ format: 'A4' })
332+
})
333+
```
334+
335+
You can now display links to download or open the PDF in your pages:
336+
337+
```vue [pages/index.vue]
338+
<template>
339+
<a href="/invoice.pdf" download>Download PDF</a>
340+
<a href="/invoice.pdf">Open PDF</a>
341+
</template>
342+
```

playground/app/app.vue

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,5 @@
1-
<script setup>
2-
const colorMode = useColorMode()
3-
4-
function toggleColorMode() {
5-
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark'
6-
}
7-
8-
useHead({
9-
htmlAttrs: { lang: 'en' }
10-
})
11-
12-
useSeoMeta({
13-
title: 'NuxtHub Demo',
14-
description:
15-
'A Nuxt demo hosted with Edge-side rendering, authentication and queyring a SQLite database'
16-
})
17-
18-
const links = [
19-
{ label: 'Home', to: '/' },
20-
{ label: 'AI', to: '/ai' },
21-
{ label: 'Browser', to: '/browser' },
22-
{ label: 'Blob', to: '/blob' },
23-
{ label: 'Database', to: '/database' },
24-
{ label: 'KV', to: '/kv' }
25-
]
26-
</script>
27-
281
<template>
29-
<UContainer class="min-h-screen flex flex-col md:pt-12">
30-
<div class="flex justify-between">
31-
<UHorizontalNavigation :links="links" />
32-
<UButton
33-
square
34-
variant="ghost"
35-
color="black"
36-
:icon="$colorMode.preference === 'dark' ? 'i-heroicons-moon' : 'i-heroicons-sun'"
37-
@click="toggleColorMode"
38-
/>
39-
</div>
2+
<NuxtLayout>
403
<NuxtPage />
41-
42-
<footer class="text-center mt-2">
43-
<NuxtLink
44-
href="https://github.com/nuxt-hub/core"
45-
target="_blank"
46-
class="text-sm text-gray-500 hover:text-gray-700"
47-
>
48-
GitHub
49-
</NuxtLink>
50-
·
51-
<NuxtLink
52-
href="https://twitter.com/nuxt_hub"
53-
target="_blank"
54-
class="text-sm text-gray-500 hover:text-gray-700"
55-
>
56-
Twitter
57-
</NuxtLink>
58-
</footer>
59-
</UContainer>
60-
<UNotifications />
4+
</NuxtLayout>
615
</template>
62-
63-
<style lang="postcss">
64-
body {
65-
@apply font-sans text-gray-950 bg-gray-50 dark:bg-gray-950 dark:text-gray-50;
66-
}
67-
</style>

playground/app/layouts/blank.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<slot />
3+
</template>

playground/app/layouts/default.vue

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script setup>
2+
const colorMode = useColorMode()
3+
4+
function toggleColorMode() {
5+
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark'
6+
}
7+
8+
useHead({
9+
htmlAttrs: { lang: 'en' }
10+
})
11+
12+
useSeoMeta({
13+
title: 'NuxtHub Demo',
14+
description:
15+
'A Nuxt demo hosted with Edge-side rendering, authentication and queyring a SQLite database'
16+
})
17+
18+
const links = [
19+
{ label: 'Home', to: '/' },
20+
{ label: 'AI', to: '/ai' },
21+
{ label: 'Browser', to: '/browser' },
22+
{ label: 'Blob', to: '/blob' },
23+
{ label: 'Database', to: '/database' },
24+
{ label: 'KV', to: '/kv' }
25+
]
26+
</script>
27+
28+
<template>
29+
<UContainer class="min-h-screen flex flex-col md:pt-12">
30+
<div class="flex justify-between">
31+
<UHorizontalNavigation :links="links" />
32+
<UButton
33+
square
34+
variant="ghost"
35+
color="black"
36+
:icon="$colorMode.preference === 'dark' ? 'i-heroicons-moon' : 'i-heroicons-sun'"
37+
@click="toggleColorMode"
38+
/>
39+
</div>
40+
<slot />
41+
42+
<footer class="text-center mt-2">
43+
<NuxtLink
44+
href="https://github.com/nuxt-hub/core"
45+
target="_blank"
46+
class="text-sm text-gray-500 hover:text-gray-700"
47+
>
48+
GitHub
49+
</NuxtLink>
50+
·
51+
<NuxtLink
52+
href="https://twitter.com/nuxt_hub"
53+
target="_blank"
54+
class="text-sm text-gray-500 hover:text-gray-700"
55+
>
56+
Twitter
57+
</NuxtLink>
58+
</footer>
59+
</UContainer>
60+
<UNotifications />
61+
</template>
62+
63+
<style lang="postcss">
64+
body {
65+
@apply font-sans text-gray-950 bg-gray-50 dark:bg-gray-950 dark:text-gray-50;
66+
}
67+
</style>

playground/app/pages/_invoice.vue

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script setup>
2+
definePageMeta({
3+
layout: 'blank'
4+
})
5+
6+
// TODO: Fetch data from API instead of hardcoding
7+
const currentDate = ref(new Date().toLocaleDateString())
8+
const items = ref([
9+
{ name: 'Item 1', quantity: 2, price: 10.00 },
10+
{ name: 'Item 2', quantity: 1, price: 15.00 },
11+
{ name: 'Item 3', quantity: 3, price: 7.50 }
12+
])
13+
const total = computed(() => {
14+
return items.value.reduce((sum, item) => sum + item.quantity * item.price, 0)
15+
})
16+
</script>
17+
18+
<template>
19+
<div style="font-family: Arial, sans-serif; padding: 20px;">
20+
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
21+
<div>
22+
<p><strong>Invoice To:</strong></p>
23+
<p>John Doe</p>
24+
<p>123 Main St</p>
25+
<p>Anytown, USA 12345</p>
26+
</div>
27+
<div>
28+
<p><strong>Invoice Number:</strong> INV-001</p>
29+
<p><strong>Date:</strong> {{ currentDate }}</p>
30+
</div>
31+
</div>
32+
<table>
33+
<thead>
34+
<tr>
35+
<th>Item</th>
36+
<th>Qty</th>
37+
<th>Price</th>
38+
<th>Total</th>
39+
</tr>
40+
</thead>
41+
<tbody>
42+
<tr v-for="(item, index) in items" :key="index">
43+
<td>{{ item.name }}</td>
44+
<td>{{ item.quantity }}</td>
45+
<td>${{ item.price.toFixed(2) }}</td>
46+
<td>${{ (item.quantity * item.price).toFixed(2) }}</td>
47+
</tr>
48+
</tbody>
49+
</table>
50+
<div style="text-align: right; margin-top: 20px;">
51+
<p><strong>Total: ${{ total.toFixed(2) }}</strong></p>
52+
</div>
53+
</div>
54+
</template>
55+
56+
<style scoped>
57+
table { width: 100%; border-collapse: collapse; }
58+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
59+
th { background-color: #f2f2f2; }
60+
</style>

playground/app/pages/browser.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,13 @@ const capture = async () => {
4444
<UAlert v-if="!image" :title="loading ? 'Capturing...' : 'No screenshot captured'" color="white" icon="i-ph-info-duotone" />
4545
<img v-if="image" :src="image" class="rounded border dark:border-gray-800" style="aspect-ratio: 16/9;" :class="{ 'animate-pulse': loading }">
4646
<UAlert v-if="framework" class="mt-4" :class="{ 'animate-pulse': loading }" :title="`This website is made with ${framework}`" color="white" icon="i-ph-code-duotone" />
47+
<p class="mt-4">
48+
Or open our <UButtonGroup>
49+
<UButton to="/invoice.pdf" external color="gray">
50+
PDF invoice
51+
</UButton>
52+
<UButton to="/invoice.pdf" external download color="gray" icon="i-ph-download-duotone" />
53+
</UButtonGroup>
54+
</p>
4755
</UCard>
4856
</template>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default eventHandler(async (event) => {
2+
const { page } = await hubBrowser()
3+
await page.goto(`${getRequestURL(event).origin}/_invoice`)
4+
5+
setHeader(event, 'Content-Type', 'application/pdf')
6+
return page.pdf({ format: 'A4' })
7+
})

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)