diff --git a/docs/content/scripts/google-maps/.navigation.yml b/docs/content/scripts/google-maps/.navigation.yml new file mode 100644 index 00000000..b7dd8d23 --- /dev/null +++ b/docs/content/scripts/google-maps/.navigation.yml @@ -0,0 +1 @@ +title: Google Maps diff --git a/docs/content/scripts/google-maps/1.guides/.navigation.yml b/docs/content/scripts/google-maps/1.guides/.navigation.yml new file mode 100644 index 00000000..f1ad8d56 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/.navigation.yml @@ -0,0 +1,2 @@ +title: Guides +icon: i-ph-book-duotone diff --git a/docs/content/scripts/google-maps/1.guides/1.performance.md b/docs/content/scripts/google-maps/1.guides/1.performance.md new file mode 100644 index 00000000..36cf9581 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/1.performance.md @@ -0,0 +1,127 @@ +--- +title: Performance +--- + +`ScriptGoogleMaps` is optimized by default: the JavaScript API only loads when the user interacts with the map. Before that, a lightweight static image placeholder is shown. + +## Loading Strategies + +### Default: Lazy (Recommended) + +By default, the map loads on `mouseenter`, `mouseover`, or `mousedown`. Most page visitors never interact with the map, so you avoid the Maps JavaScript API charge for those sessions. + +```vue + +``` + +### Immediate Loading + +Forces the JavaScript API to load with the page. Use this only when the map is the primary content and you need it interactive from the start. + +```vue + +``` + +::callout{color="amber"} +Immediate loading charges the Maps JavaScript API ($7/1000) on every page view. Only use this when the map is essential to the page experience. +:: + +### Custom Triggers + +You can control exactly when the map loads using any [Element Event Trigger](/docs/guides/script-triggers#element-event-triggers). + +```vue + +``` + +## Placeholder Optimization + +### Above the Fold + +When the map is visible without scrolling, mark it for priority loading. This sets the placeholder image to `loading="eager"` and adds a `preconnect` hint. + +```vue + +``` + +### Custom Placeholder + +Replace the default Google Static Maps image with your own custom image to avoid Static Maps API charges. Useful when you have a screenshot or illustration of the map area. + +```vue + +``` + +You can also access the generated static map URL if you want to use it with custom styling. Note that rendering the `placeholder` URL still makes a Static Maps API request and incurs charges: + +```vue + +``` + +### Loading State + +Show a custom indicator while the JavaScript API loads: + +```vue + +``` + +## Marker Performance + +When rendering many markers, use `ScriptGoogleMapsMarkerClusterer` to group nearby markers. This significantly reduces DOM elements and improves pan/zoom performance. + +```vue + +``` + +See [Billing & Permissions](/scripts/google-maps/guides/billing) for a full cost breakdown and optimization strategies. diff --git a/docs/content/scripts/google-maps/1.guides/2.programmatic-api.md b/docs/content/scripts/google-maps/1.guides/2.programmatic-api.md new file mode 100644 index 00000000..5eea73eb --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/2.programmatic-api.md @@ -0,0 +1,130 @@ +--- +title: Programmatic API +--- + +The `ScriptGoogleMaps` component exposes its internal APIs via template ref, giving you full control beyond what the declarative SFC components provide. + +## Accessing the API + +```vue + + + +``` + +### Exposed Properties + +| Property | Type | Description | +|---|---|---| +| `googleMaps` | `Ref`{lang="html"} | The Google Maps API namespace | +| `map` | `ShallowRef`{lang="html"} | The map instance | +| `createAdvancedMapMarker` | `(options?) => Promise`{lang="html"} | Create a marker programmatically | +| `resolveQueryToLatLng` | `(query: string) => Promise`{lang="html"} | Geocode a location string | +| `importLibrary` | `(key: string) => Promise`{lang="html"} | Load an additional Google Maps library | + +## Creating Markers Programmatically + +For cases where declarative `v-for` markers aren't flexible enough (dynamic data, imperative creation logic), use `createAdvancedMapMarker`: + +```vue + + + +``` + +::callout +For most use cases, prefer the declarative `ScriptGoogleMapsAdvancedMarkerElement` component with `v-for`. Use the programmatic API when you need fine-grained control over marker lifecycle or are integrating with external data sources. +:: + +## Geocoding Queries + +Convert location strings to coordinates using `resolveQueryToLatLng`. When you enable the registry proxy, this resolves server-side (cheaper, API key hidden). Otherwise it falls back to the client-side Places API. + +```vue + + + +``` + +## Importing Libraries + +Google Maps splits functionality into libraries that load on demand. Use `importLibrary` to access geometry, drawing, places, and visualization APIs: + +```vue + +``` + +Available libraries: `marker`, `places`, `geometry`, `drawing`, `visualization` + +## Subscribing to Map Events + +Use the `@ready` event to access the map instance and subscribe to native Google Maps events: + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/1.guides/3.map-styling.md b/docs/content/scripts/google-maps/1.guides/3.map-styling.md new file mode 100644 index 00000000..9f205999 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/3.map-styling.md @@ -0,0 +1,105 @@ +--- +title: Map Styling +--- + +Google Maps supports two styling approaches: legacy JSON styles and cloud-based map IDs. Both work with Nuxt Scripts, including the static map placeholder. + +## JSON Styles + +Use the `mapOptions.styles` prop with a JSON style array. You can find pre-made styles on [Snazzy Maps](https://snazzymaps.com/). + +Styles automatically apply to both the static map placeholder and the interactive map. + +```vue + + + +``` + +## Cloud-Based Map IDs + +Google's [Map Styling](https://developers.google.com/maps/documentation/cloud-customization) lets you create and manage styles in the Google Cloud Console, then apply them with a map ID. + +```vue + +``` + +::callout{color="amber"} +JSON `styles` and `mapId` are mutually exclusive. When you provide both, the component ignores `mapId` and applies `styles`. Note that `AdvancedMarkerElement` requires a map ID to function; legacy `Marker` works without one. +:: + +## Dark Mode / Color Mode + +Switch map styles automatically based on the user's color mode preference. Provide a `mapIds` object with light and dark map IDs: + +```vue + +``` + +This auto-detects `@nuxtjs/color-mode` if installed. You can also control it manually with the `colorMode` prop: + +```vue + + + +``` + +## Combining Styles with Markers + +Custom-styled maps pair well with custom marker content for a cohesive look: + +```vue + +``` diff --git a/docs/content/scripts/google-maps/1.guides/4.billing.md b/docs/content/scripts/google-maps/1.guides/4.billing.md new file mode 100644 index 00000000..8313ce19 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/4.billing.md @@ -0,0 +1,66 @@ +--- +title: Billing & Permissions +--- + +## Required API Permissions + +Your Google Cloud project needs these APIs enabled: + +| API | Required? | When it's used | +|---|---|---| +| [Maps JavaScript API](https://developers.google.com/maps/documentation/javascript/cloud-setup) | Yes | Interactive map rendering | +| [Static Maps API](https://developers.google.com/maps/documentation/maps-static/cloud-setup) | Recommended | Placeholder image before JS loads (unless you provide a `#placeholder` slot) | +| [Geocoding API](https://developers.google.com/maps/documentation/geocoding/cloud-setup) | Optional | Server-side geocode proxy for query strings (e.g. `center="Brooklyn Bridge, NY"`) | +| [Places API](https://developers.google.com/maps/documentation/places/web-service/cloud-setup) | Optional | Client-side fallback when geocode proxy is unavailable and using query strings | + +## Cost Breakdown + +Google Maps uses a pay-per-use model. Here's what each interaction costs: + +| Action | API Charged | Cost per 1,000 | When it happens | +|---|---|---|---| +| Page loads with map placeholder | Static Maps API | $2 | Every page view (unless `#placeholder` slot overrides) | +| User interacts with map (hover/click) | Maps JavaScript API | $7 | Only when user triggers the map to load | +| Location query resolved server-side | Geocoding API | $5 | Only when using string queries like `"Sydney, Australia"` | +| Location query resolved client-side | Places API | $17 | Fallback when geocode proxy is not enabled | + +### How Nuxt Scripts Minimizes Costs + +Nuxt Scripts is designed to reduce Google Maps billing: + +**Lazy loading**: The JavaScript API ($7/1000) only loads when a user interacts with the map. If they never hover or click, you only pay for the static placeholder ($2/1000). + +**Geocode proxy**: When you enable `googleMaps: true` in the registry, the server resolves location queries via the Geocoding API ($5/1000) instead of the client-side Places API ($17/1000). That's a 70% cost reduction for query resolution. + +**Static maps proxy**: Placeholder images are routed through your server, which enables caching. Repeated visits to the same page serve cached images instead of hitting Google's API again. + +### Cost Optimization Tips + +1. **Use coordinates, not queries**: Pass `center` as `{ lat, lng }` instead of `"Sydney, Australia"` to avoid geocoding charges entirely. + +2. **Provide a `#placeholder` slot**: Replace the static map image with your own placeholder to eliminate Static Maps API charges. + + ```vue + + + + ``` + +3. **Use `trigger="immediate"`** only when needed: The default trigger (mouseover/mousedown) means most page views won't load the JS API. Setting `trigger="immediate"` charges $7/1000 on every page view. + +4. **Consider Iframe Embed for non-interactive maps**: If you don't need full interactivity, the [Google Maps Embed API](https://developers.google.com/maps/documentation/embed/get-started) is free and you can load it with `useScript()`{lang="ts"}. + +## Monthly Cost Estimates + +Assuming 100,000 page views per month: + +| Scenario | Monthly Cost | +|---|---| +| Static placeholder only (no interaction) | ~$200 | +| 20% of visitors interact with map | ~$340 | +| All visitors interact with map | ~$900 | +| Custom `#placeholder` slot, 20% interact | ~$140 | + +These estimates assume you have Google's [$200/month free credit](https://mapsplatform.google.com/pricing/). Most small to mid-size sites fall within the free tier. diff --git a/docs/content/scripts/google-maps/2.api/.navigation.yml b/docs/content/scripts/google-maps/2.api/.navigation.yml new file mode 100644 index 00000000..e506a40e --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/.navigation.yml @@ -0,0 +1,2 @@ +title: API +icon: i-lucide-code diff --git a/docs/content/scripts/google-maps/2.api/1.script-google-maps.md b/docs/content/scripts/google-maps/2.api/1.script-google-maps.md new file mode 100644 index 00000000..d54a93a7 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/1.script-google-maps.md @@ -0,0 +1,130 @@ +--- +title: +--- + +The [``{lang="html"}](/scripts/google-maps){lang="html"} component is a wrapper around the [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps/api/use-script-google-maps){lang="ts"} composable. It provides a simple way to embed Google Maps in your Nuxt app. + +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). + +By default, it will load on the `mouseenter`, `mouseover`, and `mousedown` events. + +## Key Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `apiKey` | `string` | - | Google Maps API key | +| `center` | `LatLngLiteral \| LatLng \| string` | - | Map center (coordinates or query string) | +| `zoom` | `number` | `15` | Map zoom level (0-21). Reactive, takes precedence over `mapOptions.zoom` | +| `mapOptions` | `google.maps.MapOptions` | - | Full map configuration | +| `markers` | `(string \| AdvancedMarkerElementOptions)[]` | - | Quick markers (use SFC components for full control) | +| `trigger` | `ElementScriptTrigger` | `['mouseenter', 'mouseover', 'mousedown']` | When to load the Google Maps script | +| `aboveTheFold` | `boolean` | `false` | Prioritize placeholder image loading | + +See the [Facade Component API](/docs/guides/facade-components#facade-components-api) for all props, events, and slots. + +## Events + +The [``{lang="html"}](/scripts/google-maps){lang="html"} component emits a single `ready` event when Google Maps loads. + +The `ready` event payload contains: + +| Property | Type | Description | +|---|---|---| +| `googleMaps` | `Ref`{lang="html"} | The Google Maps API namespace | +| `map` | `ShallowRef`{lang="html"} | The map instance | +| `createAdvancedMapMarker` | `(options?) => Promise`{lang="html"} | Create a marker programmatically | +| `resolveQueryToLatLng` | `(query: string) => Promise`{lang="html"} | Geocode a location string | +| `importLibrary` | `(key: string) => Promise`{lang="html"} | Load an additional Google Maps library | + +To subscribe to Google Map events, you can use the `ready` event. + +```vue + + + +``` + +## Slots + +The component provides minimal UI by default, only enough to be functional and accessible. There are a number of slots for you to customize the maps however you like. + +**default** + +The default slot displays content that will always be visible. + +```vue + +``` + +**awaitingLoad** + +Shown before the user triggers the map to load (e.g. before hover/click). Use this to show a call-to-action overlay on top of the static placeholder. + +```vue + +``` + +**loading** + +Shown after the user triggers loading but before the map is interactive (script is being fetched/initialized). + +Note: This shows a `ScriptLoadingIndicator` by default for accessibility and UX, by providing a slot you will +override this component. Make sure you provide a loading indicator. + +```vue + +``` + +**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. + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/10.rectangle.md b/docs/content/scripts/google-maps/2.api/10.rectangle.md new file mode 100644 index 00000000..74d4c04e --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/10.rectangle.md @@ -0,0 +1,30 @@ +--- +title: +--- + +Rectangular overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsRectangle"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/11.geojson.md b/docs/content/scripts/google-maps/2.api/11.geojson.md new file mode 100644 index 00000000..578e8ebb --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/11.geojson.md @@ -0,0 +1,60 @@ +--- +title: +--- + +Renders [GeoJSON](https://geojson.org/) data on the map using the Google Maps Data layer. Accepts a URL string or inline GeoJSON object. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsGeoJson"} +:: + +## Usage + +### Inline GeoJSON + +```vue + + + +``` + +### Remote URL + +```vue + +``` + +Both `src` and `style` are reactive. Changing `src` clears existing features and loads the new data. Changing `style` updates feature styling in place. diff --git a/docs/content/scripts/google-maps/2.api/12.heatmap-layer.md b/docs/content/scripts/google-maps/2.api/12.heatmap-layer.md new file mode 100644 index 00000000..f7f6c2aa --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/12.heatmap-layer.md @@ -0,0 +1,37 @@ +--- +title: +deprecated: true +--- + +Heatmap visualization layer. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. The component automatically loads the `visualization` library. + +::callout{color="amber"} +Google has deprecated `HeatmapLayer` (May 2025) and will remove it in May 2026. Consider using [deck.gl HeatmapLayer](https://deck.gl/docs/api-reference/aggregation-layers/heatmap-layer) as an alternative. +:: + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsHeatmapLayer"} +:: + +## Usage + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/2.api/13.overlay-view.md b/docs/content/scripts/google-maps/2.api/13.overlay-view.md new file mode 100644 index 00000000..49f86c89 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/13.overlay-view.md @@ -0,0 +1,99 @@ +--- +title: +--- + +Renders arbitrary Vue slot content at a map lat/lng position. Unlike `InfoWindow`, you have full control over HTML structure and styling. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsOverlayView"} +:: + +## Marker Anchoring + +When nested inside a `ScriptGoogleMapsMarker` or `ScriptGoogleMapsAdvancedMarkerElement`, the overlay automatically inherits the marker's position. This makes it a fully customizable alternative to `InfoWindow`, the overlay follows the marker when dragged. + +```vue + +``` + +## Popup on Marker Click + +Using `v-model:open` keeps the overlay mounted, toggling visibility via CSS. This avoids remount cost and preserves internal state. + +```vue + + + +``` + +For simple cases where remounting is acceptable, `v-if` also works: + +```vue + + + + + +``` + +## Persistent Label + +```vue + +``` + +::callout +The `blockMapInteraction` prop (default `true`) calls `google.maps.OverlayView.preventMapHitsAndGesturesFrom()`{lang="ts"} to stop clicks, taps, and drags from propagating through the overlay to the map. Set it to `false` for non-interactive overlays like labels. +:: diff --git a/docs/content/scripts/google-maps/2.api/14.use-script-google-maps.md b/docs/content/scripts/google-maps/2.api/14.use-script-google-maps.md new file mode 100644 index 00000000..0ef72002 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/14.use-script-google-maps.md @@ -0,0 +1,39 @@ +--- +title: useScriptGoogleMaps() +--- + +The [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps/api/use-script-google-maps){lang="ts"} composable lets you have fine-grained control over the Google Maps SDK. It provides a way to load the Google Maps SDK and interact with it programmatically. + +```ts +export function useScriptGoogleMaps(_options?: GoogleMapsInput) {} +``` + +Please follow the [Registry Scripts](/docs/guides/registry-scripts) guide to learn more about advanced usage. + +## Example + +Loading the Google Maps SDK and interacting with it programmatically. + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/2.api/2.marker.md b/docs/content/scripts/google-maps/2.api/2.marker.md new file mode 100644 index 00000000..eb4d5dd0 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/2.marker.md @@ -0,0 +1,43 @@ +--- +title: +--- + +::callout{color="amber" icon="i-heroicons-exclamation-triangle"} +`google.maps.Marker` is deprecated by Google. Use [``{lang="html"}](/scripts/google-maps/api/advanced-marker-element) instead for new projects. +:: + +Classic map marker with icon support. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsMarker"} +:: + +## Usage + +```vue + +``` + +### With Info Window + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md b/docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md new file mode 100644 index 00000000..c7bcb823 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md @@ -0,0 +1,70 @@ +--- +title: +--- + +Modern advanced marker with HTML content support. This is the recommended marker type. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsAdvancedMarkerElement"} +:: + +## Usage + +```vue + +``` + +### Custom Marker Content + +The `#content` slot replaces the default pin with any HTML or Vue content. The slot content becomes the marker's visual on the map. + +```vue + +``` + +::callout +Use `ScriptGoogleMapsPinElement` if you only need to customize colors or glyph while keeping the pin shape. The `#content` slot replaces the marker entirely. If you use both, `PinElement` takes precedence. +:: + +### With Custom Overlay + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/4.pin-element.md b/docs/content/scripts/google-maps/2.api/4.pin-element.md new file mode 100644 index 00000000..11522e81 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/4.pin-element.md @@ -0,0 +1,29 @@ +--- +title: +--- + +Customizable pin marker. Place as a child of [``{lang="html"}](/scripts/google-maps/api/advanced-marker-element). + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsPinElement"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/5.info-window.md b/docs/content/scripts/google-maps/2.api/5.info-window.md new file mode 100644 index 00000000..b7de4b5a --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/5.info-window.md @@ -0,0 +1,27 @@ +--- +title: +--- + +Information window that automatically opens on parent marker click. Place as a child of [``{lang="html"}](/scripts/google-maps/api/marker) or [``{lang="html"}](/scripts/google-maps/api/advanced-marker-element). + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsInfoWindow"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/6.marker-clusterer.md b/docs/content/scripts/google-maps/2.api/6.marker-clusterer.md new file mode 100644 index 00000000..5a3bcbd1 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/6.marker-clusterer.md @@ -0,0 +1,38 @@ +--- +title: +--- + +Groups nearby markers into clusters for cleaner map visualization at lower zoom levels. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsMarkerClusterer"} +:: + +## Installation + +Requires the `@googlemaps/markerclusterer` peer dependency: + +```bash +pnpm add @googlemaps/markerclusterer +``` + +## Usage + +Child markers register and unregister themselves automatically. + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/7.circle.md b/docs/content/scripts/google-maps/2.api/7.circle.md new file mode 100644 index 00000000..93d93803 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/7.circle.md @@ -0,0 +1,28 @@ +--- +title: +--- + +Circular overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsCircle"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/8.polygon.md b/docs/content/scripts/google-maps/2.api/8.polygon.md new file mode 100644 index 00000000..ca5414f3 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/8.polygon.md @@ -0,0 +1,29 @@ +--- +title: +--- + +Polygon shape overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsPolygon"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/9.polyline.md b/docs/content/scripts/google-maps/2.api/9.polyline.md new file mode 100644 index 00000000..499029df --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/9.polyline.md @@ -0,0 +1,29 @@ +--- +title: +--- + +Line path overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsPolyline"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/index.md b/docs/content/scripts/google-maps/index.md new file mode 100644 index 00000000..848229c5 --- /dev/null +++ b/docs/content/scripts/google-maps/index.md @@ -0,0 +1,227 @@ +--- +title: Google Maps +description: Show performance-optimized Google Maps in your Nuxt app. +links: + - label: useScriptGoogleMaps + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/google-maps.ts + size: xs + - label: "" + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue + size: xs +--- + +[Google Maps](https://maps.google.com/) allows you to embed maps in your website and customize them with your content. + +Nuxt Scripts provides a [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps/api/use-script-google-maps){lang="ts"} composable and a headless [``{lang="html"}](/scripts/google-maps/api/script-google-maps){lang="html"} component to interact with the Google Maps. + +::script-types{exclude-components} +:: + +## Types + +To use Google Maps with full TypeScript support, you will need +to install the `@types/google.maps` dependency. + +```bash +pnpm add -D @types/google.maps +``` + +## Setup + +Enable Google Maps in your `nuxt.config` and provide your API key via environment variable: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + scripts: { + registry: { + googleMaps: true, + }, + }, + runtimeConfig: { + public: { + scripts: { + googleMaps: { + apiKey: '', // NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY + }, + }, + }, + }, +}) +``` + +```text [.env] +NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY= +``` + +You must add this. It registers server proxy routes that keep your API key server-side: +- `/_scripts/proxy/google-static-maps` for placeholder images +- `/_scripts/proxy/google-maps-geocode` for location search + +::callout{color="amber"} +You can pass `api-key` directly on the ``{lang="html"} component, but this approach is not recommended, as it exposes your key in client-side requests. +:: + +See [Billing & Permissions](/scripts/google-maps/guides/billing) for API costs and required permissions. + +## Demo + +::code-group + +:google-maps-demo{label="Output"} + +```vue [Input] + + + +``` + +:: + +## Quick Start + +### Minimal Map + +```vue + +``` + +### Markers with Info Windows + +```vue + +``` + +### Custom Marker Content + +The `#content` slot replaces the default pin with any Vue template. + +```vue + +``` + +### Custom Popups + +`ScriptGoogleMapsOverlayView` gives full control over popup styling. Use `v-model:open` to toggle without remounting. + +```vue + + + +``` diff --git a/playground/pages/third-parties/google-maps/sfcs.vue b/playground/pages/third-parties/google-maps/sfcs.vue index 3fd79955..79b6d620 100644 --- a/playground/pages/third-parties/google-maps/sfcs.vue +++ b/playground/pages/third-parties/google-maps/sfcs.vue @@ -58,6 +58,19 @@ const geoJsonData = { ], } +const isOverlayViewShown = ref(false) + +const isOverlayViewOnMarkerShown = ref(false) + +const isOverlayViewOnAdvancedMarkerShown = ref(false) + +const isCustomMarkerContentShown = ref(false) + +const isOverlayPopupShown = ref(false) +const overlayPopupOpen = ref(false) + +const zoom = ref(8) + const googleMapsRef = useTemplateRef('googleMapsRef') whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => { @@ -88,9 +101,9 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => { :width="1280" :height="720" above-the-fold + :zoom="zoom" :map-options="{ center: { lat: -34.397, lng: 150.644 }, - zoom: 8, mapId: 'DEMO_MAP_ID', }" > @@ -224,6 +237,80 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => { }" /> + +
+ Custom Overlay +

Vue slot content with full reactivity

+
+
+ + + +
+ Drag me! Custom overlay on Marker +
+
+
+ + + +
+ Custom tooltip +
OverlayView on AdvancedMarker
+
+
+
+ + + + + + + + + +
+ Custom Popup +

v-model:open, no remount

+ +
+
+
+ + {{ `${isOverlayViewShown ? 'Hide' : 'Show'} overlay view` }} + + + + + + + + + + + +
+ + +
diff --git a/scripts/generate-registry-types.ts b/scripts/generate-registry-types.ts index 2af5a242..1f22b971 100644 --- a/scripts/generate-registry-types.ts +++ b/scripts/generate-registry-types.ts @@ -13,11 +13,6 @@ interface ExtractedDeclaration { code: string } -interface ExtractedProps { - code: string - defaults: Record -} - // --- Helpers --- function getName(node: any): string | null { @@ -549,6 +544,8 @@ const componentToSlug: Record = { ScriptGoogleMapsPolygon: 'google-maps', ScriptGoogleMapsPolyline: 'google-maps', ScriptGoogleMapsRectangle: 'google-maps', + ScriptGoogleMapsOverlayView: 'google-maps', + ScriptGoogleMapsGeoJson: 'google-maps', ScriptCarbonAds: 'carbon-ads', ScriptCrisp: 'crisp', ScriptIntercom: 'intercom', diff --git a/src/registry-types.json b/src/registry-types.json index a87a611f..adab2f46 100644 --- a/src/registry-types.json +++ b/src/registry-types.json @@ -202,7 +202,7 @@ { "name": "ScriptGoogleMapsAdvancedMarkerElementEvents", "kind": "interface", - "code": "interface ScriptGoogleMapsAdvancedMarkerElementEvents {\n animation_changed: -\n clickable_changed: -\n cursor_changed: -\n draggable_changed: -\n flat_changed: -\n icon_changed: -\n position_changed: -\n shape_changed: -\n title_changed: -\n visible_changed: -\n zindex_changed: -\n click: google.maps.MapMouseEvent\n contextmenu: google.maps.MapMouseEvent\n dblclick: google.maps.MapMouseEvent\n drag: google.maps.MapMouseEvent\n dragend: google.maps.MapMouseEvent\n dragstart: google.maps.MapMouseEvent\n mousedown: google.maps.MapMouseEvent\n mouseout: google.maps.MapMouseEvent\n mouseover: google.maps.MapMouseEvent\n mouseup: google.maps.MapMouseEvent\n}" + "code": "interface ScriptGoogleMapsAdvancedMarkerElementEvents {\n click: google.maps.MapMouseEvent\n drag: google.maps.MapMouseEvent\n dragend: google.maps.MapMouseEvent\n dragstart: google.maps.MapMouseEvent\n}" }, { "name": "ScriptGoogleMapsCircleProps", @@ -214,6 +214,16 @@ "kind": "interface", "code": "interface ScriptGoogleMapsCircleEvents {\n center_changed: -\n radius_changed: -\n click: google.maps.MapMouseEvent\n dblclick: google.maps.MapMouseEvent\n drag: google.maps.MapMouseEvent\n dragend: google.maps.MapMouseEvent\n dragstart: google.maps.MapMouseEvent\n mousedown: google.maps.MapMouseEvent\n mousemove: google.maps.MapMouseEvent\n mouseout: google.maps.MapMouseEvent\n mouseover: google.maps.MapMouseEvent\n mouseup: google.maps.MapMouseEvent\n rightclick: google.maps.MapMouseEvent\n}" }, + { + "name": "ScriptGoogleMapsGeoJsonProps", + "kind": "interface", + "code": "interface ScriptGoogleMapsGeoJsonProps {\n src: string | object\n style?: google.maps.Data.StylingFunction | google.maps.Data.StyleOptions\n}" + }, + { + "name": "ScriptGoogleMapsGeoJsonEvents", + "kind": "interface", + "code": "interface ScriptGoogleMapsGeoJsonEvents {\n click: google.maps.Data.MouseEvent\n contextmenu: google.maps.Data.MouseEvent\n dblclick: google.maps.Data.MouseEvent\n mousedown: google.maps.Data.MouseEvent\n mousemove: google.maps.Data.MouseEvent\n mouseout: google.maps.Data.MouseEvent\n mouseover: google.maps.Data.MouseEvent\n mouseup: google.maps.Data.MouseEvent\n addfeature: google.maps.Data.AddFeatureEvent\n removefeature: google.maps.Data.RemoveFeatureEvent\n setgeometry: google.maps.Data.SetGeometryEvent\n setproperty: google.maps.Data.SetPropertyEvent\n removeproperty: google.maps.Data.RemovePropertyEvent\n}" + }, { "name": "ScriptGoogleMapsHeatmapLayerProps", "kind": "interface", @@ -2116,57 +2126,41 @@ ], "ScriptGoogleMapsAdvancedMarkerElementEvents": [ { - "name": "animation_changed", - "type": "-", - "required": false - }, - { - "name": "clickable_changed", - "type": "-", - "required": false - }, - { - "name": "cursor_changed", - "type": "-", - "required": false - }, - { - "name": "draggable_changed", - "type": "-", - "required": false - }, - { - "name": "flat_changed", - "type": "-", + "name": "click", + "type": "google.maps.MapMouseEvent", "required": false }, { - "name": "icon_changed", - "type": "-", + "name": "drag", + "type": "google.maps.MapMouseEvent", "required": false }, { - "name": "position_changed", - "type": "-", + "name": "dragend", + "type": "google.maps.MapMouseEvent", "required": false }, { - "name": "shape_changed", - "type": "-", + "name": "dragstart", + "type": "google.maps.MapMouseEvent", "required": false - }, + } + ], + "ScriptGoogleMapsCircleProps": [ { - "name": "title_changed", - "type": "-", + "name": "options", + "type": "Omit", "required": false - }, + } + ], + "ScriptGoogleMapsCircleEvents": [ { - "name": "visible_changed", + "name": "center_changed", "type": "-", "required": false }, { - "name": "zindex_changed", + "name": "radius_changed", "type": "-", "required": false }, @@ -2175,11 +2169,6 @@ "type": "google.maps.MapMouseEvent", "required": false }, - { - "name": "contextmenu", - "type": "google.maps.MapMouseEvent", - "required": false - }, { "name": "dblclick", "type": "google.maps.MapMouseEvent", @@ -2205,6 +2194,11 @@ "type": "google.maps.MapMouseEvent", "required": false }, + { + "name": "mousemove", + "type": "google.maps.MapMouseEvent", + "required": false + }, { "name": "mouseout", "type": "google.maps.MapMouseEvent", @@ -2219,79 +2213,89 @@ "name": "mouseup", "type": "google.maps.MapMouseEvent", "required": false + }, + { + "name": "rightclick", + "type": "google.maps.MapMouseEvent", + "required": false } ], - "ScriptGoogleMapsCircleProps": [ + "ScriptGoogleMapsGeoJsonProps": [ { - "name": "options", - "type": "Omit", + "name": "src", + "type": "string | object", + "required": true + }, + { + "name": "style", + "type": "google.maps.Data.StylingFunction | google.maps.Data.StyleOptions", "required": false } ], - "ScriptGoogleMapsCircleEvents": [ + "ScriptGoogleMapsGeoJsonEvents": [ { - "name": "center_changed", - "type": "-", + "name": "click", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "radius_changed", - "type": "-", + "name": "contextmenu", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "click", - "type": "google.maps.MapMouseEvent", + "name": "dblclick", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "dblclick", - "type": "google.maps.MapMouseEvent", + "name": "mousedown", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "drag", - "type": "google.maps.MapMouseEvent", + "name": "mousemove", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "dragend", - "type": "google.maps.MapMouseEvent", + "name": "mouseout", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "dragstart", - "type": "google.maps.MapMouseEvent", + "name": "mouseover", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "mousedown", - "type": "google.maps.MapMouseEvent", + "name": "mouseup", + "type": "google.maps.Data.MouseEvent", "required": false }, { - "name": "mousemove", - "type": "google.maps.MapMouseEvent", + "name": "addfeature", + "type": "google.maps.Data.AddFeatureEvent", "required": false }, { - "name": "mouseout", - "type": "google.maps.MapMouseEvent", + "name": "removefeature", + "type": "google.maps.Data.RemoveFeatureEvent", "required": false }, { - "name": "mouseover", - "type": "google.maps.MapMouseEvent", + "name": "setgeometry", + "type": "google.maps.Data.SetGeometryEvent", "required": false }, { - "name": "mouseup", - "type": "google.maps.MapMouseEvent", + "name": "setproperty", + "type": "google.maps.Data.SetPropertyEvent", "required": false }, { - "name": "rightclick", - "type": "google.maps.MapMouseEvent", + "name": "removeproperty", + "type": "google.maps.Data.RemovePropertyEvent", "required": false } ], diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue index e0d6cd90..03f4d3b6 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue @@ -57,6 +57,11 @@ const props = withDefaults(defineProps<{ * A latitude / longitude of where to focus the map. */ center?: google.maps.LatLng | google.maps.LatLngLiteral | `${string},${string}` + /** + * Zoom level for the map (0-21). Reactive: changing this will update the map. + * Takes precedence over mapOptions.zoom when provided. + */ + zoom?: number /** * Should a marker be displayed on the map where the centre is. */ @@ -152,8 +157,12 @@ const currentMapId = computed(() => { const mapsApi = ref() -if (import.meta.dev && !apiKey) - throw new Error('GoogleMaps requires an API key. Please provide `apiKey` on the or globally via `runtimeConfig.public.scripts.googleMaps.apiKey`.') +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 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') +} // TODO allow a null center may need to be resolved via an API function @@ -175,7 +184,7 @@ const { load, status, onLoaded } = useScriptGoogleMaps({ const options = computed(() => { const mapId = props.mapOptions?.styles ? undefined : (currentMapId.value || 'map') - return defu({ center: centerOverride.value, mapId }, props.mapOptions, { + return defu({ center: centerOverride.value, mapId, zoom: props.zoom }, props.mapOptions, { center: props.center, zoom: 15, }) @@ -236,7 +245,7 @@ async function createAdvancedMapMarker(_options?: google.maps.marker.AdvancedMar const queryToLatLngCache = new Map() -async function resolveQueryToLatLang(query: string) { +async function resolveQueryToLatLng(query: string) { if (query && typeof query === 'object') return Promise.resolve(query) if (queryToLatLngCache.has(query)) { @@ -319,7 +328,7 @@ const googleMaps = { googleMaps: mapsApi, map, createAdvancedMapMarker, - resolveQueryToLatLang, + resolveQueryToLatLng, importLibrary, } as const @@ -383,7 +392,7 @@ onMounted(() => { if (center) { if (isLocationQuery(center) && ready.value) { // need to resolve center from query - center = await resolveQueryToLatLang(center as string) + center = await resolveQueryToLatLng(center as string) } map.value!.setCenter(center as google.maps.LatLng) if (props.centerMarker) { @@ -418,7 +427,7 @@ onMounted(() => { map.value = new mapsApi.value!.Map(mapEl.value!, _options) if (center && isLocationQuery(center)) { // need to resolve center - centerOverride.value = await resolveQueryToLatLang(center) + centerOverride.value = await resolveQueryToLatLng(center) map.value?.setCenter(centerOverride.value) } ready.value = true diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue index b13fd3a4..166b730e 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue @@ -1,5 +1,6 @@ diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue index 163687d8..712c6fa6 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue @@ -1,5 +1,6 @@