Creating a HTML document with inline GeoJSON from Airtable List Node

Describe the issue/error/question

Hello again n8n community!
I have an Airtable database containing location information, and I’m trying to build a HTML page using Mapbox which dynamically updates the map markers based on data in the Airtable. The Mapbox page works with inline GeoJSON and so it seems feasible that I could trigger a workflow when there are changes to the table that gets the current dataset, builds the page with static HTML and inserts the GeoJSON, then uploads to my web server using the FTP node. Without good knowledge of JS however, I’m struggling a little and wonder if I might find some help here.

What is the error message (if any)?

The first challenge I’ve encountered is how to iterate through the results of the Airtable List node to build the GeoJSON payload.

Here is what I have so far:

This is the output from my Airtable List Node (with dummy data):

[
{
"id": "rec2mEmvEzbTqwDtd",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:30:07.000Z"
},
{
"id": "rec6b5rxaP3UtN7Ih",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:34:04.000Z"
},
{
"id": "rec8qfIlvh6UtkRhJ",
"fields": {
"Lat Long": "39.289374 ,-76.607427",
"Address": "20 Market Place",
"City": "Baltimore",
"State": "Maryland",
"Zip": "21202",
"Country": "United States",
"Phone": "+12849573939"
},
"createdTime": "2022-01-17T13:35:43.000Z"
},
{
"id": "recBU3l1nVBa1mmu3",
"fields": {
"Lat Long": "39.289613 ,-76.609842",
"Address": "409 East Baltimore Street",
"City": "Baltimore",
"State": "Maryland",
"Zip": "21202",
"Country": "United States",
"Phone": "+573104646721"
},
"createdTime": "2022-01-17T13:45:02.000Z"
},
{
"id": "recBwIBksSekotAjm",
"fields": {
"Phone": "+96170730630"
},
"createdTime": "2022-01-17T17:34:28.000Z"
},
{
"id": "recCfBq7bs20A5Lpt",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T14:18:18.000Z"
},
{
"id": "recEjC32zZt2DJe3s",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:08:46.000Z"
},
{
"id": "recM4MYs6p5NEVUhD",
"fields": {
"Lat Long": "39.289374 ,-76.607427",
"Address": "20 Market Place",
"City": "Baltimore",
"State": "Maryland",
"Zip": "21202",
"Country": "United States"
},
"createdTime": "2022-01-17T15:26:17.000Z"
},
{
"id": "recMNJU8KioasuaIe",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:10:49.000Z"
},
{
"id": "recMR99DR1hVa3g46",
"fields": {
"Phone": "+447496116617"
},
"createdTime": "2022-01-17T15:36:50.000Z"
},
{
"id": "recMX2BJG7T3uUK4B",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T14:19:56.000Z"
},
{
"id": "recNY00Do124j9u0k",
"fields": {
"Lat Long": "40.637803 ,-73.781049",
"Address": "Terminal 4 Departures",
"State": "New York",
"Zip": "11430",
"Country": "United States"
},
"createdTime": "2022-01-16T20:48:09.000Z"
},
{
"id": "recNzVNRMszv3ibLH",
"fields": {
"Lat Long": "39.288445 ,-76.608483",
"Address": "520 East Lombard Street",
"City": "Baltimore",
"State": "Maryland",
"Zip": "21202",
"Country": "United States",
"Phone": "+529841858393"
},
"createdTime": "2022-01-17T15:37:21.000Z"
},
{
"id": "recQIpVsyl4R20ljs",
"fields": {
"Lat Long": "39.289154 ,-76.609095",
"Address": "22 South Gay Street",
"City": "Baltimore",
"State": "Maryland",
"Zip": "21202",
"Country": "United States"
},
"createdTime": "2022-01-17T14:42:59.000Z"
},
{
"id": "recRoWgHRi3Cz9mlj",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:32:29.000Z"
},
{
"id": "recbln9bdsyRBZkPM",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T15:55:42.000Z"
},
{
"id": "recdUiXLNfiZIXvuM",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-16T16:48:59.000Z"
},
{
"id": "recfNWi3E30SRrFNO",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:05:44.000Z"
},
{
"id": "recltnT5LX5q4TLsZ",
"fields": {
"Lat Long": "39.286476 ,-76.604022",
"Address": "301-499 President Street",
"City": "Baltimore",
"State": "Maryland",
"Zip": "21202",
"Country": "United States"
},
"createdTime": "2022-01-17T20:41:42.000Z"
},
{
"id": "recnBcbCcg8rkLbg6",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T14:03:50.000Z"
},
{
"id": "recnWAcz8KLVsUyXV",
"fields": {
"Lat Long": "40.649901 ,-73.783068",
"Address": "1 Central Terminal Area",
"State": "New York",
"Zip": "11430",
"Country": "United States"
},
"createdTime": "2022-01-16T17:50:02.000Z"
},
{
"id": "recoPmdxeDx7Q5cb4",
"fields": {
"Lat Long": "40.660724 ,-73.782604",
"Address": "6 North Boundary Road",
"State": "New York",
"Zip": "11430",
"Country": "United States"
},
"createdTime": "2022-01-16T17:49:52.000Z"
},
{
"id": "recokyAHBmWLapvWa",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T15:55:11.000Z"
},
{
"id": "recqEc0A0MJefupI7",
"fields": {
"Lat Long": "43,-72",
"City": "Hancock",
"State": "New Hampshire",
"Country": "United States"
},
"createdTime": "2022-01-17T17:31:38.000Z"
},
{
"id": "recqnbJV1gyOxjQ0d",
"fields": {
"Lat Long": "40.650361 ,-73.792398",
"State": "New York",
"Zip": "11430",
"Country": "United States"
},
"createdTime": "2022-01-16T17:50:50.000Z"
}
]

This is an example of the GeoJSON I would like to build:

{
        'type': 'FeatureCollection',
        'features': [
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.034084142948, 38.909671288923]
            },
            'properties': {
              'phoneFormatted': '(202) 234-7336',
              'phone': '2022347336',
              'address': '1471 P St NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at 15th St NW',
              'postalCode': '20005',
              'state': 'D.C.'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.049766, 38.900772]
            },
            'properties': {
              'phoneFormatted': '(202) 507-8357',
              'phone': '2025078357',
              'address': '2221 I St NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at 22nd St NW',
              'postalCode': '20037',
              'state': 'D.C.'
            }
          }
        ]
      }

Here is the final HTML page that I would like. to create (you will see dummy GeoJSON data):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Demo: Build a store locator using Mapbox GL JS</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://api.tiles.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js"></script>
    <link
      href="https://api.tiles.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css"
      rel="stylesheet"
    />
    <style>
      body {
        color: #404040;
        font: 400 15px/22px 'Source Sans Pro', 'Helvetica Neue', sans-serif;
        margin: 0;
        padding: 0;
        -webkit-font-smoothing: antialiased;
      }

      * {
        box-sizing: border-box;
      }

      .sidebar {
        position: absolute;
        width: 33.3333%;
        height: 100%;
        top: 0;
        left: 0;
        overflow: hidden;
        border-right: 1px solid rgba(0, 0, 0, 0.25);
      }

      .map {
        position: absolute;
        left: 33.3333%;
        width: 66.6666%;
        top: 0;
        bottom: 0;
      }

      h1 {
        font-size: 22px;
        margin: 0;
        font-weight: 400;
        line-height: 20px;
        padding: 20px 2px;
      }

      a {
        color: #404040;
        text-decoration: none;
      }

      a:hover {
        color: #101010;
      }

      .heading {
        background: #fff;
        border-bottom: 1px solid #eee;
        min-height: 60px;
        line-height: 60px;
        padding: 0 10px;
        background-color: #00853e;
        color: #fff;
      }

      .listings {
        height: 100%;
        overflow: auto;
        padding-bottom: 60px;
      }

      .listings .item {
        display: block;
        border-bottom: 1px solid #eee;
        padding: 10px;
        text-decoration: none;
      }

      .listings .item:last-child {
        border-bottom: none;
      }
      .listings .item .title {
        display: block;
        color: #00853e;
        font-weight: 700;
      }

      .listings .item .title small {
        font-weight: 400;
      }
      .listings .item.active .title,
      .listings .item .title:hover {
        color: #8cc63f;
      }
      .listings .item.active {
        background-color: #f8f8f8;
      }
      ::-webkit-scrollbar {
        width: 3px;
        height: 3px;
        border-left: 0;
        background: rgba(0, 0, 0, 0.1);
      }
      ::-webkit-scrollbar-track {
        background: none;
      }
      ::-webkit-scrollbar-thumb {
        background: #00853e;
        border-radius: 0;
      }

      .marker {
        border: none;
        cursor: pointer;
        height: 56px;
        width: 56px;
        background-image: url(marker.png);
      }

      /* Marker tweaks */
      .mapboxgl-popup {
        padding-bottom: 50px;
      }

      .mapboxgl-popup-close-button {
        display: none;
      }
      .mapboxgl-popup-content {
        font: 400 15px/22px 'Source Sans Pro', 'Helvetica Neue', sans-serif;
        padding: 0;
        width: 180px;
      }
      .mapboxgl-popup-content h3 {
        background: #91c949;
        color: #fff;
        margin: 0;
        padding: 10px;
        border-radius: 3px 3px 0 0;
        font-weight: 700;
        margin-top: -15px;
      }

      .mapboxgl-popup-content h4 {
        margin: 0;
        padding: 10px;
        font-weight: 400;
      }

      .mapboxgl-popup-content div {
        padding: 10px;
      }

      .mapboxgl-popup-anchor-top > .mapboxgl-popup-content {
        margin-top: 15px;
      }

      .mapboxgl-popup-anchor-top > .mapboxgl-popup-tip {
        border-bottom-color: #91c949;
      }
    </style>
  </head>
  <body>
    <div class="sidebar">
      <div class="heading">
        <h1>Our locations</h1>
      </div>
      <div id="listings" class="listings"></div>
    </div>
    <div id="map" class="map"></div>
    <script>
      mapboxgl.accessToken = 'pk.eyJ1IjoiZmVsaXhjYXJ2ZXIiLCJhIjoiY2t5ZWtuNXNpMTNsbDJ1bnAzajgxMGF6eiJ9._MZ3M6IxSs8WWm3PS7FRiQ';

      /**
       * Add the map to the page
       */
      const map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/light-v10',
        center: [-77.034084142948, 38.909671288923],
        zoom: 13,
        scrollZoom: false
      });

      const stores = {
        'type': 'FeatureCollection',
        'features': [
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.034084142948, 38.909671288923]
            },
            'properties': {
              'phoneFormatted': '(202) 234-7336',
              'phone': '2022347336',
              'address': '1471 P St NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at 15th St NW',
              'postalCode': '20005',
              'state': 'D.C.'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.049766, 38.900772]
            },
            'properties': {
              'phoneFormatted': '(202) 507-8357',
              'phone': '2025078357',
              'address': '2221 I St NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at 22nd St NW',
              'postalCode': '20037',
              'state': 'D.C.'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.043929, 38.910525]
            },
            'properties': {
              'phoneFormatted': '(202) 387-9338',
              'phone': '2023879338',
              'address': '1512 Connecticut Ave NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at Dupont Circle',
              'postalCode': '20036',
              'state': 'D.C.'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.0672, 38.90516896]
            },
            'properties': {
              'phoneFormatted': '(202) 337-9338',
              'phone': '2023379338',
              'address': '3333 M St NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at 34th St NW',
              'postalCode': '20007',
              'state': 'D.C.'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.002583742142, 38.887041080933]
            },
            'properties': {
              'phoneFormatted': '(202) 547-9338',
              'phone': '2025479338',
              'address': '221 Pennsylvania Ave SE',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'btwn 2nd & 3rd Sts. SE',
              'postalCode': '20003',
              'state': 'D.C.'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-76.933492720127, 38.99225245786]
            },
            'properties': {
              'address': '8204 Baltimore Ave',
              'city': 'College Park',
              'country': 'United States',
              'postalCode': '20740',
              'state': 'MD'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.097083330154, 38.980979]
            },
            'properties': {
              'phoneFormatted': '(301) 654-7336',
              'phone': '3016547336',
              'address': '4831 Bethesda Ave',
              'cc': 'US',
              'city': 'Bethesda',
              'country': 'United States',
              'postalCode': '20814',
              'state': 'MD'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.359425054188, 38.958058116661]
            },
            'properties': {
              'phoneFormatted': '(571) 203-0082',
              'phone': '5712030082',
              'address': '11935 Democracy Dr',
              'city': 'Reston',
              'country': 'United States',
              'crossStreet': 'btw Explorer & Library',
              'postalCode': '20190',
              'state': 'VA'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.10853099823, 38.880100922392]
            },
            'properties': {
              'phoneFormatted': '(703) 522-2016',
              'phone': '7035222016',
              'address': '4075 Wilson Blvd',
              'city': 'Arlington',
              'country': 'United States',
              'crossStreet': 'at N Randolph St.',
              'postalCode': '22203',
              'state': 'VA'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-75.28784, 40.008008]
            },
            'properties': {
              'phoneFormatted': '(610) 642-9400',
              'phone': '6106429400',
              'address': '68 Coulter Ave',
              'city': 'Ardmore',
              'country': 'United States',
              'postalCode': '19003',
              'state': 'PA'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-75.20121216774, 39.954030175164]
            },
            'properties': {
              'phoneFormatted': '(215) 386-1365',
              'phone': '2153861365',
              'address': '3925 Walnut St',
              'city': 'Philadelphia',
              'country': 'United States',
              'postalCode': '19104',
              'state': 'PA'
            }
          },
          {
            'type': 'Feature',
            'geometry': {
              'type': 'Point',
              'coordinates': [-77.043959498405, 38.903883387232]
            },
            'properties': {
              'phoneFormatted': '(202) 331-3355',
              'phone': '2023313355',
              'address': '1901 L St. NW',
              'city': 'Washington DC',
              'country': 'United States',
              'crossStreet': 'at 19th St',
              'postalCode': '20036',
              'state': 'D.C.'
            }
          }
        ]
      };

      /**
       * Assign a unique id to each store. You'll use this `id`
       * later to associate each point on the map with a listing
       * in the sidebar.
       */
      stores.features.forEach((store, i) => {
        store.properties.id = i;
      });

      /**
       * Wait until the map loads to make changes to the map.
       */
      map.on('load', () => {
        /**
         * This is where your '.addLayer()' used to be, instead
         * add only the source without styling a layer
         */
        map.addSource('places', {
          'type': 'geojson',
          'data': stores
        });

        /**
         * Add all the things to the page:
         * - The location listings on the side of the page
         * - The markers onto the map
         */
        buildLocationList(stores);
        addMarkers();
      });

      /**
       * Add a marker to the map for every store listing.
       **/
      function addMarkers() {
        /* For each feature in the GeoJSON object above: */
        for (const marker of stores.features) {
          /* Create a div element for the marker. */
          const el = document.createElement('div');
          /* Assign a unique `id` to the marker. */
          el.id = `marker-${marker.properties.id}`;
          /* Assign the `marker` class to each marker for styling. */
          el.className = 'marker';

          /**
           * Create a marker using the div element
           * defined above and add it to the map.
           **/
          new mapboxgl.Marker(el, { offset: [0, -23] })
            .setLngLat(marker.geometry.coordinates)
            .addTo(map);

          /**
           * Listen to the element and when it is clicked, do three things:
           * 1. Fly to the point
           * 2. Close all other popups and display popup for clicked store
           * 3. Highlight listing in sidebar (and remove highlight for all other listings)
           **/
          el.addEventListener('click', (e) => {
            /* Fly to the point */
            flyToStore(marker);
            /* Close all other popups and display popup for clicked store */
            createPopUp(marker);
            /* Highlight listing in sidebar */
            const activeItem = document.getElementsByClassName('active');
            e.stopPropagation();
            if (activeItem[0]) {
              activeItem[0].classList.remove('active');
            }
            const listing = document.getElementById(
              `listing-${marker.properties.id}`
            );
            listing.classList.add('active');
          });
        }
      }

      /**
       * Add a listing for each store to the sidebar.
       **/
      function buildLocationList(stores) {
        for (const store of stores.features) {
          /* Add a new listing section to the sidebar. */
          const listings = document.getElementById('listings');
          const listing = listings.appendChild(document.createElement('div'));
          /* Assign a unique `id` to the listing. */
          listing.id = `listing-${store.properties.id}`;
          /* Assign the `item` class to each listing for styling. */
          listing.className = 'item';

          /* Add the link to the individual listing created above. */
          const link = listing.appendChild(document.createElement('a'));
          link.href = '#';
          link.className = 'title';
          link.id = `link-${store.properties.id}`;
          link.innerHTML = `${store.properties.address}`;

          /* Add details to the individual listing. */
          const details = listing.appendChild(document.createElement('div'));
          details.innerHTML = `${store.properties.city}`;
          if (store.properties.phone) {
            details.innerHTML += ` &middot; ${store.properties.phoneFormatted}`;
          }

          /**
           * Listen to the element and when it is clicked, do four things:
           * 1. Update the `currentFeature` to the store associated with the clicked link
           * 2. Fly to the point
           * 3. Close all other popups and display popup for clicked store
           * 4. Highlight listing in sidebar (and remove highlight for all other listings)
           **/
          link.addEventListener('click', function () {
            for (const feature of stores.features) {
              if (this.id === `link-${feature.properties.id}`) {
                flyToStore(feature);
                createPopUp(feature);
              }
            }
            const activeItem = document.getElementsByClassName('active');
            if (activeItem[0]) {
              activeItem[0].classList.remove('active');
            }
            this.parentNode.classList.add('active');
          });
        }
      }

      /**
       * Use Mapbox GL JS's `flyTo` to move the camera smoothly
       * a given center point.
       **/
      function flyToStore(currentFeature) {
        map.flyTo({
          center: currentFeature.geometry.coordinates,
          zoom: 15
        });
      }

      /**
       * Create a Mapbox GL JS `Popup`.
       **/
      function createPopUp(currentFeature) {
        const popUps = document.getElementsByClassName('mapboxgl-popup');
        if (popUps[0]) popUps[0].remove();
        const popup = new mapboxgl.Popup({ closeOnClick: false })
          .setLngLat(currentFeature.geometry.coordinates)
          .setHTML(
            `<h3>Sweetgreen</h3><h4>${currentFeature.properties.address}</h4>`
          )
          .addTo(map);
      }
    </script>
  </body>
</html>

You will see from my WF that I assume I need to use either the Function/FunctionItem Node’s to get the data output in a single Item, so that the set node can format it correctly. Perhaps the set node isn’t really required b. t it’s what seemed obvious to me.

Then how to insert that payload into a HTML doc and upload? I’m clearly missing a few steps here but it doesn’t seem very far away.

Thanks in advance for reading - any guidance would be greatly appreciated.

I managed to figure this out myself, with a little more digging and some good ol’ fashioned trail and error!

For anyone else who might find this useful here is the simple workflow:

n8n wins again!

1 Like