https://github.com/gvellut/mapvibe
Google My Maps replacement for my blog
https://github.com/gvellut/mapvibe
maplibre mapping vibecoded
Last synced: 3 months ago
JSON representation
Google My Maps replacement for my blog
- Host: GitHub
- URL: https://github.com/gvellut/mapvibe
- Owner: gvellut
- License: mit
- Created: 2025-07-21T18:34:01.000Z (12 months ago)
- Default Branch: master
- Last Pushed: 2026-03-14T11:18:59.000Z (4 months ago)
- Last Synced: 2026-03-14T22:17:09.820Z (4 months ago)
- Topics: maplibre, mapping, vibecoded
- Language: TypeScript
- Homepage:
- Size: 1.14 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# MapVibe
A static map interface for embedding in blog posts or websites, as a Google My Maps replacement. Runs fully client-side, loads configuration from a single JSON file.
Made with [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) and React in TypeScript.
## Configuration
- Uses the [MapLibre Style Spec](https://maplibre.org/maplibre-style-spec/) for sources, layers, and styling.
- The config file must include a `customUi` object:
- `customUi.imports`: Optional list of imported style fragments using Mapbox-style `{ id, url }` entries. Imported styles are fetched asynchronously and inserted below non-background top-style overlay/data layers. It is similar to the *Imports* in the Mapbox SDK v2 (after the Maplibre fork).
- `customUi.backgroundLayers`: List of selectable backgrounds shown in the layer chooser.
- Each entry uses its own `id` / `name` plus a required `layerIds` array.
- `backgroundLayers[].id` is a `customUi` id, separate from top-style layer ids and import ids.
- `backgroundLayers[].layerIds` references top-style layer ids and/or import ids in bottom-to-top order.
- Set `visible: true` on one entry to choose the initial GUI selection. If multiple entries set it, MapVibe warns and uses the first. If none set it, MapVibe selects the first background entry.
- Background entries default to not selected unless `visible: true` is set.
- `customUi.dataLayers`: List of data layers (lines, points, polygons) for toggling visibility.
- `dataLayers[].layerIds` references top-style layer ids.
- Data layers default to visible. Set `visible: false` to start hidden.
- Set `interactive: true` to make a data layer clickable.
- Add `clusterInteractive: true` on an interactive clustered data layer to make generated cluster features zoom to their expansion level instead of opening the info panel. When omitted or `false`, cluster clicks are ignored and only non-cluster features remain actionable.
- Add `openUrl: true` on an interactive data layer to open each clicked feature's `url` property in a new tab instead of showing the info panel.
- `customUi.panel`: Panel color and width.
- Optional `imageSizeIsMax: true` uses each feature's `imageSize` as the maximum image box size in the info panel. Default is `false`, which keeps the current full-width behavior.
- Info panel features can also provide `imageBackgroundColor` as a hex color string such as `#000000` to fill the image wrapper behind the image.
- `customUi.controls`: Which UI controls to show (zoom, scale, layer chooser, fullscreen, attribution).
- `customUi.globalMinZoom` / `globalMaxZoom`: Clamp zoom range for all backgrounds.
- On startup, MapVibe hides all top-style layers first, then applies visibility from `customUi.backgroundLayers` and `customUi.dataLayers`. Raw top-style `layout.visibility` does not control initial visibility.
- The layer chooser still uses a single selected background at a time. Selecting a background entry hides the other managed backgrounds and restores that entry's top-style/import members as one virtual background.
### Notes
In `"customUi" > "panel"`, to recenter marker when it would be covered by info panel, add:
```js
"marginRecenterOnOpen": 10,
"recenterOnOpen": true
```
To enable cooperative gestures (`ctrl + scroll` to zoom on desktop + 2 finger pan on mobile) in the standalone `/mapvibe` app, add `mgc=y` to the URL. `mgc=0`, `mgc=n`, or `mgc=no` disables it. That parameter is forced to `mgc=n` when opening the map in a new tab from the fullscreen button.
To remember the last map position, use `rlp=page` or `rlp=domain` in the standalone `/mapvibe` URL:
- `rlp=page` remembers pan/zoom per host + path
- `rlp=domain` remembers pan/zoom per host across paths
- `rlp=1` is the same as `rlp=page`
- `rlp=0` disables the feature and ignores any saved position
To override the fullscreen button in the standalone `/mapvibe` app, use `fs=y` to force-enable it or `fs=0`, `fs=n`, or `fs=no` to force-disable it. When `fs` is absent, the app defers to `customUi.controls.fullscreen` in the config JSON. The fullscreen button opens the new tab with `fs=no` so that view does not show another fullscreen button.
When enabled, the remembered position takes precedence over `center`, `zoom`, `bounds`, and auto-fit-to-data on reload.
When an imported background is selected, MapVibe may switch the map `glyphs` URL to the imported style's `glyphs` URL. For background entries with multiple imported styles declaring different `glyphs` URLs, MapVibe warns and uses the last imported member in the entry's order.
## Usage
- Host the `dist` output (see the doc on [Build](#build)) in folder `/mapvibe`.
- You will have to create a config JSON file and host it on the same server (see the `samples` folder for examples for `config.json` files).
- Your config JSON file can refer to icons other than the included ones: Host them on your server.
- The config can refer to your own GeoJSON data layers: Also host them on your server.
- Embed in your page with ``.
### Deployment in Hugo
The `dist` output can be added under `static/mapvibe` in the Hugo folder, for later deployment on your server.
Create a new `mapvibe` shortcode in `layouts/shortcodes/mapvibe.html`. For example:
```html
```
This assumes the post is a page bundle with the `config.json` hosted inside (so `RelPermalink` is actually a folder in Hugo).
The map can be embedded in a Hugo content using:
```html
{{< mapvibe "640" "480" >}}
```
### Example
Hugo blog:
https://blog.vellut.com/2025/07/hike-to-pointe-noire-de-pormenaz/ (scroll a little to see the map)
`MapVibe` was meant to replace something like:
https://blog.vellut.com/2025/06/hike-to-pointe-des-aravis-aiguille-de-borderan/ (Google My Maps)
## Development
```bash
npm install
npm run dev
```
Some simple `config.json` samples can be found in folder `samples`. To load one of them, use something like
`http://localhost:5173/mapvibe/?config=samples/sample1/config.json`
as the URL for testing. For imports and multi-layer backgrounds, file `samples/sample4/config.json` has a sample of use. For clustered interactive points with click-to-zoom clusters, file `samples/sample5/config.json` shows `clusterInteractive: true`.
Or, since the project will be used inside an iframe (with limited width and height), use :
`http://localhost:5173/mapvibe/iframe.html?config=samples/sample2/config.json`
## Build
### Building the Website
```bash
npm run build
```
This will output static files to `dist` for hosting as a standalone website.
Before building:
- The hosting path can be customized in `vite.config.ts` (the CSS will load some assets using that path so it should correspond to where it will be hosted).
- The favicon can also be changed.
- New icons can be added in `public/assets/markers` (but they can be loaded from any place so not really needed except convenience).
### Building the Library
To build MapVibe as an npm library:
```bash
npm run build:lib
```
This will generate:
- `dist/mapvibe.mjs` - ES module build
- `dist/mapvibe.cjs` - CommonJS build
- `dist/mapvibe.css` - Compiled CSS styles
- `dist/lib.d.ts` - TypeScript declarations for autocomplete and type checking
## Publishing to NPM
To publish the library to npm:
1. Update the version in `package.json`
2. Ensure you're logged in to npm: `npm login`
3. Run: `npm publish`
The `prepublishOnly` script will automatically run `npm run build:lib` before publishing.
## Using MapVibe as a Library
### Installation
```bash
npm install mapvibe
```
### Peer Dependencies
MapVibe requires the following peer dependencies:
- `react` (^18.0.0 || ^19.0.0)
- `react-dom` (^18.0.0 || ^19.0.0)
- `maplibre-gl` (^4.0.0 || ^5.0.0)
Make sure to install them if not already present:
```bash
npm install react react-dom maplibre-gl
```
### Basic Usage
```tsx
import { MapVibeMap, type AppConfig } from 'mapvibe';
import 'mapvibe/style.css';
function App({ config }: { config: AppConfig }) {
return ;
}
```
`MapVibeMap` is the embeddable component for host applications. If you want the standalone app that reads a `config` URL parameter, use the built website output described earlier in this README.
`rememberLastPosition` accepts `false | 0 | true | 1 | "page" | "domain"`.
- `false` / `0`: disabled, never load saved pan/zoom even if one exists
- `true` / `1` / `"page"`: remember pan/zoom per host + path
- `"domain"`: remember pan/zoom per host across paths
When a remembered position exists, it overrides `config.center`, `config.zoom`, `config.bounds`, and the automatic fit-to-sources fallback.
### Accessing the MapLibre Instance
`MapVibeMap` exposes the underlying `maplibregl.Map` instance through a React ref so the embedding app can wire custom globals such as `window.goto`.
The handle also exposes:
- `getLayerIdsForBackgroundLayer(id)` to resolve a `customUi.backgroundLayers[].id` to the concrete runtime layer ids currently associated with it
- `getImportInfo(id)` to inspect a loaded import's original URL and namespaced layer/source/sprite ids
```tsx
import { createRef } from 'react';
import { createRoot } from 'react-dom/client';
import { MapVibeMap, type AppConfig, type MapVibeMapHandle } from 'mapvibe';
import 'mapvibe/style.css';
const mapRef = createRef();
const root = createRoot(document.getElementById('map')!);
declare global {
interface Window {
goto?: (lat: number, lon: number) => void;
}
}
window.goto = (lat: number, lon: number) => {
mapRef.current?.getMap()?.flyTo({ center: [lon, lat] });
};
root.render(
);
```
### Importing CSS and Assets
When impoorting the library published on NPM (TBD):
**CSS**: The library exports a compiled CSS file that must be imported in your application:
```tsx
import 'mapvibe/style.css';
```
Alternatively, you can import it in your main CSS file:
```css
@import 'mapvibe/style.css';
```
**Icons and Assets**: The library includes UI icons (layers, close, fullscreen) in the compiled CSS (by default Vite inline the referenced icons smaller than 4kB: These are embedded as data URIs). When using the library:
1. If you're using custom marker icons, host them on your server and reference them in your config.json
2. The default UI icons (layer chooser, close button, fullscreen) are bundled with the CSS
### TypeScript Support
MapVibe is written in TypeScript and includes full type definitions. When using the library in a TypeScript project, you'll get:
- Full autocomplete for the `MapVibeMap` component
- Type definitions for configuration interfaces:
- `AppConfig`
- `BackgroundLayerConfig`
- `StyleImportConfig`
- `DataLayerConfig`
- `CustomUiConfig`
- `InfoPanelData`
- `MapVibeImportInfo`
- `MapVibeMapHandle`
Example with types:
```tsx
import { MapVibeMap, AppConfig } from 'mapvibe';
import 'mapvibe/style.css';
// TypeScript will provide autocomplete for config structure
const config: AppConfig = {
title: "My Map",
center: [6.8665, 45.9237],
zoom: 12,
// ... rest of config with full type checking
};
```
For `openUrl` layers, each clicked feature is expected to expose a string `url` property in its GeoJSON `properties`. If `url` is missing, MapVibe logs a warning in the console and does not open a popup.
For clustered GeoJSON sources, generated cluster features should be grouped into the same `dataLayers[].layerIds` entry as the leaf layer if they share one visibility toggle. `clusterInteractive: true` changes only click behavior for those generated cluster features; leaf features in the same data layer still use `openUrl` or the info panel.
## Notes
### Upgrade dev deps
`npx npm-check-updates -u --dep dev`
## License
MIT