Eric
Howey.

There is a hidden page you can visit at /you-are-awesome. Didn't want you to miss out on the fun!
Content has not been updated for more than 2 years, proceed with caution.

Geocoding with Gatsby and SANITY.io

Geocoding is the process of turning a regular address into latitude and longtitude coordinates that can be used in a mapping application like Google Maps or LeafletJS. I am going to walk through setting up a SANITY.io schema, querying the SANITY.io API via gatsby-node.js, passing the query results to the OpenCage geocoder API, and then finally passing those combined results into a brand new GraphQL node that can be queried inside your Gatsby site.

In a future post I will talk about using these coordinates to create a map using LeafletJS.

For the purpose of this tutorial we will assume you are building something like a real estate app - where you would need to map locations based on their address. But a realtor (or a typical content editor) is not going to know the latitude, longitude of a home - we need to handle that process for them behind the scenes.

There is a plugin called gatsby-transformer-opencage-geocoder which didn’t work for me. However, I did take inspiration from the plugin for how to handle this.

Add additional dependencies

The first step we will do is add a few additional dependencies we need to our project.

yarn add opencage-api-client graphql-request

or

npm install opencage-api-client graphql-request

Setup your SANITY.io content structure

Next you will need to setup a SANITY.io content structure. Here is what a really basic content structure for a “home” in the database could look like.

// home.js
export default {
  name: 'home',
  title: 'Home',
  type: 'document',
  fields: [
    {
      name: 'name',
      title: 'Name',
      type: 'string',
    },
    {
      name: 'address',
      title: 'Address',
      type: 'address', // References address.js
    },
    {
      name: 'summary',
      title: 'Summary',
      type: 'text',
    },
  ],
}
// address.js referenced in home.js
export default {
  name: 'address',
  title: 'Address',
  type: 'document',
  fields: [
    {
      name: 'address',
      title: 'Street Address',
      type: 'string',
    },
    {
      name: 'city',
      title: 'Municipality',
      type: 'string',
    },
    {
      name: 'province',
      title: 'Province',
      type: 'string',
    },
    {
      name: 'country',
      title: 'Country',
      type: 'string',
    },
    {
      name: 'postal',
      title: 'Postal Code',
      type: 'string',
    },
  ],
}

When queried we will get a data structure back from SANITY.io that looks something like this:

home: {
  name: "Childhood Home"
  summary: "Where I grew up."
  address: {
    address: "863 W Hastings St",
    city: "Vancouver",
    province: "BC",
    country: "Canada",
    postal: "V6C 3J1",
  }
}

Query for content in gatsby-node.js before node creation

Typically when you see queries happening in gatsby-node.js (for example in the createPages lifecycle hook) they happen later in the build process and can access the GraphQL nodes Gatsby has created earlier on. In this case we want to create custom GraphQL nodes ourself so we need to get the data straight from SANITY.io rather than using the source plugin.

We will be using a GraphQL library called graphql-request to handle the API call.

All of these steps happen inside of the sourceNodes API from Gatsby. This is the step in the build process where Gatsby actually creates the GraphQL data layer.

Here is what that query looks like:

// gatsby-node.js
const { request } = require('graphql-request')
 
exports.sourceNodes = async ({
  actions: { createNode },
  createNodeId,
  createContentDigest,
}) => {
  // Get the data from SANITY.io
  const endpoint = `https://[project-id-here].apicdn.sanity.io/v1/graphql/production/default`
  const query = `{
    allHome {
      name
      summary
      address {
        address
        city
        province
        country
        postal
      }
    }
  }`
  const data = await request(endpoint, query)
  const homes = await data.allHome // Make the data easier to work with
}

Geocode function

Now that we have our data from SANITY.io we need a function to handle resolving the coordinates from a geocode API. In this example I am using the API from OpenCage Geocoder but you could probably adapt this example for other geocode services. We will call this function in the next step when we create the nodes.

// gatsby-node.js
const { request } = require('graphql-request')
const opencage = require('opencage-api-client')
require('dotenv').config()
 
exports.sourceNodes = async ({
  actions: { createNode },
  createNodeId,
  createContentDigest,
}) => {
  // Get the data from SANITY.io
  const endpoint = `https://[project-id-here].apicdn.sanity.io/v1/graphql/production/default`
  const query = `{
    ...query here
  }`
  const data = await request(endpoint, query)
  const homes = await data.allHome
 
  // Geocode function
  const getGeoCode = async (address) => {
    // Build the query from our data in the formnat address, city, province, country, postal code
    const query = `${address.address}, ${address.city}, ${address.province}, ${address.country}, ${address.postal}`
    // Use an env variable for your API key
    const apiRequestOptions = { key: process.env.OPENCAGE_API_KEY, q: query }
    // Fetch the data from OpenCage
    const data = await opencage.geocode(apiRequestOptions)
    // Parse out just the lat/long coordinates from the returned data
    const place = data.results[0].geometry
    // Create a new JSON object with the full address including coordinates.
    const fullAddress = Object.assign({}, address, place)
    // Return that address
    return fullAddress
  }
}

Create the GraphQL nodes

Now we need to loop over all the “homes” using the map method to create our new GraphQL nodes. The node creation process needs to be wrapped in a Promise to ensure that it does not try to run without the data from SANITY.io and Opencage APIs. We will be creating GraphQL nodes named Home and Gatsby will also automatically create an allHome node we can query.

Here is the code with comments to explain what is happening on each line:

// gatsby-node.js
const { request } = require('graphql-request')
const opencage = require('opencage-api-client')
require('dotenv').config()
 
exports.sourceNodes = async ({
  actions: { createNode },
  createNodeId,
  createContentDigest,
}) => {
  // Get the data from SANITY.io
  // Put your project ID in - this assumes a public dataset
  const endpoint = `https://[project-id-here].apicdn.sanity.io/v1/graphql/production/default`
  const query = `{
    ...query here
  }`
  const data = await request(endpoint, query)
  const homes = await data.allHome
 
  // Geocode function
  const getGeoCode = async (address) => {
    // ... geocode function here
  }
 
  // Create the GraphQL nodes
  // Wrap everything in a promise so that it does not try to run without the data
  await Promise.all(
    // Map over all of the homes
    homes.map(async (home) => {
      // Get the full address from the Geocode function
      const homeAddress = await getGeoCode(home.address)
      // Remove the old home.address field from the homne object
      delete home.address
      // Create a new home object that has the full address with Geocode.
      // This will also have other information like name, categories, summary, etc.
      const homeComplete = Object.assign({}, home, homeAddress)
      // Create the nodeMetadata
      const nodeMetadata = {
        id: createNodeId(home.name),
        parent: null,
        children: [],
        internal: {
          type: `Home`,
          content: JSON.stringify(homeComplete),
          contentDigest: createContentDigest(homeComplete),
        },
      }
      // Create the full node object
      const node = Object.assign({}, homeComplete, nodeMetadata)
      // Create the GraphQL node
      createNode(node)
    }),
  )
}

Catch those errors

Last but not least, we are going to wrap everything in a try…catch statement to give us better error logging. I have also included the complete code in this snippet so you can see all the steps put together.

// gatsby-node.js
const { request } = require('graphql-request')
const opencage = require('opencage-api-client')
require('dotenv').config()
 
exports.sourceNodes = async ({
  actions: { createNode },
  createNodeId,
  createContentDigest,
}) => {
  try {
    // Get the data from SANITY.io
    const endpoint = `https://[project-id-here].apicdn.sanity.io/v1/graphql/production/default`
    const query = `{
    allHome {
      name
      summary
      address {
        address
        city
        province
        country
        postal
      }
    }
  }`
    const data = await request(endpoint, query)
    const homes = await data.allHome // Make the data easier to work with
 
    // Geocode function
    const getGeoCode = async (address) => {
      // Build the query from our data in the formnat address, city, province, country, postal code
      const query = `${address.address}, ${address.city}, ${address.province}, ${address.country}, ${address.postal}`
      // Use an env variable for your API key
      const apiRequestOptions = { key: process.env.OPENCAGE_API_KEY, q: query }
      // Fetch the data from OpenCage
      const data = await opencage.geocode(apiRequestOptions)
      // Parse out just the lat/long coordinates from the returned data
      const place = data.results[0].geometry
      // Create a new JSON object with the full address including coordinates.
      const fullAddress = Object.assign({}, address, place)
      // Return that address
      return fullAddress
    }
    // Create the GraphQL nodes
    // Wrap everything in a promise so that it does not try to run without the data
    await Promise.all(
      // Map over all of the homes
      homes.map(async (home) => {
        // Get the full address from the Geocode function
        const homeAddress = await getGeoCode(home.address)
        // Remove the old home.address field from the homne object
        delete home.address
        // Create a new home object that has the full address with Geocode.
        // This will also have other information like name, summary, etc.
        const homeComplete = Object.assign({}, home, homeAddress)
        // Create the nodeMetadata
        const nodeMetadata = {
          id: createNodeId(home.name),
          parent: null,
          children: [],
          internal: {
            type: `Home`,
            content: JSON.stringify(homeComplete),
            contentDigest: createContentDigest(homeComplete),
          },
        }
        // Create the full node JSON object
        const node = Object.assign({}, homeComplete, nodeMetadata)
        // Create the GraphQL node
        createNode(node)
      }),
    )
  } catch (e) {
    console.error(e)
  }
}

I hope this helps in your mapping and geocoding journey in the JAMStack and as always I would be happy to answer questions on Twitter or by email.

Happy coding!