SSR / Nuxt
vue3-maptiler-gl is SSR-safe out of the box. Map creation is guarded with isBrowser checks and all window.* references have been removed.
Why SSR Compatibility Matters
MapTiler SDK requires:
- WebGL context (browser exclusive)
- DOM manipulation (render phase only)
- window/document APIs
Without SSR guards, rendering on the server would fail. Vue3 MapTiler SDK handles this automatically.
Nuxt Module (Recommended)
The official Nuxt module handles SSR configuration automatically. Install it for the best DX:
bun add nuxt-maptiler-glnpm install nuxt-maptiler-glyarn add nuxt-maptiler-glpnpm add nuxt-maptiler-glSetup
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-maptiler-gl'],
maptiler: {
/**
* Auto-import vue3-maptiler-gl CSS styles
* @default true
*/
css: true,
/**
* Prefix for auto-imported composables
* Set to 'map' to use mapUseMapTiler, mapUseFlyTo, etc.
* @default '' (no prefix)
*/
prefix: '',
},
});Module Features
The module automatically configures:
- CSS Auto-Import -
@maptiler/sdk/dist/maptiler-sdk.cssandvue3-maptiler-gl/dist/style.cssinjected - Component Auto-Import - All 10 components available without imports
- Composable Auto-Import - All 38 composables available without imports
- SSR Support - Map components rendered only on client
- Build Configuration:
- vue3-maptiler-gl transpiled for SSR
- @maptiler/sdk excluded from SSR bundle (requires WebGL)
- vite.optimizeDeps.exclude configured
Usage
With the module installed, use components and composables directly in templates/scripts without imports:
<template>
<ClientOnly>
<!-- Components available without imports -->
<MapTiler :options="mapOptions" style="height: 500px">
<GeoJsonSource :data="geoData">
<FillLayer :style="fillStyle" />
</GeoJsonSource>
</MapTiler>
</ClientOnly>
</template>
<script setup>
// No imports needed - all auto-imported by module
import { ref } from 'vue';
const mapOptions = ref({
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
const geoData = ref({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: { name: 'Center' },
},
],
});
const fillStyle = ref({
'fill-color': '#088',
'fill-opacity': 0.8,
});
// Composables auto-imported
const { mapInstance, isMapReady } = useMapTiler();
watch(isMapReady, async () => {
// Map is ready
const { flyTo } = useFlyTo({ map: mapInstance });
await flyTo({ center: [100, 50], zoom: 10 });
});
</script>Auto-Imported Composables
The Nuxt module auto-imports these composables by default:
// Map Management
(useCreateMapTiler, useMapTiler, useMapTilerConfig);
// Camera Animations (7)
(useFlyTo,
useEaseTo,
useJumpTo,
useFitBounds,
useCameraForBounds,
usePanBy,
usePanTo,
useZoomTo,
// Zoom/Rotation (6)
useZoomIn,
useZoomOut,
useRotateTo,
useResetNorth,
useResetNorthPitch,
useSnapToNorth);
// Layers (4)
(useCreateFillLayer,
useCreateCircleLayer,
useCreateLineLayer,
useCreateSymbolLayer);
// Events (3)
(useMapEventListener, useLayerEventListener, useGeolocateEventListener);
// Sources (2)
(useCreateGeoJsonSource, useGeoJsonSource);
// Controls
useGeolocateControl;Auto-Imported Components
// Core
(MapTiler, GeoJsonSource);
// Layers (4)
(FillLayer, CircleLayer, LineLayer, SymbolLayer);
// Overlays
(Marker, Popup, Image);
// Controls
GeolocateControls;Using Composable Prefix
Optionally add a prefix to avoid conflicts:
// nuxt.config.ts
export default defineNuxtConfig({
maptiler: {
prefix: 'map',
},
});Then use with prefix:
// Composables now prefixed
mapUseMapTiler(); // instead of useMapTiler()
mapUseFlyTo(); // instead of useFlyTo()
mapUseMapEventListener(); // instead of useMapEventListener()Manual Setup (Without Module)
If you prefer not to use the module, follow these steps:
1. Install Package
bun add vue3-maptiler-gl @maptiler/sdknpm install vue3-maptiler-gl @maptiler/sdkyarn add vue3-maptiler-gl @maptiler/sdkpnpm add vue3-maptiler-gl @maptiler/sdk2. Configure Nuxt
// nuxt.config.ts
export default defineNuxtConfig({
css: [
'@maptiler/sdk/dist/maptiler-sdk.css',
'vue3-maptiler-gl/dist/style.css',
],
build: {
transpile: ['vue3-maptiler-gl'],
},
vite: {
optimizeDeps: {
exclude: ['@maptiler/sdk'],
},
},
});3. Wrap Components in ClientOnly
<template>
<!-- Critical: Wrap map components in ClientOnly -->
<ClientOnly>
<MapTiler :options="mapOptions" style="height: 500px">
<GeoJsonSource :data="geoData">
<FillLayer :style="fillStyle" />
</GeoJsonSource>
</MapTiler>
</ClientOnly>
</template>
<script setup>
import { ref } from 'vue';
import { MapTiler, GeoJsonSource, FillLayer } from 'vue3-maptiler-gl';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import 'vue3-maptiler-gl/dist/style.css';
const mapOptions = ref({
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
const geoData = ref({ type: 'FeatureCollection', features: [] });
const fillStyle = ref({ 'fill-color': '#088', 'fill-opacity': 0.8 });
</script>SSR Internals
Understanding what happens under the hood:
Browser Guards
All components check isBrowser before creating map instances:
import { isBrowser } from 'vue3-maptiler-gl';
import { Map } from 'vue3-maptiler-gl/maptiler';
if (isBrowser) {
// Safe to create MapTiler instance
const map = new Map({ container: el, style: 'url' });
}
// On server: skipped, no error@maptiler/sdk Exclusion
The @maptiler/sdk package is excluded from SSR bundling:
// Usually removed from server bundle
import { Map, GeoJSONSource } from '@maptiler/sdk';
// Server: import fails gracefully (never called)
// Client: import succeeds (WebGL available)ClientOnly Wrapper
Ensures components only render in the browser:
<ClientOnly>
<!-- Rendered only on client -->
<MapTiler ... />
</ClientOnly>
<!-- Fallback shown during hydration -->Deployment Considerations
Nuxt SSR Deployment (Cloudflare, Vercel, Netlify, etc.)
- Module handles everything - Deploy as normal
- Check build logs - Verify @maptiler/sdk excluded from server build
- Test locally first -
nuxi generatefor static generation
Hybrid Rendering (Nuxt)
Use route rules for optimal performance:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/maps/**': { swr: 3600 }, // ISR: revalidate hourly
},
});Static Generation (SSG)
Maps can't be pre-rendered (client-only), use hybrid rendering:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true,
// Exclude map routes from prerendering
ignore: ['/maps'],
},
},
});Troubleshooting
"window is not defined"
Cause: Map component rendered on server
Fix: Wrap in <ClientOnly>
<ClientOnly>
<MapTiler ... />
</ClientOnly>"WebGL context lost"
Cause: Usually transient, map recovers automatically
Fix: Use the error event (or the onMapError prop) to recover
<MapTiler :options="options" @error="handleMapError" />"@maptiler/sdk not found"
Cause: Not transpiled for SSR
Fix: Configure Nuxt
// nuxt.config.ts
export default defineNuxtConfig({
build: {
transpile: ['vue3-maptiler-gl'],
},
});Performance Tips
- Lazy load maps - Use dynamic imports for map pages
- Use route preloading - Prefetch map routes
- Optimize GeoJSON - Simplify geometries before sending
- Enable data compression - gzip GeoJSON responses
- Cache styles - Browser cache map styles (long TTL)
Further Reading
<template>
<ClientOnly>
<MapTiler :options="mapOptions" style="height: 500px">
<GeoJsonSource :data="geoData">
<FillLayer :style="fillStyle" />
</GeoJsonSource>
</MapTiler>
</ClientOnly>
</template>
<script setup>
import { ref } from 'vue';
import { MapTiler, GeoJsonSource, FillLayer } from 'vue3-maptiler-gl';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import 'vue3-maptiler-gl/dist/style.css';
const mapOptions = ref({
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
const geoData = ref({ type: 'FeatureCollection', features: [] });
const fillStyle = ref({ 'fill-color': '#088', 'fill-opacity': 0.8 });
</script>Nuxt Config
If you encounter SSR build errors with @maptiler/sdk, add it to noExternal:
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
optimizeDeps: {
exclude: ['@maptiler/sdk'],
},
},
// Transpile the library for SSR
build: {
transpile: ['vue3-maptiler-gl'],
},
});How It Works
The library uses a simple isBrowser guard:
// Exported from vue3-maptiler-gl
export const isBrowser =
typeof window !== 'undefined' && typeof document !== 'undefined';This guard is checked before:
- Map creation (
useCreateMapTiler) - Marker creation (
useCreateMarker) - Popup creation (
useCreatePopup)
During SSR, these composables return early without creating DOM elements or WebGL contexts.
Vue SPA (Non-Nuxt)
No special configuration needed. The library works normally in Vue SPA mode since isBrowser is always true in the browser.