Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/content/docs/4.migration-guide/1.v0-to-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,42 @@ Default `object-fit` changed from `contain` to `cover`:

Player instances are now properly isolated. Remove any workarounds for multiple players.

### Google Maps Static Placeholder ([#673](https://github.com/nuxt/scripts/pull/673))

v1 extracts the built-in static map placeholder into a standalone [`<ScriptGoogleMapsStaticMap>`{lang="html"}](/scripts/google-maps/api/static-map) component. This removes the following props from `<ScriptGoogleMaps>`{lang="html"}:

- `placeholderOptions`
- `placeholderAttrs`
- `aboveTheFold`

The `#placeholder` slot no longer passes a `placeholder` URL string. It is now empty by default.

```diff
<!-- Before -->
<ScriptGoogleMaps
:center="center"
:zoom="7"
above-the-fold
:placeholder-options="{ maptype: 'satellite' }"
:placeholder-attrs="{ class: 'rounded' }"
/>

<!-- After -->
+<ScriptGoogleMaps :center="center" :zoom="7">
+ <template #placeholder>
+ <ScriptGoogleMapsStaticMap
+ :center="center"
+ :zoom="7"
+ loading="eager"
+ maptype="satellite"
+ :img-attrs="{ class: 'rounded' }"
+ />
+ </template>
+</ScriptGoogleMaps>
```

Use the new component standalone for store locators, contact pages, and directions previews without loading the interactive Maps API.

### Google Tag Manager

#### onBeforeGtmStart Callback
Expand Down
16 changes: 9 additions & 7 deletions docs/content/scripts/google-maps/2.api/1.script-google-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps){lang="html"} compo

It's optimized for performance by using the [Element Event Triggers](/docs/guides/script-triggers#element-event-triggers), only loading the Google Maps when specific elements events happen.

Before Google Maps loads, it shows a placeholder using [Maps Static API](https://developers.google.com/maps/documentation/maps-static).
The `#placeholder` slot is empty by default. Use [`<ScriptGoogleMapsStaticMap>`{lang="html"}](/scripts/google-maps/api/static-map) inside it to show a static map image while the interactive map loads.

By default, it will load on the `mouseenter`, `mouseover`, and `mousedown` events.

Expand Down Expand Up @@ -124,15 +124,17 @@ override this component. Make sure you provide a loading indicator.

**placeholder**

This slot displays a placeholder image before Google Maps loads. By default, this will show the Google Maps Static API image for the map.

Provide custom `#placeholder` content without rendering the provided `placeholder` URL to skip the Static Maps API request and avoid those charges.
The placeholder slot is empty by default. Use [`<ScriptGoogleMapsStaticMap>`{lang="html"}](/scripts/google-maps/api/static-map) to show a static map preview while the interactive map loads.

```vue
<template>
<ScriptGoogleMaps>
<template #placeholder="{ placeholder }">
<img :src="placeholder">
<ScriptGoogleMaps :center="center" :zoom="7">
<template #placeholder>
<ScriptGoogleMapsStaticMap
:center="center"
:zoom="7"
loading="eager"
/>
</template>
</ScriptGoogleMaps>
</template>
Expand Down
99 changes: 99 additions & 0 deletions docs/content/scripts/google-maps/2.api/1b.static-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: <ScriptGoogleMapsStaticMap>
---

Renders a [Google Maps Static API](https://developers.google.com/maps/documentation/maps-static) image. Use standalone for static map previews, or drop into the `#placeholder` slot of [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps/api/script-google-maps) for a loading placeholder.

::script-types{script-key="google-maps" filter="ScriptGoogleMapsStaticMap"}
::

## Usage

### Standalone

Display a static map without loading the interactive Google Maps JavaScript API.

```vue
<template>
<ScriptGoogleMapsStaticMap
center="51.95,19.13"
:zoom="7"
width="100%"
height="300"
/>
</template>
```

### As Placeholder

Use inside [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps/api/script-google-maps) to show a static map while the interactive map loads.

```vue
<template>
<ScriptGoogleMaps :center="center" :zoom="7">
<template #placeholder>
<ScriptGoogleMapsStaticMap
:center="center"
:zoom="7"
loading="eager"
maptype="satellite"
/>
</template>
</ScriptGoogleMaps>
</template>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `center` | `string \| { lat: number, lng: number }` | | Map center as `"lat,lng"` string or `LatLngLiteral`. |
| `zoom` | `number` | `15` | Zoom level (0 to 21). |
| `size` | `` `${number}x${number}` `` | auto | Explicit pixel size for the API request. When omitted, measures the rendered container on mount. Falls back to `640x400` during SSR. |
| `scale` | `1 \| 2` | `2` | Device pixel ratio. |
| `format` | `StaticMapFormat` | | Image format: `png`, `jpg`, `gif`, `png8`, `png32`, `jpg-baseline`. |
| `maptype` | `StaticMapType` | | Map type: `roadmap`, `satellite`, `terrain`, `hybrid`. |
| `mapId` | `string` | | Cloud-based map styling ID. |
| `markers` | `string \| string[]` | | [Marker descriptors](https://developers.google.com/maps/documentation/maps-static/start#Markers). |
| `path` | `string \| string[]` | | [Polyline path descriptors](https://developers.google.com/maps/documentation/maps-static/start#Paths). |
| `visible` | `string \| string[]` | | Locations that should be visible on the map. |
| `style` | `string \| string[] \| MapTypeStyle[]` | | Map styling. Accepts raw Static Maps API style strings or Google Maps JS API `MapTypeStyle[]` objects. |
| `language` | `string` | | Language code for map labels. |
| `region` | `string` | | Region bias. |
| `signature` | `string` | | Digital signature for keyless requests. |
| `apiKey` | `string` | | API key override. Takes priority over the proxy; the component falls back to the server-side key when omitted. |
| `width` | `number \| string` | `640` | CSS width for the container. |
| `height` | `number \| string` | `400` | CSS height for the container. |
| `loading` | `'eager' \| 'lazy'` | `'lazy'` | Image loading strategy. |
| `objectFit` | `string` | `'cover'` | Object-fit for the `<img>`{lang="html"} within its container. |
| `imgAttrs` | `ImgHTMLAttributes` | | Additional attributes for the `<img>`{lang="html"} element. |

## Size Handling

When `size` is not provided, the component:

1. **Client-side**: Measures the container's pixel dimensions on mount and clamps to the [Static Maps API limit](https://developers.google.com/maps/documentation/maps-static/start) of 640x640 (preserving aspect ratio).
2. **SSR**: Derives from `width`/`height` props if both are pixel values.
3. **Fallback**: Uses `640x400`.

Set `size` explicitly to bypass auto-measurement.

## Proxy Support

Configuring `googleMaps` in `scripts.registry` enables a server-side proxy automatically. The component routes requests through the proxy unless you provide an explicit `apiKey` prop.

## Slots

**default**

Override the `<img>`{lang="html"} element with a custom template. Receives `{ src }` with the computed Static Maps URL.

```vue
<template>
<ScriptGoogleMapsStaticMap center="51.95,19.13" :zoom="7">
<template #default="{ src }">
<img :src="src" alt="Custom static map" class="rounded-lg shadow">
</template>
</ScriptGoogleMapsStaticMap>
</template>
```
117 changes: 11 additions & 106 deletions packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<script lang="ts">
/// <reference types="google.maps" />
import type { ElementScriptTrigger } from '#nuxt-scripts/types'
import type { QueryObject } from 'ufo'
import type { HTMLAttributes, ImgHTMLAttributes, ReservedProps, ShallowRef } from 'vue'
import type { HTMLAttributes, ReservedProps, ShallowRef } from 'vue'
import { useScriptTriggerElement } from '#nuxt-scripts/composables/useScriptTriggerElement'
import { useScriptGoogleMaps } from '#nuxt-scripts/registry/google-maps'
import { scriptRuntimeConfig } from '#nuxt-scripts/utils'
import { defu } from 'defu'
import { tryUseNuxtApp, useHead, useRuntimeConfig } from 'nuxt/app'
import { withQuery } from 'ufo'
import { computed, onBeforeUnmount, onMounted, provide, ref, shallowRef, toRaw, watch } from 'vue'
import ScriptAriaLoadingIndicator from '../ScriptAriaLoadingIndicator.vue'

Expand All @@ -18,36 +16,11 @@ export { MAP_INJECTION_KEY } from './useGoogleMapsResource'
</script>

<script lang="ts" setup>
export interface PlaceholderOptions {
width?: string | number
height?: string | number
center?: string
zoom?: number
size?: string
scale?: number
format?: 'png' | 'jpg' | 'gif' | 'png8' | 'png32' | 'jpg-baseline'
maptype?: 'roadmap' | 'satellite' | 'terrain' | 'hybrid'
language?: string
region?: string
markers?: string
path?: string
visible?: string
style?: string
map_id?: string
key?: string
signature?: string
}

const props = withDefaults(defineProps<{
/**
* Defines the trigger event to load the script.
*/
trigger?: ElementScriptTrigger
/**
* Is Google Maps being rendered above the fold?
* This will load the placeholder image with higher priority.
*/
aboveTheFold?: boolean
/**
* Defines the Google Maps API key. Must have access to the Static Maps API as well.
*/
Expand Down Expand Up @@ -85,16 +58,6 @@ const props = withDefaults(defineProps<{
* Defines the height of the map
*/
height?: number | string
/**
* Customize the placeholder image attributes.
*
* @see https://developers.google.com/maps/documentation/maps-static/start.
*/
placeholderOptions?: PlaceholderOptions
/**
* Customize the placeholder image attributes.
*/
placeholderAttrs?: ImgHTMLAttributes & ReservedProps & Record<string, unknown>
/**
* Customize the root element attributes.
*/
Expand Down Expand Up @@ -130,7 +93,6 @@ const emits = defineEmits<{

const apiKey = props.apiKey || scriptRuntimeConfig('googleMaps')?.apiKey
const runtimeConfig = useRuntimeConfig()
const proxyConfig = (runtimeConfig.public['nuxt-scripts'] as any)?.googleStaticMapsProxy

// Color mode support - try to auto-detect from @nuxtjs/color-mode
const nuxtApp = tryUseNuxtApp()
Expand All @@ -155,8 +117,6 @@ const mapsApi = ref<typeof google.maps | undefined>()
if (import.meta.dev) {
if (!apiKey)
throw new Error('GoogleMaps requires an API key. Enable it in your nuxt.config:\n\n scripts: {\n registry: {\n googleMaps: true\n }\n }\n\nThen set NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY in your .env file.\n\nAlternatively, pass `api-key` directly on the <ScriptGoogleMaps> component (note: this exposes the key client-side).')
if (!proxyConfig?.enabled && !props.apiKey)
console.warn('[nuxt-scripts] Google Maps proxy is not enabled. Enable `googleMaps` in your nuxt.config registry to keep your API key server-side. See: https://scripts.nuxt.com/scripts/google-maps#setup')
}

const rootEl = ref<HTMLElement>()
Expand Down Expand Up @@ -339,90 +299,37 @@ onMounted(() => {
})
})

if (import.meta.server && !proxyConfig?.enabled) {
if (import.meta.server) {
useHead({
link: [
{
rel: props.aboveTheFold ? 'preconnect' : 'dns-prefetch',
rel: 'dns-prefetch',
href: 'https://maps.googleapis.com',
},
],
})
}

function transformMapStyles(styles: google.maps.MapTypeStyle[]) {
return styles.map((style) => {
const feature = style.featureType ? `feature:${style.featureType}` : ''
const element = style.elementType ? `element:${style.elementType}` : ''
const rules = (style.stylers || []).map((styler) => {
return Object.entries(styler).map(([key, value]) => {
if (key === 'color' && typeof value === 'string') {
value = value.replace('#', '0x')
}
return `${key}:${value}`
}).join('|')
}).filter(Boolean).join('|')
return [feature, element, rules].filter(Boolean).join('|')
}).filter(Boolean)
function toCssUnit(value: string | number): string {
return typeof value === 'number' ? `${value}px` : value
}

const placeholder = computed(() => {
let center = options.value.center
if (center && typeof center === 'object') {
center = `${center.lat},${center.lng}`
}
// @ts-expect-error lazy type
const placeholderOptions: PlaceholderOptions = defu(props.placeholderOptions, {
// only map option values
zoom: options.value.zoom,
center,
}, {
size: `${props.width}x${props.height}`,
// Only include API key if not using proxy (proxy injects it server-side)
key: proxyConfig?.enabled ? undefined : apiKey,
scale: 2, // we assume a high DPI to avoid hydration issues
style: props.mapOptions?.styles ? transformMapStyles(props.mapOptions.styles) : undefined,
map_id: currentMapId.value,
})

const baseUrl = proxyConfig?.enabled
? '/_scripts/proxy/google-static-maps'
: 'https://maps.googleapis.com/maps/api/staticmap'

return withQuery(baseUrl, placeholderOptions as QueryObject)
})

const placeholderAttrs = computed(() => {
return defu(props.placeholderAttrs, {
src: placeholder.value,
alt: 'Google Maps Static Map',
loading: props.aboveTheFold ? 'eager' : 'lazy',
style: {
cursor: 'pointer',
width: '100%',
objectFit: 'cover',
height: '100%',
},
} satisfies ImgHTMLAttributes)
})

const rootAttrs = computed(() => {
return defu(props.rootAttrs, {
'aria-busy': status.value === 'loading',
'aria-label': status.value === 'awaitingLoad'
? 'Google Maps Static Map'
? 'Google Maps'
: status.value === 'loading'
? 'Google Maps Map Embed Loading'
: 'Google Maps Embed',
? 'Google Maps Loading'
: 'Google Maps',
'aria-live': 'polite',
'role': 'application',
'style': {
cursor: 'pointer',
position: 'relative',
maxWidth: '100%',
width: `${props.width}px`,
height: `'auto'`,
aspectRatio: `${props.width}/${props.height}`,
width: toCssUnit(props.width),
height: toCssUnit(props.height),
},
...(trigger instanceof Promise ? trigger.ssrAttrs || {} : {}),
}) as HTMLAttributes
Expand All @@ -444,9 +351,7 @@ onBeforeUnmount(() => {
<template>
<div ref="rootEl" v-bind="rootAttrs">
<div v-show="ready" ref="mapEl" :style="{ width: '100%', height: '100%', maxWidth: '100%' }" />
<slot v-if="!ready" :placeholder="placeholder" name="placeholder">
<img v-bind="placeholderAttrs">
</slot>
<slot v-if="!ready" name="placeholder" />
<slot v-if="status !== 'awaitingLoad' && !ready" name="loading">
<ScriptAriaLoadingIndicator />
</slot>
Expand Down
Loading
Loading