Hands On

Integrating GeoJSON in Your Static Sites

By Raymond Camden | 04 November 2019

Honestly I worry that readers may be a bit bored with how much I'm talking about GeoJSON (be sure to see my introduction to the topic and then my follow up). I mean, it's just a JSON flavor so is it really that interesting? But what I find so interesting about GeoJSON is how much it's format lets you do fun stuff with your data. Knowing you always have location available means you can plot your data (of course!) but also do sorting by distance, filtering to a certain distance, routing, and more.

I'm a huge fan of the JAMStack. The "JAM" in JAMStack stands for JavaScript, APIs, and Markup. It's a fancy new way of saying "static web site", but as static web sites are far more powerful then they were in the past, a new name is appropriate. I thought it would be fun to take a look at how you could integrate GeoJSON data into the JAMStack. In this post I'm going to demonstrate four different integrations to hopefully inspire you to build your own. If you do, be sure to leave me a comment (and a link) about what you did.

For all my examples, I'll be making use of the Eleventy static site generator. Please note that this is simply one of near five hundred options for generating JAMStack sites. While there are definitely particulars of the Eleventy platform that I'll be covering, the important thing to keep in mind is what I'm doing with my data. You should be able to apply these principles to any other engine of your choice.

Our Data and Our Code

For all of my demos, I'll be using a rather small list of features that cover three cats. Each cat has a name, gender, and of course, a location. Here's the entire source of that file.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [ -90.0715, 29.9510 ]
      },
      "properties": {
        "name": "Fred",
    		"gender": "Male"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [ -92.7298, 30.7373 ]
      },
      "properties": {
        "name": "Martha",
    		"gender": "Female"
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [ -91.1473, 30.4711 ]
      },
      "properties": {
        "name": "Zelda",
		    "gender": "Female"
      }
    }
  ]
}

Typically your data would probably include many more features, but we'll keep it short and sweet for our examples.

If you want to play with any of the demos I show below, you can find the entire repository here: https://github.com/cfjedimaster/eleventy-demos/tree/master/geojson

Example One

For our first example, I'm going to keep it as simple as possible. I'm going to take the GeoJSON file, drop it in the appropriate place, and try to make use of it within my site. This end result will be pretty simple:

v1

So how did I build this? To use ad hoc data to your Eleventy site, you typically drop it within a _data folder. Eleventy has built in support for json files, but, there's a problem. GeoJSON files typically end with .geojson. There's an open ticket with the Eleventy project to allow for customization of this feature, but for the first example, I took the expedient route and renamed it to cats.json.

Once the file is renamed and copied, my templates can make use of it. Here's how I built the screenshot above.


<h1>Cats</h1>

<ul>
{% for cat in cats.features %}
<li>{{ cat.properties.name }} is {{ cat.properties.gender }} and is 
	located at {{ cat.geometry.coordinates[1] }}, {{ cat.geometry.coordinates[0] }}</li>
{% endfor %}
</ul>

When you drop a file named cats.json into your _data folder, Eleventy will expose it to your templates as cats. Since I know that my cats are stored under the features property, I setup my iteration to go over each feature. Then, to display the name and gender, I remember that they exist under properties. And finally, I display latitude and longitude via geometry.coordinates. GeoJSON puts longitude first. And yes, I constantly get confused as to what is where.

All in all, not too hard, but we had to rename our file, and our HTML template had to really dig into the particulars of GeoJSON in order to display our cats.

This version of the application may be found in the v1 folder of the repository I shared above.

Example Two

Let's kick it up a notch and use another feature of Eleventy to make our data a bit easier to work with. Eleventy supports JSON and JavaScript files to source data for your templates. What if we keep our GeoJSON file as is, but then use a JavaScript file to read it and make it available? Here's how this could be done.

const fs = require('fs');

module.exports = function() {
	let catData = JSON.parse(fs.readFileSync('./_data/cats.geojson', 'utf8'));
	let cats = catData.features.map(c => {
		return {
			name: c.properties.name,
			gender: c.properties.gender,
			location: {lng: c.geometry.coordinates[0], lat: c.geometry.coordinates[1] }
		}
	});

	return cats;
}

I saved this file as cats.js so that it will eventually be exposed as cats to my template. I read in my original GeoJSON file and then loop over it. I wanted to make it easier for my template to use my data, so I map the name, gender, and location values into a simple object. Each cat then should be exposed like so:

{
	name: "Alfred E. Cat",
	gender: "male", 
	location: { lng: -90, lat: 30 }
}

You can see this in play in the new version of the HTML template.


<h1>Cats</h1>

<ul>
{% for cat in cats %}
<li>{{ cat.name }} is {{ cat.gender }} and is 
	located at {{ cat.location.lat] }}, {{ cat.location.lng }}</li>
{% endfor %}
</ul>

That's far simpler, and if for some reason my GeoJSON changes, or I switch to another format entirely, I can simply update cats.js to handle the new form.

This version of the application may be found in the v2 folder of the repository I shared above.

Example Three - Now with Maps

Let's kick it up a notch. By using the HERE Map Image API, we can easily create static maps for each of our cat locations.

v3

The Map Image API is very easy to use. You simply craft a URL with your parameters and then use that URL in an image tag. Here's the updated layout template for this version:


<h1>Cats</h1>
{% for cat in cats %}
<p>
<img src="https://image.maps.api.here.com/mia/1.6/mapview?app_id={{ global.here_app_id }}&app_code={{ global.here_app_code }}&c={{cat.location.lat}},{{cat.location.lng}}&z=9">

{{ cat.name }} is {{ cat.gender }} and is located at {{ cat.address }}.
</p>
{% endfor %}

There's a few things to note in the URL. First, the API requires an app_id and app_code value. I included mine in a new data file called global.json:

{
	"here_app_id":"sdwyQs1YA25FUX6cI7Ko",
	"here_app_code":"SX9_A9lBaqJYoWX_DIO1ag"
}

Next, I tell the api to center (via c) on the cat's location. The last value I specified, z, specified the zoom. The API supports a heck of a lot more options, but I wanted to keep it simple for the demo and just show the general area where the cat may be found.

This version of the application may be found in the v3 folder of the repository I shared above.

Example Four

And now for the final, and most awesome, version. (Ok, "awesome" may be a bit much.) In the previous example, we used a HERE API to render a map of the cat's location. For most people, that's a heck of a lot more useful than displaying the longitude and latitude. We can go even further though. By using HERE's reverse geocoding API we can translate the GPS locations to a human readable address instead. You can see the results below:

v4

In order to add this information, I modified cats.js as so:

const fs = require('fs');
const fetch = require('node-fetch');
const global = require('./global');

module.exports = async function() {
	let catData = JSON.parse(fs.readFileSync('./_data/cats.geojson', 'utf8'));

	let cats = catData.features.map(c => {
		return {
			name: c.properties.name,
			gender: c.properties.gender,
			location: { lng: c.geometry.coordinates[0], lat: c.geometry.coordinates[1] }
		}
	});

	//https://stackoverflow.com/a/37576787/52160
	for(const cat of cats) {
		console.log('do address for ' + cat.name);
		let url = `https://reverse.geocoder.api.here.com/6.2/reversegeocode.json?app_id=${global.here_app_id}&app_code=${global.here_app_code}&prox=${cat.location.lat}%2C${cat.location.lng}%2C250&mode=retrieveAddresses&maxresults=1`;

		let locationRequest = await fetch(url);
		let location = await locationRequest.json();
		cat.address = location.Response.View[0].Result[0].Location.Address.Label;

	}
	
	return cats;
}

You can see we still have the original code to modify the GeoJSON into a nicer format. Then we loop over that data and call the API to load in the address. This bit, cat.address = location.Response.View[0].Result[0].Location.Address.Label is where we grab the result. The API returns a lot of information, but we can grab just the simple text-based address.

This version of the application may be found in the v4 folder of the repository I shared above.

Wrap Up

So I hope these examples inspire you both to look into using HERE's APIs as well as how you can add geographic data to your JAMStack-based web sites. As always, if you've got any questions, suggestions, or examples, just leave a comment below.