├── .gitignore ├── README.md ├── app.vue ├── assets └── css │ └── main.css ├── components └── Globe.vue ├── composables ├── lang.ts └── location.ts ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── scripts ├── miniflare.sh └── publish.sh ├── server └── api │ ├── hello.ts │ ├── location.ts │ └── vercelLocation.ts ├── tsconfig.json ├── types └── index.d.ts ├── wrangler.toml └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | 11h 10 | .vercel 11 | .vercel_build_output 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt in the edge 2 | 3 | This Nuxt app was used as a demo at the Vue.js Roadtrip Barcelona 2022 conference. 4 | 5 | - 🌎 Demo: https://nuxt-edge-test.graficos-net.workers.dev/ 6 | - 📺 Slides: https://slides.com/paul_melero/nuxt-on-the-edge 7 | - 👀 Recording from Vue.js RoadTrip Barcelona 2022: https://youtu.be/MZbDrmgpqcc?t=19267 8 | 9 | ## Setup 10 | 11 | Make sure to install the dependencies: 12 | 13 | ```bash 14 | # npm 15 | npm install 16 | ``` 17 | 18 | ## Development Server 19 | 20 | Start the development server on http://localhost:3000 21 | 22 | ```bash 23 | npm run dev 24 | ``` 25 | 26 | ## Production 27 | 28 | Build the application for production: 29 | 30 | ```bash 31 | npm run build 32 | # until Nuxt+Nitro gets more stable, you might need to set manually NITRO_PRESET to your target preset 33 | ``` 34 | 35 | Locally preview production build: 36 | 37 | ```bash 38 | # If you're targeting Cloudflare Workers: 39 | NITRO_PRESET=cloudflare npm run build && npm run miniflare 40 | # If you're targeting Vercel: 41 | npm i -g vercel 42 | NITRO_PRESET=vercel-edge yarn build && vercel 43 | ``` 44 | 45 | Deploy to Clouflare Workers: 46 | 47 | ```bash 48 | NITRO_PRESET=cloudflare npm run build && npm run publish 49 | ``` 50 | 51 | Deploy to Vercel: 52 | 53 | ```bash 54 | # if you haven't installed vercel CLI yet, do so: 55 | npm i -g vercel 56 | # and then: 57 | NITRO_PRESET=vercel-edge yarn build && vercel 58 | ``` 59 | 60 | Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information. 61 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 39 | 40 | 92 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | background-color: #000; 8 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /components/Globe.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 109 | -------------------------------------------------------------------------------- /composables/lang.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | 3 | export const useLang: () => Ref = () => { 4 | if (process.client) { 5 | return ref(navigator.language); 6 | } 7 | 8 | const parseLangString: (languages: string) => string = (languages) => { 9 | return languages.split(',')[0]; 10 | }; 11 | const nuxt = useNuxtApp(); 12 | const lang: string = parseLangString( 13 | nuxt.ssrContext.event.req.headers['accept-language'] 14 | ); 15 | return ref(lang); 16 | }; 17 | -------------------------------------------------------------------------------- /composables/location.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | import type { IpApiResponse, VercelApiResponse } from '@/types'; 3 | 4 | export const useLocation = async (): Promise<{ 5 | location: Ref; 6 | error: string; 7 | message: Ref; 8 | }> => { 9 | let location: Ref = ref(); 10 | 11 | const message = ref('Application running (server-side) from:'); 12 | 13 | try { 14 | const info = await globalThis.$fetch( 15 | '/api/vercelLocation', 16 | { 17 | headers: useRequestHeaders(['x-forwarded-for', 'x-vercel-ip-city']), 18 | } 19 | ); 20 | 21 | if (info.ip === '-') { 22 | throw new Error("Can't connect with Vercel Network"); 23 | } 24 | 25 | location.value = info; 26 | message.value = `Hello from Vercel Edge`; 27 | } catch (_e) { 28 | try { 29 | location.value = await globalThis.$fetch(`/api/location`); 30 | } catch (error) { 31 | console.log(error); 32 | message.value = `Edge server failed`; 33 | return { location: null, error, message }; 34 | } 35 | } 36 | 37 | return { location, error: null, message }; 38 | }; 39 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { defineNuxtConfig } from 'nuxt'; 3 | 4 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 5 | export default defineNuxtConfig({ 6 | css: ['@/assets/css/main.css'], 7 | publicRuntimeConfig: { 8 | API_TOKEN: process.env.API_TOKEN, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "miniflare": "sh scripts/miniflare.sh", 9 | "publish": "sh scripts/publish.sh", 10 | "test:rome": "npx playwright open --timezone='Europe/Rome' --geolocation='41.890221,12.492348' --lang='it-IT' http://localhost:3000", 11 | "test:berlin": "npx playwright open --timezone='Europe/Berlin' --geolocation='52.5069704,13.2846504' --lang='de-GE' http://localhost:3000" 12 | }, 13 | "devDependencies": { 14 | "@types/three": "^0.141.0", 15 | "nuxt": "^3.0.0-rc.4" 16 | }, 17 | "dependencies": { 18 | "three": "^0.142.0", 19 | "three-globe": "^2.24.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/miniflare.sh: -------------------------------------------------------------------------------- 1 | NITRO_PRESET=cloudflare yarn build 11h 0m 12s 07:27:30 2 | npx miniflare .output/server/index.mjs --site .output/public 3 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | NITRO_PRESET=cloudflare yarn build 2 | 3 | wrangler publish 4 | -------------------------------------------------------------------------------- /server/api/hello.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async () => { 2 | return 'Hello'; 3 | }); 4 | -------------------------------------------------------------------------------- /server/api/location.ts: -------------------------------------------------------------------------------- 1 | import { IpApiResponse } from '~~/types'; 2 | 3 | export default defineEventHandler(async () => { 4 | const key = useRuntimeConfig().API_TOKEN; 5 | 6 | const data: IpApiResponse = await $fetch( 7 | `https://pro.ip-api.com/json/?key=${key}` 8 | ); 9 | return data; 10 | }); 11 | -------------------------------------------------------------------------------- /server/api/vercelLocation.ts: -------------------------------------------------------------------------------- 1 | import { IpApiResponse, VercelApiResponse } from '~~/types'; 2 | 3 | export default eventHandler(async (event) => { 4 | const key = useRuntimeConfig().API_TOKEN; 5 | // https://github.com/pi0/nuxt-on-the-edge/blob/main/server/api/info.ts 6 | // https://vercel.com/changelog/ip-geolocation-for-serverless-functions 7 | const cityHeader = event.req.headers['x-vercel-ip-city'] as string; 8 | const city = cityHeader ? decodeURIComponent(cityHeader) : '-'; 9 | const ipHeader = event.req.headers['x-forwarded-for'] as string; 10 | const ip = ipHeader ? ipHeader.split(',')[0] : '-'; 11 | 12 | if (ip.match(/^127\.0\.0/)) { 13 | throw new Error("You're on dev environment, can't get IP"); 14 | } 15 | 16 | const { 17 | lat, 18 | lon, 19 | city: city2, 20 | } = await globalThis.$fetch( 21 | `https://pro.ip-api.com/json/${ip}?key=${key}&fields=lat,lon,city` 22 | ); 23 | 24 | return { 25 | city: city === '-' ? city2 : city, 26 | ip, 27 | lat, 28 | lon, 29 | } as VercelApiResponse; 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface IpApiResponse { 2 | as: string; 3 | city: string; 4 | country: string; 5 | countryCode: string; 6 | isp: string; 7 | lat: number; 8 | lon: number; 9 | org: string; 10 | query: string; 11 | region: string; 12 | regionName: string; 13 | status: string; 14 | timezone: string; 15 | zip: string; 16 | } 17 | 18 | export interface VercelApiResponse { 19 | ip: string; 20 | city: string; 21 | lat: number; 22 | lon: number; 23 | } 24 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "nuxt-edge-test" 2 | type = "javascript" 3 | account_id = "81bb9e915adce027f7f72b700da03532" 4 | workers_dev = true 5 | route = "" 6 | zone_id = "" 7 | compatibility_date = "2022-04-07" 8 | 9 | [site] 10 | bucket = ".output/public" 11 | entry-point = ".output" 12 | 13 | [build] 14 | command = "" 15 | 16 | [build.upload] 17 | format = "service-worker" 18 | 19 | 20 | [vars] 21 | API_TOKEN = "AmUN9xAaQALVYu6" --------------------------------------------------------------------------------