A place to cache linked articles (think custom and personal wayback machine)
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.md 18KB

title: A microdata enhanced HTML Webcomponent for Leaflet url: https://blog.k-nut.eu/leaflet-microdata-html-webcomponent hash_url: 65fba9cd02 archive_date: 2024-03-25 og_image: description: An example of using schema.org microdata to build a HTML Webcomponent for Leaflet favicon: https://blog.k-nut.eu/favicon-32x32.png language: en_US

The people in my RSS reader have been talking some more about Web Components in the past couple of months. Jim Nielsen had a post referring to Jeremy Keith coining the term HTML Web Components for a special kind of Web Components. The idea essentially is that you have regular HTML markup that is wrapped by a custom Web Component which enhances the user experience.

I thought that this was quite an interesting idea and thought about situations in which it could be applied. Working with open data quite a bit, we often find ourselves in situations where we build maps. Additionally, the German open data scene has been lobbying for more linked open data in the past couple of months. I think I came up with an example which combines these three things - HTML Web Components, maps and Linked Data (or a flavor thereof) quite nicely.

As an example, assume that we want to render schools on a map. The data in our example is taken from our jedeschule.de API which has the goal of making all primary and secondary schools in Germany searchable and queryable.

With the approach I’m proposing, we can author markup that looks like this:

<leaflet-map>
  <div id="map"></div>
  <ul class="locations">
      <li itemscope itemtype="https://schema.org/School" data-fresh-key="NW-153710">
          <h2 itemprop="name">Dahlingschule, Städt. Förderschule im integr. Verbund, FSP Lernen u. Emot. und soziale Entwicklung,-Primarstufe u. SekI</h2>
          <div itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
              <div itemprop="streetAddress">Dahlingstr. 40</div>
              <span itemprop="postalCode">47229</span>
              <span itemprop="addressLocality">Duisburg</span>
          </div>
          <div itemprop="geo" itemscope itemtype="https://schema.org/GeoCoordinates">
              <meta itemprop="latitude" content="51.38331874331818"/>
              <meta itemprop="longitude" content="6.700606101666003"/>
          </div>
      </li>
      <li itemscope itemtype="https://schema.org/School" data-fresh-key="NW-166480">
          <h2 itemprop="name">Montessori-Gymnasium Städt. Gymnasium für Jungen und Mädchen</h2>
          <div itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
              <div itemprop="streetAddress">Rochusstr. 145</div>
              <span itemprop="postalCode">50827</span>
              <span itemprop="addressLocality">Köln</span>
          </div>
          <div itemprop="geo" itemscope itemtype="https://schema.org/GeoCoordinates">
              <meta itemprop="latitude" content="50.963204818343755"/>
              <meta itemprop="longitude" content="6.904840537750957"/>
          </div>
      </li>
      <li itemscope itemtype="https://schema.org/School" data-fresh-key="NW-167782">
          <h2 itemprop="name">Städt. Grillo-Gymnasium</h2>
          <div itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
              <div itemprop="streetAddress">Hauptstr. 60</div>
              <span itemprop="postalCode">45879</span>
              <span itemprop="addressLocality">Gelsenkirchen</span>
          </div>
          <div itemprop="geo" itemscope itemtype="https://schema.org/GeoCoordinates">
              <meta itemprop="latitude" content="51.5130837882258"/>
              <meta itemprop="longitude" content="7.099798427442939"/>
          </div>
      </li>
  </ul>
</leaflet-map>
<style>
#map {
    width: 800px;
    height: auto;
    aspect-ratio: 16/9;
    background-image: url("/map.png");
    background-size: contain;
}
</style>

In this form, this isn’t interactive yet but it can already be easily consumed by both humans and computers. Humans will see a static map (I simply took a screenshot and set is as a background image for the #map node) and a list of schools with their addresses. Computers will be able to also parse the schema.org annotations to extract structured data out of the list.

Since the data is semi-structured and machine readable, we can now also consume it from a Web Component to add interactivity with Leaflet. The code to do so looks like this:

class LeafletMap extends HTMLElement {
  connectedCallback() {
    const mapElement = this.querySelector("#map");
    mapElement.style.backgroundImage = "none";

    const schools = this.querySelectorAll(
      '[itemType="https://schema.org/School"]',
    );

    var map = L.map(mapElement).setView([51.505, -0.09], 13);
    L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
      attribution:
        '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    }).addTo(map);

    const markers = new L.featureGroup();
    schools.forEach((school) => {
      const latitude = school.querySelector('[itemProp="latitude"]').content;
      const longitude = school.querySelector('[itemProp="longitude"]').content;
      const name = school.querySelector('[itemProp="name"]').textContent;
      const address = school.querySelector('[itemProp="address"]');

      const h2 = document.createElement("h2");
      h2.innerText = name;
      const popup = document.createElement("div");
      popup.classList.add("popup");
      popup.appendChild(h2);
      popup.appendChild(address.cloneNode(true));

      const marker = L.marker([latitude, longitude]).bindPopup(popup);
      marker.addTo(markers);
    });
    markers.addTo(map);

    map.fitBounds(markers.getBounds());
  }
}

window.customElements.define("leaflet-map", LeafletMap);

I quite like the way that this reads. The itemProps make for very nice selectors and actually allow developers to completely change the structure of the HTML. As long as the annotations are kept, the Web Component will be able to still extract the locations and to load them into leaflet.

You can try this on CodePen:

This of course is nowhere from production ready but shall only serve as a proof of concept. In a more advanced version, we wouldn’t limit our initial query selector to [itemType="https://schema.org/School] but would probably want to find a way to query for everything that is a child of https://schema.org/Place. We would also need to adapt the static image if we wanted to change the underlying data. If we have server side rendering at our disposal, we could use something like the Mapbox Static Images API to dynamically create the fallback images for clients that do not support JavaScript or Web Components.

All in all, this feels like a nice approach to me though. It provides content that can be consumed by computers and humans with up to date or legacy (or privacy concious) devices easily. Using the itemProps for query selectors also makes for a nice authoring experience with good separation of concerns.

What do you think? Let me know on mastodon.