|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- title: A microdata enhanced HTML Webcomponent for Leaflet
- url: https://blog.k-nut.eu/leaflet-microdata-html-webcomponent
- hash_url: 65fba9cd025cd2403f932cb2c928cf14
- 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
-
- <p>The people in my RSS reader have been talking some more about Web Components in the
- past couple of months. Jim Nielsen had <a href="https://blog.jim-nielsen.com/2023/html-web-components/">a post</a>
- referring to Jeremy Keith coining the term <a href="https://adactio.com/journal/20618">HTML Web Components</a>
- 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.</p>
-
- <p>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.</p>
-
- <p>As an example, assume that we want to render schools on a map. The data in
- our example is taken from our <a href="https://jedeschule.codefor.de/docs">jedeschule.de API</a>
- which has the goal of making all primary and secondary schools in Germany
- searchable and queryable.</p>
-
- <p>With the approach I’m proposing, we can author markup that looks like this:</p>
-
- <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><leaflet-map></span>
- <span class="nt"><div</span> <span class="na">id=</span><span class="s">"map"</span><span class="nt">></div></span>
- <span class="nt"><ul</span> <span class="na">class=</span><span class="s">"locations"</span><span class="nt">></span>
- <span class="nt"><li</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/School"</span> <span class="na">data-fresh-key=</span><span class="s">"NW-153710"</span><span class="nt">></span>
- <span class="nt"><h2</span> <span class="na">itemprop=</span><span class="s">"name"</span><span class="nt">></span>Dahlingschule, Städt. Förderschule im integr. Verbund, FSP Lernen u. Emot. und soziale Entwicklung,-Primarstufe u. SekI<span class="nt"></h2></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"address"</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/PostalAddress"</span><span class="nt">></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"streetAddress"</span><span class="nt">></span>Dahlingstr. 40<span class="nt"></div></span>
- <span class="nt"><span</span> <span class="na">itemprop=</span><span class="s">"postalCode"</span><span class="nt">></span>47229<span class="nt"></span></span>
- <span class="nt"><span</span> <span class="na">itemprop=</span><span class="s">"addressLocality"</span><span class="nt">></span>Duisburg<span class="nt"></span></span>
- <span class="nt"></div></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"geo"</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/GeoCoordinates"</span><span class="nt">></span>
- <span class="nt"><meta</span> <span class="na">itemprop=</span><span class="s">"latitude"</span> <span class="na">content=</span><span class="s">"51.38331874331818"</span><span class="nt">/></span>
- <span class="nt"><meta</span> <span class="na">itemprop=</span><span class="s">"longitude"</span> <span class="na">content=</span><span class="s">"6.700606101666003"</span><span class="nt">/></span>
- <span class="nt"></div></span>
- <span class="nt"></li></span>
- <span class="nt"><li</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/School"</span> <span class="na">data-fresh-key=</span><span class="s">"NW-166480"</span><span class="nt">></span>
- <span class="nt"><h2</span> <span class="na">itemprop=</span><span class="s">"name"</span><span class="nt">></span>Montessori-Gymnasium Städt. Gymnasium für Jungen und Mädchen<span class="nt"></h2></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"address"</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/PostalAddress"</span><span class="nt">></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"streetAddress"</span><span class="nt">></span>Rochusstr. 145<span class="nt"></div></span>
- <span class="nt"><span</span> <span class="na">itemprop=</span><span class="s">"postalCode"</span><span class="nt">></span>50827<span class="nt"></span></span>
- <span class="nt"><span</span> <span class="na">itemprop=</span><span class="s">"addressLocality"</span><span class="nt">></span>Köln<span class="nt"></span></span>
- <span class="nt"></div></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"geo"</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/GeoCoordinates"</span><span class="nt">></span>
- <span class="nt"><meta</span> <span class="na">itemprop=</span><span class="s">"latitude"</span> <span class="na">content=</span><span class="s">"50.963204818343755"</span><span class="nt">/></span>
- <span class="nt"><meta</span> <span class="na">itemprop=</span><span class="s">"longitude"</span> <span class="na">content=</span><span class="s">"6.904840537750957"</span><span class="nt">/></span>
- <span class="nt"></div></span>
- <span class="nt"></li></span>
- <span class="nt"><li</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/School"</span> <span class="na">data-fresh-key=</span><span class="s">"NW-167782"</span><span class="nt">></span>
- <span class="nt"><h2</span> <span class="na">itemprop=</span><span class="s">"name"</span><span class="nt">></span>Städt. Grillo-Gymnasium<span class="nt"></h2></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"address"</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/PostalAddress"</span><span class="nt">></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"streetAddress"</span><span class="nt">></span>Hauptstr. 60<span class="nt"></div></span>
- <span class="nt"><span</span> <span class="na">itemprop=</span><span class="s">"postalCode"</span><span class="nt">></span>45879<span class="nt"></span></span>
- <span class="nt"><span</span> <span class="na">itemprop=</span><span class="s">"addressLocality"</span><span class="nt">></span>Gelsenkirchen<span class="nt"></span></span>
- <span class="nt"></div></span>
- <span class="nt"><div</span> <span class="na">itemprop=</span><span class="s">"geo"</span> <span class="na">itemscope</span> <span class="na">itemtype=</span><span class="s">"https://schema.org/GeoCoordinates"</span><span class="nt">></span>
- <span class="nt"><meta</span> <span class="na">itemprop=</span><span class="s">"latitude"</span> <span class="na">content=</span><span class="s">"51.5130837882258"</span><span class="nt">/></span>
- <span class="nt"><meta</span> <span class="na">itemprop=</span><span class="s">"longitude"</span> <span class="na">content=</span><span class="s">"7.099798427442939"</span><span class="nt">/></span>
- <span class="nt"></div></span>
- <span class="nt"></li></span>
- <span class="nt"></ul></span>
- <span class="nt"></leaflet-map></span>
- <span class="nt"><style></span>
- <span class="nf">#map</span> <span class="p">{</span>
- <span class="nl">width</span><span class="p">:</span> <span class="m">800px</span><span class="p">;</span>
- <span class="nl">height</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
- <span class="py">aspect-ratio</span><span class="p">:</span> <span class="m">16</span><span class="p">/</span><span class="m">9</span><span class="p">;</span>
- <span class="nl">background-image</span><span class="p">:</span> <span class="sx">url("/map.png")</span><span class="p">;</span>
- <span class="nl">background-size</span><span class="p">:</span> <span class="n">contain</span><span class="p">;</span>
- <span class="p">}</span>
- <span class="nt"></style></span>
- </code></pre></div></div>
-
- <p>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 <code class="highlighter-rouge">#map</code> 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.</p>
-
- <p>Since the data is semi-structured and machine readable, we can now also consume it from
- a Web Component to add interactivity with <a href="https://leafletjs.com">Leaflet</a>. The code to do so
- looks like this:</p>
- <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">LeafletMap</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
- <span class="nx">connectedCallback</span><span class="p">()</span> <span class="p">{</span>
- <span class="kd">const</span> <span class="nx">mapElement</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s2">"#map"</span><span class="p">);</span>
- <span class="nx">mapElement</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">backgroundImage</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span>
-
- <span class="kd">const</span> <span class="nx">schools</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span>
- <span class="s1">'[itemType="https://schema.org/School"]'</span><span class="p">,</span>
- <span class="p">);</span>
-
- <span class="kd">var</span> <span class="nx">map</span> <span class="o">=</span> <span class="nx">L</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">mapElement</span><span class="p">).</span><span class="nx">setView</span><span class="p">([</span><span class="mf">51.505</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.09</span><span class="p">],</span> <span class="mi">13</span><span class="p">);</span>
- <span class="nx">L</span><span class="p">.</span><span class="nx">tileLayer</span><span class="p">(</span><span class="s2">"https://tile.openstreetmap.org/{z}/{x}/{y}.png"</span><span class="p">,</span> <span class="p">{</span>
- <span class="na">attribution</span><span class="p">:</span>
- <span class="s1">'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'</span><span class="p">,</span>
- <span class="p">}).</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
-
- <span class="kd">const</span> <span class="nx">markers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">L</span><span class="p">.</span><span class="nx">featureGroup</span><span class="p">();</span>
- <span class="nx">schools</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">school</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
- <span class="kd">const</span> <span class="nx">latitude</span> <span class="o">=</span> <span class="nx">school</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'[itemProp="latitude"]'</span><span class="p">).</span><span class="nx">content</span><span class="p">;</span>
- <span class="kd">const</span> <span class="nx">longitude</span> <span class="o">=</span> <span class="nx">school</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'[itemProp="longitude"]'</span><span class="p">).</span><span class="nx">content</span><span class="p">;</span>
- <span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">school</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'[itemProp="name"]'</span><span class="p">).</span><span class="nx">textContent</span><span class="p">;</span>
- <span class="kd">const</span> <span class="nx">address</span> <span class="o">=</span> <span class="nx">school</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'[itemProp="address"]'</span><span class="p">);</span>
-
- <span class="kd">const</span> <span class="nx">h2</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"h2"</span><span class="p">);</span>
- <span class="nx">h2</span><span class="p">.</span><span class="nx">innerText</span> <span class="o">=</span> <span class="nx">name</span><span class="p">;</span>
- <span class="kd">const</span> <span class="nx">popup</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">"div"</span><span class="p">);</span>
- <span class="nx">popup</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s2">"popup"</span><span class="p">);</span>
- <span class="nx">popup</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">h2</span><span class="p">);</span>
- <span class="nx">popup</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">address</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>
-
- <span class="kd">const</span> <span class="nx">marker</span> <span class="o">=</span> <span class="nx">L</span><span class="p">.</span><span class="nx">marker</span><span class="p">([</span><span class="nx">latitude</span><span class="p">,</span> <span class="nx">longitude</span><span class="p">]).</span><span class="nx">bindPopup</span><span class="p">(</span><span class="nx">popup</span><span class="p">);</span>
- <span class="nx">marker</span><span class="p">.</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">markers</span><span class="p">);</span>
- <span class="p">});</span>
- <span class="nx">markers</span><span class="p">.</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
-
- <span class="nx">map</span><span class="p">.</span><span class="nx">fitBounds</span><span class="p">(</span><span class="nx">markers</span><span class="p">.</span><span class="nx">getBounds</span><span class="p">());</span>
- <span class="p">}</span>
- <span class="p">}</span>
-
- <span class="nb">window</span><span class="p">.</span><span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="s2">"leaflet-map"</span><span class="p">,</span> <span class="nx">LeafletMap</span><span class="p">);</span>
- </code></pre></div></div>
-
- <p>I quite like the way that this reads. The <code class="highlighter-rouge">itemProp</code>s 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.</p>
-
- <p>You can try this on CodePen:</p>
-
-
- <p>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 <code class="highlighter-rouge">[itemType="https://schema.org/School]</code>
- but would probably want to find a way to query for everything that is a child of <code class="highlighter-rouge">https://schema.org/Place</code>.
- 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 <a href="https://docs.mapbox.com/api/maps/static-images/">Mapbox Static Images
- API</a> to dynamically create the fallback
- images for clients that do not support JavaScript or Web Components.</p>
-
- <p>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 <code class="highlighter-rouge">itemProp</code>s for query selectors also makes for a nice
- authoring experience with good separation of concerns.</p>
-
- <p>What do you think? Let me know on <a href="https://berlin.social/@knut">mastodon</a>.</p>
|