Skip to Content
Installation

Installation

There are three ways to load <mapvx-website>. They all lead to the same result: the code of main.js running as an ES module, styles.css loaded as a stylesheet, and one <mapvx-website> element in the page.

MethodUse it for
CDNPlain HTML pages, CMS templates and quick tests.
npm and a bundlerClient-rendered apps built with Vite or a similar tool.
Self-hostingServing the files from your own origin, including frameworks that serve static files from a public folder, such as Next.js and Angular.

The examples use version 1.6.1, the current release. Pin the exact version you tested, both in URLs and in package.json.

Requirements

  • MapVX credentials: an API key and an institution id issued by MapVX. The API key is required for the data, the map, search and routes. Without a matching institution id, the component falls back to the first configured city and shows no category tabs on first load. See api-key and institution-id.
  • Browser: custom elements, ES modules (<script type="module">) and CSS container queries, which drive the responsive layout.
  • WebGL for the map (map view, place profiles and routes). The grid of place cards does not need it. Version 1.6.1 uses WebGL2 and falls back to WebGL 1; plan for WebGL2, because the map engine bundled in upcoming releases requires it.
  • One element per page. Use a single <mapvx-website> per page: all elements on a page share one application (router, loaded data, map and SDK) and interfere with each other. See Multiple instances.
  • Content Security Policy: if your site sends one, allow the sources listed in Security and privacy.
  • Node.js, for npm installs only: the package declares engines.node as ^22.22.3 || >=24.15.0. It has no effect in the browser. On other Node versions npm prints an EBADENGINE warning and pnpm an Unsupported engine warning (both fail only with engine-strict), and Yarn 1 stops the install unless you pass --ignore-engines.

Package contents

@mapvx/web-components-ssr publishes these files:

FileWhat it is
dist/browser/main.jsThe element: an ES module of about 1.8 MB (about 500 KB gzipped) that contains everything it runs, including its own copy of Angular and the map engine. When it runs, it registers <mapvx-website>. The package’s main field points to it.
dist/browser/chunk-*.jsCode that main.js loads on demand, only for 3D maps (the 3D map stack). The component’s map is 2D in 1.6.1, so a page load does not request these files. The file names carry content hashes that change between releases, and every chunk imports ./main.js by that exact name.
dist/browser/styles.cssThe required global stylesheet (about 106 KB, 15 KB gzipped): design tokens, map, marker and icon styles. main.js does not inject it.
dist/types/public-api.d.tsTypeScript declarations, referenced by the package’s types field.
LICENSE, README.md, package.jsonPackage metadata.

The package does not include:

  • Fonts or images. The component asks for the Open Sans font but does not load it; see Fonts. Place images come from MapVX data.
  • A server build. The element renders only in the browser, and evaluating main.js in Node throws. See SSR and initial data.
  • A runtime JavaScript API. main.js exports only internal names used by its own chunks. The declarations are for import type only; see Types only.
  • Typings for the element itself (JSX or HTMLElementTagNameMap). See Typing the element.
  • Polyfills. None are included, and the page does not need zone.js.
  • A separate map worker file. The map worker is embedded in main.js and started from a blob: URL, which a Content Security Policy must allow (CSP).
  • Source maps.

The package has no exports field, so deep imports such as @mapvx/web-components-ssr/dist/browser/styles.css resolve.

npm

npm install --save-exact @mapvx/web-components-ssr@1.6.1

--save-exact records the exact version instead of a range, so every install gets the build you tested. Then either import the package through your bundler (Bundlers) or copy its files into your public folder (Self-hosting).

npm also installs the packages the component lists as dependencies (Angular 22 packages, Font Awesome, ngx-google-analytics, rxjs and tslib). The browser bundle does not use them: main.js already contains everything it runs.

To prepare the initial-data attribute on your server, install the helper package where your server code runs. See The helper.

npm install --save-exact @mapvx/web-components-ssr-helpers@0.1.1

CDN

<link rel="stylesheet" href="https://unpkg.com/@mapvx/web-components-ssr@1.6.1/dist/browser/styles.css" /> <script type="module" src="https://unpkg.com/@mapvx/web-components-ssr@1.6.1/dist/browser/main.js" ></script> <style> /* The element is inline by default: give it a block box */ mapvx-website { display: block; width: 100%; } </style> <mapvx-website api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID"></mapvx-website>

The URL patterns are https://unpkg.com/@mapvx/web-components-ssr@VERSION/dist/browser/FILE and https://cdn.jsdelivr.net/npm/@mapvx/web-components-ssr@VERSION/dist/browser/FILE.

  • type="module" is required. main.js is an ES module. Loaded as a classic script it fails (in Chromium, with SyntaxError: Unexpected token 'export') and the element stays empty (details). Module scripts are deferred automatically, so defer is not needed.
  • styles.css is required. Without it, colors, fonts, the map and the icons break. It is a global stylesheet that also applies to the rest of your page; see Global styles.
  • Pin an exact version. jsDelivr serves @latest in place and lets browsers cache it for up to 7 days, so a visitor can keep an old main.js after a release has changed the chunk names. unpkg redirects @latest to the exact version. Do not use the dev or rc tags in production; see Release channels.
  • Both CDNs send Access-Control-Allow-Origin: * and a JavaScript content type, so main.js can load its chunks from the same versioned URL.
  • With a Content Security Policy, allow the CDN host in script-src and style-src (Allowlist policy).
  • The display: block rule gives the element a width to lay itself out in; see Sizing.

Self-hosting

Serve the files from your own origin when you do not want a third-party CDN, or when your framework serves static files from a public folder.

Copy the whole folder

Copy dist/browser from the installed package into a directory named after the version:

// copy-mapvx.mjs: run after npm install, before building your site import { cpSync, readFileSync, rmSync } from 'node:fs' const pkgDir = 'node_modules/@mapvx/web-components-ssr' const { version } = JSON.parse(readFileSync(`${pkgDir}/package.json`, 'utf8')) const target = `public/mapvx-website/${version}` // Remove older versions, then copy main.js, every chunk-*.js and styles.css with unchanged names rmSync('public/mapvx-website', { recursive: true, force: true }) cpSync(`${pkgDir}/dist/browser`, target, { recursive: true }) console.log(`Copied @mapvx/web-components-ssr@${version} to ${target}`)

Run it from your package.json scripts, for example:

{ "scripts": { "copy-mapvx": "node copy-mapvx.mjs", "predev": "npm run copy-mapvx", "prebuild": "npm run copy-mapvx" } }

Load the files

<link rel="stylesheet" href="/mapvx-website/1.6.1/styles.css" /> <script type="module" src="/mapvx-website/1.6.1/main.js"></script>

Configure the responses

HeaderValueWhen
Content-Type for .jstext/javascriptAlways. Browsers refuse module scripts served with a non-JavaScript type, such as text/html or text/plain.
Cache-Controlpublic, max-age=31536000, immutableOnly on a versioned path, as above.
Access-Control-Allow-Origin* or your site’s originOnly when the files are served from another origin.

Keep the files exactly as published. Every chunk-*.js imports ./main.js by that name, and these rules are what keeps the bundle working when chunks are loaded. In 1.6.1, main.js loads chunks only for 3D maps, and the component’s map is 2D, so a page load does not request them and a broken rule can go unnoticed until something loads a chunk.

  • Do not rename main.js. With a renamed entry file (for example bundle.js), the chunks’ imports of ./main.js fail with a 404 when they load, or load a second copy if the original is also there. See Chunk files return 404.
  • Keep every chunk-*.js next to main.js, and never mix files from two versions. A missing or mismatched chunk fails only when it is loaded.
  • Do not add a query string to bust caches (main.js?v=2). The chunks import the plain ./main.js, which is a different URL and therefore a different module, so the first chunk that loads runs the bundle a second time, and that copy fails to register the element (Element already defined). Use a versioned directory instead.
  • On upgrade, replace the whole folder.

Caching. main.js and styles.css keep the same names in every release, while the chunk names change. That is why the long cache above is safe only on a path that contains the version. If you serve the files from a fixed path, a long cache can pair a stale main.js with the files of another version: use a short cache or no-cache for main.js and styles.css there.

Content type. A server that answers a missing file with an HTML fallback page (typical for single-page apps) makes the browser reject the module; check the path if you see a MIME type error or Unexpected token '<' (details).

Another origin. Browsers fetch module scripts and their imports in CORS mode, so if main.js and the chunks live on a different origin than the page (your own CDN, for example), that origin must send Access-Control-Allow-Origin. No crossorigin attribute is needed. If you add crossorigin="use-credentials", the server must return your exact origin instead of *, plus Access-Control-Allow-Credentials: true. styles.css, loaded with a regular stylesheet link, needs no CORS headers.

Bundlers

// Entry file of a client-rendered app import '@mapvx/web-components-ssr' import '@mapvx/web-components-ssr/dist/browser/styles.css'
  • The bare import resolves through the package’s main field to dist/browser/main.js; importing @mapvx/web-components-ssr/dist/browser/main.js is equivalent.
  • Verified with Vite 7 (build and browser run). Vite bundles the chunks again under its own file names, so the naming rules of Self-hosting do not apply to its output.
  • Not verified: webpack, Turbopack and the Angular CLI. If your bundler has trouble with the package, self-host the files instead; they need no processing.
  • Load one copy per page. Do not import the package and also add a script tag for a CDN or self-hosted copy. The second copy starts a second application and fails to register the element (Element already defined).

Client only. main.js uses browser APIs as soon as it is evaluated; in Node it throws ReferenceError: HTMLElement is not defined. Never import it at the top level of a module that also runs on the server: SSR, prerendering, or Next.js 'use client' components, which are rendered on the server too. In those setups, load it with a <script type="module"> tag or with a dynamic import in code that runs only in the browser:

// Only in browser-only code: useEffect, onMounted, onMount, a Nuxt .client plugin… import('@mapvx/web-components-ssr')

Waiting for the element

main.js starts its application first and registers <mapvx-website> a moment later, asynchronously. Elements already in the page are upgraded at that point. Most integrations do not need to wait for it:

  • Attributes set before the upgrade are read when it happens.
  • JavaScript properties set on the element before the upgrade are picked up too.
  • Event listeners can be attached right away, because the events are plain DOM events dispatched on the same element. Attach them early: events are not replayed, and a deep link can emit show-place during startup. See Listening to events.

Use customElements.whenDefined() when you need the upgraded element, for example to read a property’s current value or to hide a loading indicator of your own:

// In a module script, where top-level await is allowed const el = document.querySelector('mapvx-website') // No need to wait for these el.setAttribute('default-to-map', 'true') el.addEventListener('userAction', (event) => console.log(event.detail)) await customElements.whenDefined('mapvx-website') console.log(el.rowsPerPage) // the component's current value
  • Do not poll with setTimeout or customElements.get(): whenDefined() resolves as soon as the element is registered.
  • If main.js fails to load, or is loaded as a classic script, the promise never resolves. See Nothing renders.
  • Content you put inside <mapvx-website> is removed when the element upgrades, so do not place fallback or SEO markup there.

React

React 18 and 19 render <mapvx-website> as a plain DOM element. Three rules apply:

  1. Pass kebab-case attributes with string values: default-to-map="true", never default-to-map={true}. React 19 renders {true} as an empty attribute, which the element reads as false, and drops {false}, so the default applies. React 18 turns booleans into strings, but string values work in both versions.
  2. Do not use camelCase props such as apiKey or defaultToMap. React 19 sets them as properties only when the element is already defined at render time; otherwise they end up as lowercase attributes that the element ignores.
  3. Listen to events through a ref with addEventListener. React 18 has no event props for custom elements. React 19 does, but the prop must be on followed by the exact event name, onuserAction; onUserAction never fires.

In a client-rendered app, import the package once in the entry file:

// main.tsx (for example, a Vite app) import '@mapvx/web-components-ssr' import '@mapvx/web-components-ssr/dist/browser/styles.css'
// MapvxWebsite.tsx import { useEffect, useRef } from 'react' import type { UserAction } from '@mapvx/web-components-ssr' export function MapvxWebsite() { const ref = useRef<HTMLElement>(null) useEffect(() => { const el = ref.current if (!el) return const onUserAction = (event: Event) => { const action = (event as CustomEvent<UserAction>).detail console.log(action.type, action.data) } el.addEventListener('userAction', onUserAction) return () => el.removeEventListener('userAction', onUserAction) }, []) return ( <mapvx-website ref={ref} api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" default-to-map="true" show-city-filter="false" /> ) }

With React 19 only, the event prop is an alternative to the ref:

<mapvx-website api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" onuserAction={(event: CustomEvent<UserAction>) => console.log(event.detail)} />

With server rendering, load the bundle as shown in Next.js.

TypeScript

The package ships no JSX typing for the element, so TSX reports mapvx-website as an unknown element. Add a declaration file that the include of your tsconfig.json covers. Do not give it the same name as a .ts or .tsx file in the same folder (for example mapvx-website.d.ts next to the mapvx-website.tsx component from Next.js): TypeScript then skips the .d.ts file without any warning (Typing the element). This one was type-checked with @types/react 19. Typing the booleans as the strings 'true' and 'false' also catches the React 19 boolean pitfall at compile time.

// types/mapvx-website-react.d.ts import type { DetailedHTMLProps, HTMLAttributes } from 'react' import type { UserAction } from '@mapvx/web-components-ssr' type BoolAttr = 'true' | 'false' // React 19 turns true into "" and drops false: pass strings export interface MapvxWebsiteAttributes extends DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> { 'api-key'?: string 'institution-id'?: string 'initial-data'?: string 'default-to-map'?: BoolAttr 'show-category-filters'?: BoolAttr 'show-city-filter'?: BoolAttr 'inherit-font-family'?: BoolAttr 'hide-deals'?: BoolAttr 'show-deal-labels'?: BoolAttr 'card-width'?: number | string 'rows-per-page'?: number | string /** React 19+: listens to the case-sensitive 'userAction' event */ onuserAction?: (event: CustomEvent<UserAction>) => void /** React 19+: listens to the 'cardSelected' event */ oncardSelected?: (event: CustomEvent<string>) => void } declare module 'react' { namespace JSX { interface IntrinsicElements { 'mapvx-website': MapvxWebsiteAttributes } } }

For the element’s JavaScript properties (HTMLElementTagNameMap), see Typing the element.

Vue

Vue 3 must know that mapvx-website is a custom element. Without compilerOptions.isCustomElement, Vue warns Failed to resolve component: mapvx-website, and @userAction listeners in single-file components never fire.

// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'mapvx-website', }, }, }), ], })
// main.ts import { createApp } from 'vue' import '@mapvx/web-components-ssr' import '@mapvx/web-components-ssr/dist/browser/styles.css' import App from './App.vue' createApp(App).mount('#app')
<script setup lang="ts"> import type { UserAction } from '@mapvx/web-components-ssr' function onUserAction(event: Event) { const action = (event as CustomEvent<UserAction>).detail console.log(action.type, action.data) } </script> <template> <mapvx-website api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" default-to-map="true" show-city-filter="false" @userAction="onUserAction" /> </template>
  • Static string attributes work, and so do bindings: :default-to-map="true" sets the attribute to "true", and :show-city-filter="false" sets it to "false".
  • Once isCustomElement is set, @userAction and @cardSelected keep the event name’s case.
  • This setup was verified with Vue 3.5. In other build setups, set the same option wherever the Vue template compiler is configured; see Using custom elements in Vue .
  • For Nuxt, see SSR and initial data.

Angular

Copy the published files with an assets entry in angular.json and load them from index.html. This keeps the files exactly as published and, in Angular SSR apps, keeps main.js out of the server bundle. Importing the package from TypeScript through the Angular CLI has not been verified. The element carries its own Angular and runs next to your application’s; this setup was tested with an Angular 20 host app that uses zone.js.

Copy the files as assets

Add this entry to the assets array of your build target in angular.json, next to the entries you already have:

{ "glob": "**/*", "input": "node_modules/@mapvx/web-components-ssr/dist/browser", "output": "mapvx-website/1.6.1" }

Load them in index.html

<!-- src/index.html, inside <head>. On upgrade, change the version here and in angular.json. --> <link rel="stylesheet" href="mapvx-website/1.6.1/styles.css" /> <script type="module" src="mapvx-website/1.6.1/main.js"></script>

Use the element in a component

import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core' import type { UserAction } from '@mapvx/web-components-ssr' @Component({ selector: 'app-mapvx', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <mapvx-website [attr.api-key]="apiKey" [attr.institution-id]="institutionId()" [attr.default-to-map]="defaultToMap()" [attr.show-city-filter]="false" (userAction)="onUserAction($event)" ></mapvx-website> `, }) export class MapvxComponent { readonly apiKey = 'YOUR_API_KEY' readonly institutionId = signal('YOUR_INSTITUTION_ID') readonly defaultToMap = signal(true) onUserAction(event: Event) { const action = (event as CustomEvent<UserAction>).detail console.log(action.type, action.data) } }
  • schemas: [CUSTOM_ELEMENTS_SCHEMA] is required on every component whose template uses the element. Without it, the build fails with 'mapvx-website' is not a known element.
  • Use [attr.*] bindings. Angular writes booleans as "true" and "false", which the element reads correctly.
  • Never bind an attribute to null or undefined: Angular then removes it, which turns show-category-filters and show-city-filter off instead of restoring their defaults, and throws an error for institution-id. See Removing attributes.
  • The (userAction) template binding listens on the element itself, with the exact event name.
  • If the browser receives HTML instead of main.js (Unexpected token '<' or a MIME type error), the assets entry or the path in index.html is wrong. See Unexpected token '<'.
  • For server-rendered Angular apps, see Angular SSR.

Next.js

With the App Router, serve the published files from public, load main.js with next/script, and render the element from a client component.

Copy the files into public

Use the copy script from Self-hosting. It writes public/mapvx-website/1.6.1/, which Next.js serves as /mapvx-website/1.6.1/, with every file name unchanged.

Import the stylesheet

// app/layout.tsx import '@mapvx/web-components-ssr/dist/browser/styles.css'

Create a client component

// app/directory/mapvx-website.tsx 'use client' import Script from 'next/script' import { useEffect, useRef } from 'react' import type { UserAction } from '@mapvx/web-components-ssr' export function MapvxWebsite() { const ref = useRef<HTMLElement>(null) useEffect(() => { const el = ref.current if (!el) return const onUserAction = (event: Event) => { const action = (event as CustomEvent<UserAction>).detail console.log(action.type, action.data) } el.addEventListener('userAction', onUserAction) return () => el.removeEventListener('userAction', onUserAction) }, []) return ( <> <Script type="module" src="/mapvx-website/1.6.1/main.js" /> <mapvx-website ref={ref} api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" default-to-map="true" /> </> ) }

Render it in a page

// app/directory/page.tsx import { MapvxWebsite } from './mapvx-website' export default function Page() { return <MapvxWebsite /> }
  • Load main.js only through next/script. Never import '@mapvx/web-components-ssr' at the top of a component file: Next.js renders client components on the server too, and the import throws there.
  • Keep the default strategy (afterInteractive), which injects the script from the client after hydration. If the element upgrades before hydration, React 19 reports Hydration failed because the server rendered HTML didn't match the client and re-creates the tree on the client.
  • Keep the file names. Some older examples copy main.js as bundle.js, which breaks the chunks’ imports of ./main.js when they load; see Self-hosting.
  • The same rules as in React apply to attributes and events. For TypeScript, add the declaration file from TypeScript.
  • To pass data prepared on the server through initial-data, see Next.js in SSR and initial data.

Other frameworks

  • Nuxt, SvelteKit and Angular SSR: see SSR and initial data, with recipes for Nuxt, SvelteKit and Angular SSR. The rule for any SSR framework is to load main.js only in the browser (Client-only loading).
  • Other client-rendered frameworks: import the package through your bundler, or add the CDN or self-hosted tags to your HTML. Pass kebab-case attributes with string values, and attach listeners to the element itself, because its events do not bubble (Listening to events).
  • Server-rendered HTML without a JavaScript framework (PHP, Rails, Django, CMS themes): the CDN or self-hosted snippet works as is.

Whatever the setup, use a single <mapvx-website> per page: several elements share one application and interfere with each other. See Multiple instances.

Last updated on