Astro JS Location Map: using Leaflet & Svelte
Tags: webdev, astro, javascript, svelte
Published: 2022-08-17 09:50:30

---
title: "Astro JS Location Map: using Leaflet & Svelte"
published: "true"
description: "Astro JS Location Map: how you can use the Leaflet library to add a location map on your Astro site with Svelte. Handy for a contact page."
tags: "webdev, astro, javascript, svelte"
canonical_url: "https://rodneylab.com/astro-js-location-map/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cyc2xfu6pk16gt9utwcb.png"
---

## 🍃 Leaflet Map Library

In this Astro JS location map post, we see how you can use the Leaflet library together with SvelteKit to add interactive location maps to your Astro sites.  **Astro builds fast** static sites making it a great choice for brochure sites.  These are where you have a basic site with home page, about, some product details and a contact page.  Using the code in this tutorial you can improve the **user experience (UX)** on the map you might add to a contact page.  This will let visitors, **zoom** in and out as well as **pan** to work out directions or find nearest public transport connections.

**Leaflet** is library for adding interactive maps.  Basically with just latitude and longitude coordinates, you can add a custom map for any location with Leaflet.  The maps are **responsive** and let you pinch and zoom on mobile as well as pan to see the vicinity on the map.  Leaflet can work independently of a framework, though we will use **Svelte** here.  This will make it easier to control how the map loads using Astro later.  If that all sounds exciting, then why don&rsquo;t we make a start?

## 🧱 Astro JS Location Map: What we're Building

We will build a basic map, like one you might find on a contact page.  The maps we use wil be sourced from <a aria-label="Open the Open Street Map site" href="https://www.openstreetmap.org/">OpenStreetMap</a>.  As well as that, we use the <a aria-label="Open the Map box home page" href="https://www.mapbox.com/">Mapbox service</a> to provide tiles.  These are smaller units of the map focussed on the area of interest.  Fortunately we do not have to sweat the details of finding the right tile for our map as Mapbox and Leaflet couple well.

The map will show a location, with a marker.  On top we will add some custom text to the marker.  This will appear when the user hovers over it.

![Astro JS Location Map: screenshot of finished project with map zoomed in on Hyde Park in London.  Zoom controls are visible](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dr20akvwlxi7153hpven.png)

## ⚙️ Getting Started: Astro JS Location Map

Let&rsquo;s start by spinning up a fresh project.  From the Terminal type the following commands:

```shell
pnpm create astro@latest astro-js-location-map
cd astro-js-location-map
pnpm astro telemetry disable
pnpm astro add svelte
pnpm add --save-peer typescript
pnpm run dev
```

When prompted choose  *Empty project*, *Yes* to install pnpm dependencies.  I opted for *Strict (recommended)*TypeScript, so just delete any type annotations (in the code blocks below) if you prefer pure JavaScript. For the `pnpm add astro add svelte` command, accept the config changes proposed, and also to install the Svelte package. Apart from those options, chose what makes most sense for you.  

We need to add a couple more packages to get up and running with Leaflet:

```shell
pnpm add -D @types/leaflet leaflet
```

That&rsquo;s all the setup.  The Terminal output should give you a URL for the dev server (something like: `http://localhost:3000/`).  Copy that into your browser and you will see the skeleton template page.

## 🗺 Map Component

Ok, we&rsquo;ll start by creating a Map component in Svelte.  Create a `src/components` folder and in there, add a `Map.svelte` file with the following content:

```svelte
<script lang="ts">
  import { setMap } from '../shared/actions/map';

  export let location: {
    latitude: number;
    longitude: number;
  };
  export let zoom: number = 19;
  export let markerMarkup: string = '';

  const { latitude, longitude } = location;
</script>

<figure use:setMap={{ latitude, longitude, zoom, markerMarkup }} />

<style>
  figure {
    width: 38rem;
    height: 21rem;
    box-shadow: var(--shadow-elevation-low);
  }
</style>
```

This will essentially be a shell for the Map component, rendering it to a `figure` HTML element.  We will add the Leaflet logic in a **Svelte action** (coming soon).  In fact the code in line `2` imports that action for us.  Note the script tag in line `1` includes the attribute `lang="ts"`.  Omit the attribute if you want to use plain JavaScript.  Svelte components use the `export let` syntax to declare inputs.  Here we expect a location as an object composed on latitude and longitude fields and an optional zoom factor.  This is optional because we specify a default value of `19`.  Finally, we import some HTML markup (`markerMarkup`), which the action place on the marker label for us.

Actions are a way to access the component lifecycle.  We see more on Svelte actions in the tutorial on <a aria-label="Open tutorial on creating a Svelte video blog" href="https://plus.rodneylab.com/tutorials/svelte-video-blog">creating a Svelte video blog</a>.  You will notice a `use:setMap` attribute on the `figure` tag in line `14`.  This is exactly where we use the action.  Let&rsquo;s define it next.

## 🎬 Svelte, Map, Action!
We will now add the code for the action.  This is where we link in Leaflet, Mapbox and OpenStreetMap.  Create a `src/shared/actions` directory and in there add a `map.ts` (or `map.js`) file with this content:

```typescript
export function setMap(
  mapElement: HTMLElement,
  {
    latitude,
    longitude,
    zoom,
    markerMarkup = '',
  }: { latitude: number; longitude: number; zoom: number; markerMarkup?: string },
) {
  (async () => {
    const {
      icon: leafletIcon,
      map: leafletMap,
      marker: leafletMarker,
      tileLayer,
    } = await import('leaflet');

    const map = leafletMap(mapElement).setView([latitude, longitude], zoom);
    tileLayer(
      'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}',
      {
        attribution:
          'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
        maxZoom: 19,
        id: 'mapbox/streets-v11',
        tileSize: 512,
        zoomOffset: -1,
        accessToken: import.meta.env.PUBLIC_MAPBOX_ACCESS_TOKEN,
        detectRetina: true,
      },
    ).addTo(map);

    if (markerMarkup !== '') {
      leafletMarker([latitude, longitude]).bindPopup(markerMarkup).addTo(map);
    } else {
      leafletMarker([latitude, longitude]).addTo(map);
    }
  })();
}
```

This is mostly just boilerplate code to link in the Leaflet package.  You will see the function we define has two arguments while we only used one in the `Map.svelte` file.  That is because Svelte automatically sets the first argument to the node or element itself.  Notice we are able to pass in multiple parameter values, using an object for the second argument.

We use a dynamic import for the leaflet objects (lines `11`&ndash;`16`).  Typically this is not necessary.  We do it here specifically because importing the leaflet library causes Vite to evaluate the `window` object.  Although this is fine and dandy when code runs in the browser.  Before that, Vite runs the code in Server Side Render (SSR) mode and on the server, that `window` object is undefined.  As an aside, because we are using an action (which is only called on a mounted element), we do not need a check within the function body that the code is running in the browser (and not on the server).

In line `28`, we reference a Mapbox API key.  We&rsquo;ll set that up next.

## 🔑 Mapbox API Config

You will need a Mapbox account for this part.  On the free tier, you can have <a aria-label="Open Map box pricing page" href="https://www.mapbox.com/pricing#maps">up to 50,000 montly loads</a>, which should be plenty enough to set up a test site.  To get an <a aria-label="Open the Map box sign up form" href="https://account.mapbox.com/auth/signup/">API key, fill out the signup form</a>.

![Astro JS Location Map: Map box sign up form.  Form has fields for Username, Email, Password, First name, Last name and optionally company.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ub1eefj1m8dtpa188x5.png)

Once you have set up your account and logged in, select **Tokens** from the links at the top of the page then click **+ Create a token**.  You can call it whatever you like, you only need the actual public key value for this project.  Copy this down or save it to a safe place.

![Astro JS Location Map: Map box dashboard.  Title reads Access tokens.  A Create a token button is show.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8jn6x4omzsbrfnej4g6e.png)

Finally for this section create a `.env` file in the root folder of your project and paste in the content below (adding your new API key instead of the placeholder):

```plaintext
PUBLIC_MAPBOX_ACCESS_TOKEN="PLACEHOLDER-FOR-YOUR-OWN-KEY"
```

If you are using TypeScript, you might also want to edit `src/.env.d.ts` in the project:

```typescript
/// <reference types="astro/client" />

interface ImportMetaEnv {
  readonly PUBLIC_MAPBOX_ACCESS_TOKEN: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

That&rsquo;s all set up now.  We should be able to read the key in line `28` of the action file just fine now!

## 📍 Home Page

To see this map, we just need to replace the content in our `src/pages/index.astro`, add some styling and a touch more config.  We&rsquo;re on the home straight now.  Replace the `src/pages/index.astro` content with this:

```astro
---
import Map from '$components/Map.svelte';
import '$styles/fonts.css';
import '$styles/global.css';
import 'leaflet/dist/leaflet.css';

const location = { latitude: 51.51089, longitude: -0.17563 };
---

<html lang="en">

<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width" />
	<title>Astro Location Map with Leaflet</title>
</head>

<body>
	<main class="container">
		<h1>🚀 Astro Location Map with Leaflet</h1>
		<section class="wrapper">
			<h2>We are here:</h2>
			<Map client:load {location} zoom={14} markerMarkup="<p>We are here!</p>" />
		</section>
	</main>
</body>

</html>

<style>
	.container {
		background-color: var(--colour-light);
		border-radius: var(--spacing-1);
		padding: var(--spacing-8);
		width: min(100% - var(--spacing-16), var(--max-width-wrapper));
		margin: var(--spacing-16) auto;
	}

	.container h1 {
		padding: var(--spacing-0) var(--spacing-8);
	}

	.wrapper {
		display: grid;
		place-items: center;
		background-color: var(--colour-brand);
		border: var(--spacing-px) solid var(--colour-alt);
		border-radius: var(--spacing-px);
		padding: var(--spacing-8) auto;
		margin: var(--spacing-8);
	}
</style>
```

This will not work yet, but let&rsquo;s see what we have so far.  In line `23`, we use the Map component which we defined earlier.  Most important in this line is the `client:load` directive.  Astro ships zero JavaScript by default and without `client:load`, the map would not load.  If the map was going to appear far down the page, you might opt for `client:visible` instead. In that case, Astro will instruct the browser only to load it when the user is about to scroll it into view.

The map coordinates are set in line `7`.  Feel free to change these to a location near you.  To get coordinates, go to <a aria-label="Open the open Street map page" href="https://www.openstreetmap.org/">OpenStreetMap</a> and search for your preferred location.  Then right click on the map (above your preferred location) and choose **Show Address**.  You should now see the latitude and longitude in the left pane.

## 💅🏽 Finishing Off: Astro JS Location Map

### `$components`

In line `2` above, you see we use a `$` alias for the components folder instead of `../components`.  Use the latter if you prefer, but to make the alias work just edit `tsconfig.json` in the project root folder:

```json
{
  "compilerOptions": {
    // Enable top-level await, and other modern ESM features.
    "target": "ESNext",
    "module": "ESNext",
    // Enable node-style module resolution, for things like npm package imports.
    "moduleResolution": "node",
    // Enable JSON imports.
    "resolveJsonModule": true,
    // Enable stricter transpilation for better output.
    "isolatedModules": true,
    // Astro will directly run your TypeScript code, no transpilation needed.
    "noEmit": true,
    // Enable strict type checking.
    "strict": true,
    // Error when a value import is only used as a type.
    "importsNotUsedAsValues": "error",
    // Report errors for fallthrough cases in switch statements
    "noFallthroughCasesInSwitch": true,
    // Force functions designed to override their parent class to be specified as `override`.
    "noImplicitOverride": true,
    // Force functions to specify that they can return `undefined` if a possibe code path does not return a value.
    "noImplicitReturns": true,
    // Report an error when a variable is declared but never used.
    "noUnusedLocals": true,
    // Report an error when a parameter is declared but never used.
    "noUnusedParameters": true,
    // Force the usage of the indexed syntax to access fields declared using an index signature.
    "noUncheckedIndexedAccess": true,
    // Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value.
    "exactOptionalPropertyTypes": true,
    "baseUrl": ".",
    "paths": {
      "$components/*": ["src/components/*"],
      "$styles/*": ["src/styles/*"]
    }
  }
}
```

### Astro JS Location Map: Styling

We are using the new Google font Roboto Serif.  Create a `src/styles` folder then add fontface CSS in `fonts.css`:

```css
/* roboto-serif-regular - latin */
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: normal;
  src: local(''), url('/fonts/roboto-serif-v7-latin-regular.woff2') format('woff2'),
    /* Chrome 26+, Opera 23+, Firefox 39+ */ url('/fonts/roboto-serif-v7-latin-regular.woff')
      format('woff');
  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

/* roboto-serif-700 - latin */
@font-face {
  font-family: 'Roboto Serif';
  font-style: normal;
  font-weight: bold;
  src: local(''), url('/fonts/roboto-serif-v7-latin-700.woff2') format('woff2'),
    /* Chrome 26+, Opera 23+, Firefox 39+ */ url('/fonts/roboto-serif-v7-latin-700.woff')
      format('woff');
  /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
```

We link to fonts in `/fonts` here.  This is a location which will be available on the built site.  We make a `public/fonts` folder in our project for this to work.  <a aria-label="Download fonts for slef hosting" href="https://google-webfonts-helper.herokuapp.com/api/fonts/roboto-serif?download=zip&subsets=latin&variants=700,regular&formats=woff,woff2">Download these fonts from google-webfonts-helper</a>.  Unzip the downloaded file and copy the `.woff` and `.woff2` files to the new `public/fonts` folder.  We take a more <a aria-label="Open video on self hosting fonts in Astro" href="https://rodneylab.com/astro-self-hosted-fonts/">detailed look at self-hosting fonts with Astro</a> in a separate video post.

Finally create the global CSS stylesheet file (`src/styles/global.css`) and paste in this content:

```css
:root {
  --colour-dark: hsl(309 7% 17%);
  --colour-theme: hsl(347 64% 49%);
  --colour-brand: hsl(197 94% 58%);
  --colour-alt: hsl(233 89% 60%);
  --colour-light: hsl(1 63% 82%);

  --shadow-color: 203deg 64% 40%;
  --shadow-elevation-low: -1px 1px 1.6px hsl(var(--shadow-color) / 0.34),
    -1.7px 1.7px 2.7px -1.2px hsl(var(--shadow-color) / 0.34),
    -4px 4px 6.4px -2.5px hsl(var(--shadow-color) / 0.34);

  --spacing-px: 1px;
  --spacing-0: 0;
  --spacing-1: 0.25rem;
  --spacing-4: 1rem;
  --spacing-8: 2rem;
  --spacing-16: 4rem;
  --spacing-12: 3rem;

  --max-width-wrapper: 48rem;
}

html {
  font-family: 'Roboto Serif';
  color: var(--colour-dark);
  background-color: var(--colour-theme);
}
```

In line `4` of `src/pages/index.astro`, we include the Leaflet CSS stylesheet so this will get bundled.

The site should all be working now. Finally, you can take a look in your browser!

## 👷🏽 Build

Notice the blue marker shows up at the centre of the map.  Let&rsquo;s build the site now:

```shell
pnpm build
pnpm preview
```

The page is small so it will literally take a few seconds to build.  If you go back to your browser now, you will notice the marker has disappeared, you just see an outline.  Try inspecting the outline. 

![Astro JS Location Map: Dev tools. Screenshot of page with dev tools open and Inspector highlighting a missing marker.  Only an outline is visible for the marker.  In the Inspector tab, the img tag for the marker has marker-icon-2x.png as the src attribute, though a tooltip pointing here reads Could not load the image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwjf189lmfktcl7tr5b5.png)

Basically Vite did not know we needed it to bundle the PNG file for the marker.  Do not worry though, we can fix this.  The way to ensure Vite bundles something is to import it and use it in the code.  We will import the PNG files from the Leaflet package.  Then we will use a Leaflet interface for defining  custom icons.  Instead of defining a custom marker icon though, we will link to the default ones we import.  Here we go &mdash; update `src/shared/actions/map.ts`:

```typescript
"src.shared/actions/map.ts"
import markerIconRetinaURL from 'leaflet/dist/images/marker-icon-2x.png';
import markerIconURL from 'leaflet/dist/images/marker-icon.png';
import markerShadowURL from 'leaflet/dist/images/marker-shadow.png';

export function setMap(
  mapElement: HTMLElement,
  {
    latitude,
    longitude,
    zoom,
    markerMarkup = '',
  }: { latitude: number; longitude: number; zoom: number; markerMarkup?: string },
) {
  (async () => {
    const {
      icon: leafletIcon,
      map: leafletMap,
      marker: leafletMarker,
      tileLayer,
    } = await import('leaflet');

    const markerIcon = leafletIcon({
      iconSize: [25, 41],
      iconAnchor: [10, 41],
      popupAnchor: [2, -40],
      iconUrl: markerIconURL,
      iconRetinaUrl: markerIconRetinaURL,
      shadowUrl: markerShadowURL,
    });

    const map = leafletMap(mapElement).setView([latitude, longitude], zoom);
    tileLayer(
      'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}',
      {
        attribution:
          'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
        maxZoom: 19,
        id: 'mapbox/streets-v11',
        tileSize: 512,
        zoomOffset: -1,
        accessToken: import.meta.env.PUBLIC_MAPBOX_ACCESS_TOKEN,
        detectRetina: true,
      },
    ).addTo(map);

    if (markerMarkup !== '') {
      leafletMarker([latitude, longitude], { icon: markerIcon }).bindPopup(markerMarkup).addTo(map);
    } else {
      leafletMarker([latitude, longitude], { icon: markerIcon }).addTo(map);
    }
  })();
}
```

You can read more about <a href="https://leafletjs.com/examples/custom-icons/">using custom icons with Leaflet in the docs</a>.  For now, double check the code still works by running `pnpm dev`.  If all is well, rebuild.  You should see the markers in all their glory now!


## 🙌🏽 Astro JS Location Map: Wrapping Up

We have taken a look at adding a Astro JS Location maps in this post.  In particular, we saw:

- how to **use Svelte action with Astro**,
- a trick for making sure **Vite bundles assets** at build time,
- how to set up **Leaflet with Mapbox and OpenStreetMap**.

Leaflet is highly customisable and I recommend checking their docs to explore other possibilities.  As a challenge, you might consider adding extra pins to the map, for example show the nearest public transport terminals.  Take a look at the full project code on the <a aria-label="Open the Astro J S Location Git Hub repo" href="https://github.com/rodneylab/astro/tree/main/demos/astro-js-location-map">Rodney Lab GitHub page</a>.  I hope you found this article useful and am keen to hear how you will the starter on your own projects as well as possible improvements.

## 🙏🏽 Astro JS Location Map: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="Reach out on Twitter" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on Astro" href="https://rodneylab.com/tags/astro/">Astro</a> as well as <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a>. Also <a aria-label="Subscribe to the Rodney Lab newsletter" href="https://rodneylab.com/about/#newsletter">subscribe to the newsletter to keep up-to-date</a> with our latest projects.