A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.md 18KB

3 months ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. title: A microdata enhanced HTML Webcomponent for Leaflet
  2. url: https://blog.k-nut.eu/leaflet-microdata-html-webcomponent
  3. hash_url: 65fba9cd025cd2403f932cb2c928cf14
  4. archive_date: 2024-03-25
  5. og_image:
  6. description: An example of using schema.org microdata to build a HTML Webcomponent for Leaflet
  7. favicon: https://blog.k-nut.eu/favicon-32x32.png
  8. language: en_US
  9. <p>The people in my RSS reader have been talking some more about Web Components in the
  10. past couple of months. Jim Nielsen had <a href="https://blog.jim-nielsen.com/2023/html-web-components/">a post</a>
  11. referring to Jeremy Keith coining the term <a href="https://adactio.com/journal/20618">HTML Web Components</a>
  12. for a special kind of Web Components. The idea essentially is that you have regular HTML markup that is wrapped
  13. by a custom Web Component which enhances the user experience.</p>
  14. <p>I thought that this was quite an interesting idea and thought about situations in which
  15. it could be applied. Working with open data quite a bit, we often find ourselves in
  16. situations where we build maps. Additionally, the German open data scene has been lobbying for
  17. more linked open data in the past couple of months. I think I came up with an example which
  18. combines these three things - HTML Web Components, maps and Linked Data (or a flavor
  19. thereof) quite nicely.</p>
  20. <p>As an example, assume that we want to render schools on a map. The data in
  21. our example is taken from our <a href="https://jedeschule.codefor.de/docs">jedeschule.de API</a>
  22. which has the goal of making all primary and secondary schools in Germany
  23. searchable and queryable.</p>
  24. <p>With the approach I’m proposing, we can author markup that looks like this:</p>
  25. <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;leaflet-map&gt;</span>
  26. <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"map"</span><span class="nt">&gt;&lt;/div&gt;</span>
  27. <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"locations"</span><span class="nt">&gt;</span>
  28. <span class="nt">&lt;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">&gt;</span>
  29. <span class="nt">&lt;h2</span> <span class="na">itemprop=</span><span class="s">"name"</span><span class="nt">&gt;</span>Dahlingschule, Städt. Förderschule im integr. Verbund, FSP Lernen u. Emot. und soziale Entwicklung,-Primarstufe u. SekI<span class="nt">&lt;/h2&gt;</span>
  30. <span class="nt">&lt;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">&gt;</span>
  31. <span class="nt">&lt;div</span> <span class="na">itemprop=</span><span class="s">"streetAddress"</span><span class="nt">&gt;</span>Dahlingstr. 40<span class="nt">&lt;/div&gt;</span>
  32. <span class="nt">&lt;span</span> <span class="na">itemprop=</span><span class="s">"postalCode"</span><span class="nt">&gt;</span>47229<span class="nt">&lt;/span&gt;</span>
  33. <span class="nt">&lt;span</span> <span class="na">itemprop=</span><span class="s">"addressLocality"</span><span class="nt">&gt;</span>Duisburg<span class="nt">&lt;/span&gt;</span>
  34. <span class="nt">&lt;/div&gt;</span>
  35. <span class="nt">&lt;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">&gt;</span>
  36. <span class="nt">&lt;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">/&gt;</span>
  37. <span class="nt">&lt;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">/&gt;</span>
  38. <span class="nt">&lt;/div&gt;</span>
  39. <span class="nt">&lt;/li&gt;</span>
  40. <span class="nt">&lt;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">&gt;</span>
  41. <span class="nt">&lt;h2</span> <span class="na">itemprop=</span><span class="s">"name"</span><span class="nt">&gt;</span>Montessori-Gymnasium Städt. Gymnasium für Jungen und Mädchen<span class="nt">&lt;/h2&gt;</span>
  42. <span class="nt">&lt;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">&gt;</span>
  43. <span class="nt">&lt;div</span> <span class="na">itemprop=</span><span class="s">"streetAddress"</span><span class="nt">&gt;</span>Rochusstr. 145<span class="nt">&lt;/div&gt;</span>
  44. <span class="nt">&lt;span</span> <span class="na">itemprop=</span><span class="s">"postalCode"</span><span class="nt">&gt;</span>50827<span class="nt">&lt;/span&gt;</span>
  45. <span class="nt">&lt;span</span> <span class="na">itemprop=</span><span class="s">"addressLocality"</span><span class="nt">&gt;</span>Köln<span class="nt">&lt;/span&gt;</span>
  46. <span class="nt">&lt;/div&gt;</span>
  47. <span class="nt">&lt;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">&gt;</span>
  48. <span class="nt">&lt;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">/&gt;</span>
  49. <span class="nt">&lt;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">/&gt;</span>
  50. <span class="nt">&lt;/div&gt;</span>
  51. <span class="nt">&lt;/li&gt;</span>
  52. <span class="nt">&lt;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">&gt;</span>
  53. <span class="nt">&lt;h2</span> <span class="na">itemprop=</span><span class="s">"name"</span><span class="nt">&gt;</span>Städt. Grillo-Gymnasium<span class="nt">&lt;/h2&gt;</span>
  54. <span class="nt">&lt;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">&gt;</span>
  55. <span class="nt">&lt;div</span> <span class="na">itemprop=</span><span class="s">"streetAddress"</span><span class="nt">&gt;</span>Hauptstr. 60<span class="nt">&lt;/div&gt;</span>
  56. <span class="nt">&lt;span</span> <span class="na">itemprop=</span><span class="s">"postalCode"</span><span class="nt">&gt;</span>45879<span class="nt">&lt;/span&gt;</span>
  57. <span class="nt">&lt;span</span> <span class="na">itemprop=</span><span class="s">"addressLocality"</span><span class="nt">&gt;</span>Gelsenkirchen<span class="nt">&lt;/span&gt;</span>
  58. <span class="nt">&lt;/div&gt;</span>
  59. <span class="nt">&lt;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">&gt;</span>
  60. <span class="nt">&lt;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">/&gt;</span>
  61. <span class="nt">&lt;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">/&gt;</span>
  62. <span class="nt">&lt;/div&gt;</span>
  63. <span class="nt">&lt;/li&gt;</span>
  64. <span class="nt">&lt;/ul&gt;</span>
  65. <span class="nt">&lt;/leaflet-map&gt;</span>
  66. <span class="nt">&lt;style&gt;</span>
  67. <span class="nf">#map</span> <span class="p">{</span>
  68. <span class="nl">width</span><span class="p">:</span> <span class="m">800px</span><span class="p">;</span>
  69. <span class="nl">height</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
  70. <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>
  71. <span class="nl">background-image</span><span class="p">:</span> <span class="sx">url("/map.png")</span><span class="p">;</span>
  72. <span class="nl">background-size</span><span class="p">:</span> <span class="n">contain</span><span class="p">;</span>
  73. <span class="p">}</span>
  74. <span class="nt">&lt;/style&gt;</span>
  75. </code></pre></div></div>
  76. <p>In this form, this isn’t interactive yet but it can already be easily consumed by both humans
  77. and computers. Humans will see a static map (I simply took a screenshot and set is as a
  78. background image for the <code class="highlighter-rouge">#map</code> node) and a list of schools with their addresses.
  79. Computers will be able to also parse the schema.org annotations to extract structured data out of the list.</p>
  80. <p>Since the data is semi-structured and machine readable, we can now also consume it from
  81. a Web Component to add interactivity with <a href="https://leafletjs.com">Leaflet</a>. The code to do so
  82. looks like this:</p>
  83. <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>
  84. <span class="nx">connectedCallback</span><span class="p">()</span> <span class="p">{</span>
  85. <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>
  86. <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>
  87. <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>
  88. <span class="s1">'[itemType="https://schema.org/School"]'</span><span class="p">,</span>
  89. <span class="p">);</span>
  90. <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>
  91. <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>
  92. <span class="na">attribution</span><span class="p">:</span>
  93. <span class="s1">'&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright"&gt;OpenStreetMap&lt;/a&gt; contributors'</span><span class="p">,</span>
  94. <span class="p">}).</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
  95. <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>
  96. <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">=&gt;</span> <span class="p">{</span>
  97. <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>
  98. <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>
  99. <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>
  100. <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>
  101. <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>
  102. <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>
  103. <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>
  104. <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>
  105. <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>
  106. <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>
  107. <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>
  108. <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>
  109. <span class="p">});</span>
  110. <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>
  111. <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>
  112. <span class="p">}</span>
  113. <span class="p">}</span>
  114. <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>
  115. </code></pre></div></div>
  116. <p>I quite like the way that this reads. The <code class="highlighter-rouge">itemProp</code>s make for very nice selectors and actually
  117. allow developers to completely change the structure of the HTML. As long as the annotations
  118. are kept, the Web Component will be able to still extract the locations and to load them into
  119. leaflet.</p>
  120. <p>You can try this on CodePen:</p>
  121. <p>This of course is nowhere from production ready but shall only serve as a proof of concept.
  122. In a more advanced version, we wouldn’t limit our initial query selector to <code class="highlighter-rouge">[itemType="https://schema.org/School]</code>
  123. 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>.
  124. We would also need to adapt the static image if we wanted to change the underlying data. If we have server
  125. side rendering at our disposal, we could use something like the <a href="https://docs.mapbox.com/api/maps/static-images/">Mapbox Static Images
  126. API</a> to dynamically create the fallback
  127. images for clients that do not support JavaScript or Web Components.</p>
  128. <p>All in all, this feels like a nice approach to me though. It provides content
  129. that can be consumed by computers and humans with up to date or legacy (or privacy concious)
  130. devices easily. Using the <code class="highlighter-rouge">itemProp</code>s for query selectors also makes for a nice
  131. authoring experience with good separation of concerns.</p>
  132. <p>What do you think? Let me know on <a href="https://berlin.social/@knut">mastodon</a>.</p>