From 6efe2535c468fb5a2190dcd9b58569890b12d289 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 24 Sep 2025 21:08:01 +1000 Subject: [PATCH] chore: progress --- src/module.ts | 29 +++++++++ .../GoogleMaps/ScriptGoogleMaps.vue | 31 +++++++--- src/runtime/composables/useScript.ts | 4 ++ .../server/google-static-maps-proxy.ts | 62 +++++++++++++++++++ 4 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/runtime/server/google-static-maps-proxy.ts diff --git a/src/module.ts b/src/module.ts index 6d63c4ec..e1cd1d70 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3,6 +3,7 @@ import { addComponentsDir, addImports, addPluginTemplate, addTypeTemplate, + addServerHandler, createResolver, defineNuxtModule, hasNuxtModule, @@ -70,6 +71,21 @@ export interface ModuleOptions { */ cacheMaxAge?: number } + /** + * Google Static Maps proxy configuration. + */ + googleStaticMapsProxy?: { + /** + * Enable proxying Google Static Maps through your own origin. + * @default false + */ + enabled?: boolean + /** + * Cache duration for static map images in seconds. + * @default 3600 (1 hour) + */ + cacheMaxAge?: number + } /** * Whether the module is enabled. * @@ -107,6 +123,10 @@ export default defineNuxtModule({ timeout: 15_000, // Configures the maximum time (in milliseconds) allowed for each fetch attempt. }, }, + googleStaticMapsProxy: { + enabled: false, + cacheMaxAge: 3600, + }, enabled: true, debug: false, }, @@ -133,6 +153,7 @@ export default defineNuxtModule({ // expose for devtools version: nuxt.options.dev ? version : undefined, defaultScriptOptions: config.defaultScriptOptions, + googleStaticMapsProxy: config.googleStaticMapsProxy, } // Merge registry config with existing runtimeConfig.public.scripts for proper env var resolution @@ -267,6 +288,14 @@ export {}` }) }) + // Add Google Static Maps proxy handler if enabled + if (config.googleStaticMapsProxy?.enabled) { + addServerHandler({ + route: '/_scripts/google-static-maps-proxy', + handler: await resolvePath('./runtime/server/google-static-maps-proxy.ts'), + }) + } + if (nuxt.options.dev) setupDevToolsUI(config, resolvePath) }, diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue index fc0df6db..bc462a98 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue @@ -6,7 +6,7 @@ import { withQuery } from 'ufo' import type { QueryObject } from 'ufo' import { defu } from 'defu' import { hash } from 'ohash' -import { useHead } from 'nuxt/app' +import { useHead, useRuntimeConfig } from 'nuxt/app' import type { ElementScriptTrigger } from '#nuxt-scripts/types' import { scriptRuntimeConfig } from '#nuxt-scripts/utils' import { useScriptTriggerElement } from '#nuxt-scripts/composables/useScriptTriggerElement' @@ -119,6 +119,8 @@ const emits = defineEmits<{ }>() const apiKey = props.apiKey || scriptRuntimeConfig('googleMaps')?.apiKey +const runtimeConfig = useRuntimeConfig() +const proxyConfig = (runtimeConfig.public['nuxt-scripts'] as any)?.googleStaticMapsProxy const mapsApi = ref() @@ -381,14 +383,17 @@ onMounted(() => { }) if (import.meta.server) { - useHead({ - link: [ - { - rel: props.aboveTheFold ? 'preconnect' : 'dns-prefetch', - href: 'https://maps.googleapis.com', - }, - ], - }) + // Only preconnect to Google Maps if not using proxy + if (!proxyConfig?.enabled) { + useHead({ + link: [ + { + rel: props.aboveTheFold ? 'preconnect' : 'dns-prefetch', + href: 'https://maps.googleapis.com', + }, + ], + }) + } } function transformMapStyles(styles: google.maps.MapTypeStyle[]) { @@ -438,7 +443,13 @@ const placeholder = computed(() => { }) .join('|'), }) - return withQuery('https://maps.googleapis.com/maps/api/staticmap', placeholderOptions as QueryObject) + + // Use proxy endpoint if enabled, otherwise use direct Google Maps API + const baseUrl = proxyConfig?.enabled + ? '/_scripts/google-static-maps-proxy' + : 'https://maps.googleapis.com/maps/api/staticmap' + + return withQuery(baseUrl, placeholderOptions as QueryObject) }) const placeholderAttrs = computed(() => { diff --git a/src/runtime/composables/useScript.ts b/src/runtime/composables/useScript.ts index b02bc48c..a0e4d6f1 100644 --- a/src/runtime/composables/useScript.ts +++ b/src/runtime/composables/useScript.ts @@ -9,6 +9,10 @@ import { logger } from '../logger' function useNuxtScriptRuntimeConfig() { return useRuntimeConfig().public['nuxt-scripts'] as { defaultScriptOptions: NuxtUseScriptOptions + googleStaticMapsProxy?: { + enabled?: boolean + cacheMaxAge?: number + } } } diff --git a/src/runtime/server/google-static-maps-proxy.ts b/src/runtime/server/google-static-maps-proxy.ts new file mode 100644 index 00000000..0b863b14 --- /dev/null +++ b/src/runtime/server/google-static-maps-proxy.ts @@ -0,0 +1,62 @@ +import type { EventHandler } from 'h3' +import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { $fetch } from 'ofetch' +import { withQuery } from 'ufo' +import { useRuntimeConfig } from '#imports' + +export default defineEventHandler(async (event) => { + const runtimeConfig = useRuntimeConfig() + const proxyConfig = (runtimeConfig.public['nuxt-scripts'] as any)?.googleStaticMapsProxy + + if (!proxyConfig?.enabled) { + throw createError({ + statusCode: 404, + statusMessage: 'Google Static Maps proxy is not enabled', + }) + } + + // Get query parameters from the request + const query = getQuery(event) + + // Validate required parameters + if (!query.key) { + throw createError({ + statusCode: 400, + statusMessage: 'API key is required', + }) + } + + // Build the Google Static Maps API URL + const googleMapsUrl = withQuery('https://maps.googleapis.com/maps/api/staticmap', query) + + try { + // Fetch the image from Google Static Maps API + const response = await $fetch.raw(googleMapsUrl, { + headers: { + 'User-Agent': 'Nuxt Scripts Google Static Maps Proxy', + }, + }) + + // Set appropriate headers for caching and content type + const cacheMaxAge = proxyConfig.cacheMaxAge || 3600 + setHeader(event, 'Content-Type', response.headers.get('content-type') || 'image/png') + setHeader(event, 'Cache-Control', `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`) + setHeader(event, 'Vary', 'Accept-Encoding') + + // Add CORS headers if needed + setHeader(event, 'Access-Control-Allow-Origin', '*') + setHeader(event, 'Access-Control-Allow-Methods', 'GET, OPTIONS') + setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type') + + return response._data + } + catch (error: any) { + // Log error for debugging + console.error('Google Static Maps Proxy Error:', error) + + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to fetch static map', + }) + } +}) as EventHandler