PeterGoes.nl Blog My personal blog on the web 2023-03-04T20:03:29Z https://www.petergoes.nl/blog Peter Goes https://www.gravatar.com/avatar/12af933042ed4582a78971f13cdbbd57?size=300 Updates for 02-2023 2023-03-04T20:03:29Z https://www.petergoes.nl/blog/updates-for-02-2023/ <aside><tt>It's a secret to everyone! This post is for RSS subscribers only. <a href="https://daverupert.com/rss-club/">Read more about RSS Club</a></tt></aside> <p>## Personal stuff</p> <p>### The road to 7a (bouldering)</p> <p>I really want to nail the 7a this year. Turns out, my bouldering gym offers personal training. 3 sessions of an hour. After 2 sessions, I am even more excited. It sure is more difficult to climb when you have to think about what you do, but I feel I am making progress. 10/10 would recommend.</p> <p>### Going to concerts again!</p> <p>Having kids (6 and 3 years old) makes sure that time is scarce. I thought that going to concerts was a thing of the past, especially after the pandemic. I am not subscribed to newsletters of venues anymore, so I miss quite a lot of opportunities as well. A colleague of mine does not. Lucky me!</p> <p>After a couple of concerts now, I could not be more happy to stand in a closed space with to much people around listening to really hard music again! Man, did I missed this!</p> <p>## Work stuff</p> <p>### Frontend Masters subscription</p> <p>I decided to sign up for Frontend Masters with my education budget this year. It is awesome. The learning paths that they offer are great and the content is of such a high level.</p> <p>I was looking for good courses specifically on CSS. Great material is out there for JavaScript, but I really want to step up my CSS game. Although Frontend Masters is more JavaScript focussed, there are also quite some CSS courses. When I asked about good courses on Mastodon recently, I got some other good examples, so I will spend a good amount of time learning things the coming year!</p> <p>### Mixed feelings about the ranting on React</p> <p>Ranting about React and JavaScript frameworks is in full swing I my social network circles. While I consider myself on the progressive enhancement / use as little JS as possible side of The Divide, I do use Preact daily at work.</p> <p>I feel personally attacked by all the ranting and Keith J Grant formulated my feelings exactly in his post <a href="https://keithjgrant.com/posts/2023/01/react-pays-the-bills/">React Pays the Bills</a></p> <p>That is to say, now that an opportunity has presented itself to reevaluate our current Front-end stack. I am thinking hard about what a framework-less front-end setup of a large website would look like.</p> Looking at user stats increase my emotional connection with them 2023-03-04T14:06:42Z https://www.petergoes.nl/blog/looking-at-user-stats-increase-my-emotional-connection-with-them/ <p>It seems that the capabilities of CSS grow exponentially fast the last few years. With the <a href="https://web.dev/interop-2022/">iterop</a> initiative making sure that features are consistent across browsers, life as a Front-end Dev is quite awesome.</p> <p>Usually when I want to play with all these new toys, I look at <a href="https://caniuse.com/">Can I Use</a> to see if I can actually use them. But now that we are starting to build a complete design system from scratch, I want to know if I could use features like container queries or the <code>:has()</code> selector. It could, potentially, reduce the complexity of our components quite drastically.</p> <h2 id="looking-at-statistics-from-our-actual-users" tabindex="-1">Looking at statistics from our actual users</h2> <p>I keep a close eye to the statistics from our analytics tool to see what browsers our readers use. However, we are very privacy conscious. We use a privacy friendly analytics tool (Matomo), respect the do not track header, track only the data we actually need. That does mean, however, that we don’t know a lot about our users. The section ‘unknown’ in the browser engine section is quite large.</p> <p>To actually make more informed decisions, I wrote a small script that test CSS features I care about and track those.<br /> Now I have actual data from our users. But then I noticed something when I looked at the data.</p> <h2 id="i-care-much-more-about-stats-from-our-actual-users-than-nation-wide-stats" tabindex="-1">I care much more about stats from our actual users than nation wide stats</h2> <p>When I was primarily looking at Can I Use, there was no emotional connection. 85% of users did have Grid support? Great, start using it. Maybe not on critical things, so those 15% that do not have it do not suffer that much.</p> <p>But now, I have stats like: 91% of our members browsers support the <code>:has()</code> selector. That means 9% of our members, which pay my salary, do not. All of a sudden that is quite a substantial amount!</p> <p>I find it an even more exiting challenge to provide a base line which I can then progressively enhance. Because these stats remind me that our users are people, not a number.</p> Ditching the car turned out to be a very good idea 2023-01-29T17:05:01Z https://www.petergoes.nl/blog/ditching-the-car-turned-out-to-be-a-very-good-idea/ <p>We just hit 1500km on our cargo bike. After our car broke down in the summer of 2022 we decided to not replace it. Instead we got an electric cargo bike and a subscription for a car share service. It still felt like a challenge with two kids (2 and 5 years old at the time).</p> <p>After just two months since we got the cargo bike, <a href="https://social.petergoes.nl/@petergoes/109570485828877202">we reached the first 1000km</a> (December 24 2022). Today we reached 1500km.</p> <p><img src="https://res.cloudinary.com/https-www-petergoes-nl/image/upload/v1675021292/photo_2023-01-29_20.37.57_1_cm66ua.jpg" alt="Cargo bike computer indicating total distance traveled is 1500km in dutch" title="1500km total distance" loading="lazy" /></p> <p>The car share subscription feels like a cheat code. I don’t like to rent a car through it, but we do so from time to time.<br /> We do use the cargo bike every single day though. That feels great. We started out with the environment in mind, reducing our footprint. But the real benefit comes from some place else.</p> <p>We have so much more quality time with each other as a family. It takes longer to travel, sure. We have to plan ahead and we can not do everything we did when we had a car before. In the weekends, we used to go out quite a lot. Now that we have to calculate bike time, we are limited in what we are able to do. But we gain time with each other.</p> <p>The conversations we have when the kids are asleep in the bike are on another level from those we had when we sat in a car. Being outside does something with your mind. You are more relaxed.</p> <p>But when the kids are awake, they get to explore the world around them much better. Because the bike is slower, the can see and hear much more.</p> <p>I was a bit skeptical at first for the winter period. The dutch weather is quite wet and not all that pleasant to ride in. But the way it turns out? I can not wait until summer.</p> <p>If the previous months are any indication, the summer of 2023 will be amazing.</p> A few updates for 01-2023 2023-01-27T19:50:21Z https://www.petergoes.nl/blog/a-few-updates-for-01-2023/ <aside><tt>It's a secret to everyone! This post is for RSS subscribers only. <a href="https://daverupert.com/rss-club/">Read more about RSS Club</a></tt></aside> <p>Maybe a summary of the previous month is a good way to get in the habit of blogging more? Lets find out…</p> <p>## Personal stuff</p> <p>### The road to a 7a (bouldering)</p> <p>I don’t make new year resolutions. I am notoriously bad in keeping those kind of promises to myself. But this year I did set a goal for myself. Taking bouldering really serious again and being able to climb a 7a (V6 on V-scale?). It is an ambitious goal for sure, but I really like it!</p> <p>Yesterday I did my first 6b, so progress is being made.</p> <p>### Struggling with my own website</p> <p>I really like having my own website. I like the promise of having a true place of my own that I can experiment with. But I find myself in a place where I want to have a proper looking website because I am a front-end developer. Having a broken website looks bad I guess? That fear of having a broken website and what people might think of me as a front-end dev as a result prevents me from having that hacker mindset. The thing that makes having a personal website fun in the first place.</p> <p>The same goes for blogging, I am really happy that I found the RSS Club, because this post is not ‘published’, it makes it safer to fool around. I want that same feeling again for the site as a whole.</p> <p>On a related note, I am really in the market for a good combination of writing markdown with a tool like IA Writer and One Click Publish convenience of a CMS…</p> <p>## Work related stuff</p> <p>### Arc Browser</p> <p>Since a couple of weeks I switched to Arc Browser. And MAN, does that thing live up to the hype! I am SO happy to have a browser that is primarily keyboard driven. I almost never have the side panel open so I am looking at each page in all of its glory in an almost full screen view. I absolutely love it. And Split Mode is awesome!</p> <p>### Learning with Remix</p> <p>I am in the process of rebuilding the whole front-end of our codebase at De Correspondent with Remix. At first I was sceptical about a React driven framework, but the focus on foundational web tech is amazing. I truly think I am becoming a better front-end developer just by using this framework.</p> <p>For example, yesterday I needed to throw an Error. It did not behave exactly as I expected, so I asked around in the discord server, and someone pointed me to the MDN page of the Error object. I learned a thing or two about the <code>cause</code> option.</p> <p>Love it.</p> I got genuenly exited to find out about a secret society! 2023-01-10T21:41:23Z https://www.petergoes.nl/blog/i-got-genuenly-exited-to-find-out-about-a-secret-society/ <aside><tt>It's a secret to everyone! This post is for RSS subscribers only. <a href="https://daverupert.com/rss-club/">Read more about RSS Club</a></tt></aside> <p>I love the web. The openness, the expressiveness. The way it lets me fiddle with code and make things come alive. After the Twitter collapse I joined Mastodon, and the whole front-end community with me (or so it seems). With that I regaind an appreciation for blogs and RSS. So when I found out about this, I could not be happier. It feels like the old days of the web in a sense that it you get to access things that other people don’t know about, while working out in the open. It is weird to describe, I love to make stuff available for an as wide as possible audience, but this is somehow different? I don’t know.</p> <p>I make myself the promise (again, I know) to pick up blogging. Only this time to allow for more personal things as well. Maybe that makes it more easy. So to kick that off: I promised myself I can boulder a 7a by the end of 2023. Is that a reasonable goal? I don’t know, we’ll see :). I will keep you posted!</p> The Set-Cookie and Cookie headers got me confused, but I think I get it now 2022-12-14T19:56:05Z https://www.petergoes.nl/blog/the-set-cookie-and-cookie-headers-got-me-confused-but-i-think-i-get-it-now/ <p>I learned a thing or two about cookies today… It took me quite a while to figure it out, but I think I understand it now.</p> <p>I am rebuilding a website from an old PHP / Twig setup into something more modern. The PHP backend is still in use, but I wanted to separate the front-end and rebuild that with Remix. The backend gets slowly transferred into an API only backend that my Remix server is going to communicate with.</p> <p>Authentication is done via cookies, but since I now have a Remix server between the client and the API, I need to perform the requests to the backend with the session cookies from the user. Having the Remix server in between allows me to do a couple of requests to the API without going all the way back to the client for each response. But that means getting and sending cookies along as well.</p> <p>The problem I faced was this: When I create a user, I also need to store the country that user lives in. For the user, that is one form to will out: <code>username</code>, <code>password</code>, <code>countryCode</code>. But the backend wants to store the <code>countryCode</code> via a separate request. So first create the user, then update that user via an other endpoint with the country code. But only an authenticated user can update their country code.</p> <p>I needed to get the session cookie from the <code>createUser</code> endpoint, and provide that to the <code>updateCountry</code> endpoint right after.</p> <h2 id="set-cookie-and-cookie-are-two-different-things" tabindex="-1"><code>Set-Cookie</code> and <code>Cookie</code> are two different things</h2> <p>It took me a while before I realised that the server <strong>sends</strong> a <code>Set-Cookie</code> header to the browser, but <strong>expects</strong> a <code>Cookie</code> header when receiving.</p> <p>When I figured that out, it was a matter of getting the <code>Set-Cookie</code> header, and passing it as <code>Cookie</code>, right? No, that did not work at all.</p> <p>Looking at the original requests from the legacy codebase, each request to the backend has a <code>Cookie</code> <em>request</em> header with some information (<code>session=s3ss10n1d</code> for example). But multiple cookies can be stored for a domain, so the value can also be: <code>session=s3ss10n1d; setting=theme-blue</code>. Right, a header, cookies split via <code>; </code>. I get that.</p> <p>Looking at the <em>response</em> headers though, there were multiple <code>Set-Cookie</code> headers. One for each cookie, each of that with the following shape: <code>cookieName=cookieValue; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.some-site.com; secure; HttpOnly; SameSite=Lax</code>. Ok, the cookie has a name / value combo and some attributes separated by a <code>; </code>. I get that as well.</p> <h3 id="reading-response-headers" tabindex="-1">Reading response headers</h3> <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response">Response</a> contains a <code>headers</code> property that is an instance of <a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers">Header</a>. You can use it to query values of response headers via <code>header.get()</code>. From the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers/get">MDN documentation</a>:</p> <blockquote> <p>If the header has multiple values associated with it, the byte string will contain all the values, in the order they were added to the Headers object</p> </blockquote> <p>The code example with it:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block">myHeaders.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;Accept-Encoding&#x27;</span>); <span class="hljs-comment">// Returns &quot;deflate, gzip&quot;</span><br />myHeaders.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;Accept-Encoding&#x27;</span>).<span class="hljs-title function_">split</span>(<span class="hljs-string">&#x27;,&#x27;</span>).<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">v</span>) =&gt;</span> v.<span class="hljs-title function_">trimStart</span>()); <span class="hljs-comment">// Returns [ &quot;deflate&quot;, &quot;gzip&quot; ]</span></code></pre> <p>That does not seem so hard… Let’s have one more close look at the <code>Set-Cookie</code> value: <code>cookieName=cookieValue; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.some-site.com; secure; HttpOnly; SameSite=Lax</code>. Please notice the <code>expires</code> value: <code>Thu, 01-jan...</code>. That <code>,</code> right there is the sole reason a good amount of hours got wasted. There is no way to tell where to split the string without intimate knowledge of all possible attributes for the cookie…</p> <p>Parsing the <code>Set-Cookie</code> header with a library finally solved the problem I had.</p> <h2 id="lessons-learned" tabindex="-1">Lessons learned</h2> <ol> <li>A server <strong>sends</strong> multiple <code>Set-Cookie</code> headers where each header contains a cookie with additional attributes, split by <code>; </code>.</li> <li>A server <strong>expects</strong> a single <code>Cookie</code> header where each cookie is represented via <code>name=value</code> split by a <code>; </code>.</li> <li>For the love of all that is dear to you, use some libraries for dealing with the <code>Set-Cookie</code> header. I ended up using <a href="https://github.com/jshttp/cookie">cookie</a> and (more importantly) <a href="https://github.com/nfriedly/set-cookie-parser">set-cookie-parser</a>.</li> </ol> Should a website provide configurable accessibility options to users? 2021-06-16T09:33:35Z https://www.petergoes.nl/blog/should-a-website-provide-configurable-accessibility-options-to-users/ <h2 id="update-about-the-used-example-in-this-post" tabindex="-1">Update about the used example in this post</h2> <p>Today (17th of June 2021) I stumbled upon a <a href="https://css-tricks.com/a-complete-guide-to-css-media-queries/#accessibility">CSS Tricks article</a> which states that you CAN detect if a user has their colours inverted system wide. Kicking over the whole foundation of the examples I provide in this post. The original problem that I had is thus solved (use the <code>@media (inverted-colors)</code> media query). If you still want to read the original post, let’s get going.</p> <hr /> <p>Today I did an interview for <a href="https://toegankelijkheidsonderzoek.petergoes.nl/">my accessibility research (in Dutch, Chrome translates it well)</a>. The person I spoke with used the system setting to invert all colours, due to hyper sensitivity to light. The result was that images were also inverted and thus became negative:</p> <p><img src="https://res.cloudinary.com/https-www-petergoes-nl/image/upload/c_scale,q_auto:eco,w_1472/v1623837139/side-by-side_mkyyao.png" alt="Screenshot of a Google Images search for cats. Split in half where the first half is normal. In the second half, the colours are inverted" loading="lazy" /></p> <p>Here you can compare how inverted colours look on macOS. The left side shows a Google Images search without inverted colours. In the right side the colours are inverted. The images then become negatives. Making them really hard to interpret.</p> <p>She uses social media on a regular basis. Seeing all images and videos in their negative form, takes the fun out of using it. When an image or video contains crucial information, it becomes a bigger problem.</p> <p>On iOS there is the option to “smart invert” the colours. Meaning, all colours are inverted, except for images and video. That is the golden solution in this scenario, but I did not find similar “smart” colour invert options in macOS, Android or Windows.</p> <p>As a developer, I can provide an option to invert the colours of an image first, and when the operating system inverts all the colours on screen, my inverted image is again inverted, making it appear normal again. Like this:</p> <p><img src="https://res.cloudinary.com/https-www-petergoes-nl/image/upload/c_scale,q_auto:eco,w_1472/v1623837356/side-by-side-2_lx0mfj.png" alt="The same screenshot as before, only this time, the colour of the images on the page are inverted. This results in the second half, where all colours are inverted except for the images" loading="lazy" /></p> <p>This time, an <code>invert()</code> filter is applied within the website itself. Again, the left side shows a ‘normal’ view but with inverted colours. The right side shows the inverted version. But because the images were already inverted, they appear normal again.</p> <h2 id="you-can%E2%80%99t-(rightfully)-detect-if-accessibility-features-are-enabled" tabindex="-1">You can’t (rightfully) detect if accessibility features are enabled</h2> <p>The developer of a website can not find out if certain accessibility features are turned on or not. Not only are these settings handled system wide, from a privacy perspective a user might have good reasons not to disclose to every website they required these options.</p> <p>If I want to allow the users to browse the site with inverted images, so that the operating system can reinvent them, I need to provide a UI for that. A accessibility settings menu if you will. That in turn requires the user to a) find it, and b) actively turn it on.</p> <h2 id="questions-to-answer" tabindex="-1">Questions to answer</h2> <p>All this considered, I have a couple of questions to answer:</p> <h3 id="do-users-want-this-level-of-customisation%3F" tabindex="-1">Do users want this level of customisation?</h3> <p>I get fed up with all the cookie consent notices for every single website. I can imagine someone does not want to search for settings on every single website they visit.</p> <p>But for an app or a site you use every day, you might want to go through that trouble.</p> <h3 id="how-does-a-user-distinguish-between-this-menu-and-other-bad-actors%3F" tabindex="-1">How does a user distinguish between this menu and other bad actors?</h3> <p>Looking at the <a href="https://overlayfactsheet.com/">Overlay Factsheet</a>, other attempts to ‘fix accessibility’ are not that well received. Although I don’t think this menu is comparable to these tools, It is vital that a user understands that as well.</p> <h3 id="how-do-i-make-sure-that-this-does-not-become-a-tracking-data-point%3F" tabindex="-1">How do I make sure that this does not become a tracking data point?</h3> <p>You don’t want to reapply the same setting every time you visit a site or app. Persisting the setting for the user is important. But that will (potentially) allow others to find out these settings have been set. Which violates the users privacy.</p> <h2 id="do-you-have-any-answers%3F" tabindex="-1">Do you have any answers?</h2> <p>Did I overlook important details? Are there existing solutions out there? Do you have an other opinion? I like to know about it!</p> Review incoming WebMentions before publishing with Github Actions 2021-05-26T19:10:17Z https://www.petergoes.nl/blog/review-webmentions-before-publishing-with-github-actions/ <p>Recently I added <a href="https://indieweb.org/webmention">webmentions</a> to this blog.<br /> Which is completely static. It is build with <a href="https://11ty.dev/">Eleventy</a>,<br /> and hosted on <a href="https://www.petergoes.nl/blog/review-webmentions-before-publishing-with-github-actions/www.netlify.com">Netlify</a>.</p> <h2 id="accepting-webmentions-in-the-first-place" tabindex="-1">Accepting Webmentions in the first place</h2> <p>Accepting webmentions for a static site is relatively straight forward. I<br /> followed the article <a href="https://sia.codes/posts/webmentions-eleventy-in-depth/">An In-Depth Tutorial of Webmentions + Eleventy</a><br /> to do so. And was quite happy with the results.</p> <p>But there are a few things I did not liked about it:</p> <ol> <li>I want incomming mentions to be published as soon as possible, without the<br /> need for client-side JavaScript. Something needs to trigger the build.</li> <li>I want to be in full control of the content placed on this site. Accepting<br /> webmentions is awesome, but I want to be able moderate them.</li> </ol> <h2 id="using-a-github-action-to-create-a-pull-request-with-new-mentions" tabindex="-1">Using a Github Action to create a Pull Request with new mentions</h2> <p>I use <a href="https://webmention.io/">webmention.io</a> to receive my mentions. The service<br /> comes with an API with which I can retrieve them.</p> <p>I created a Github Action to periodically fetch my mentions and write each of<br /> them to a file.</p> <p>When the Github Action runs, it should:</p> <ol> <li>Fetch my web mentions from the API</li> <li>Store each of them in a seperate json file</li> <li>Create a Pull Request with the new and changed files</li> </ol> <h3 id="fetching-and-storing-new-web-mentions" tabindex="-1">Fetching and storing new Web Mentions</h3> <p>The (trimmed down version of the) fetching and storing logic comes down to this:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">const</span> mentionsFolder = <span class="hljs-string">&#x27;./mentions&#x27;</span><br /><br /><span class="hljs-title function_">fetch</span>(<span class="hljs-string">`https://webmention.io/api/mentions.jf2?token=&lt;MY-TOKEN&gt;&amp;per-page=10000`</span>)<br /> .<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> response.<span class="hljs-title function_">json</span>())<br /> .<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> data.<span class="hljs-property">children</span>)<br /> .<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">mentions</span> =&gt;</span> mentions.<span class="hljs-title function_">forEach</span>(writeMentionToFile))<br /><br /><span class="hljs-keyword">function</span> <span class="hljs-title function_">writeMentionToFile</span>(<span class="hljs-params">mention</span>) {<br /> <span class="hljs-keyword">const</span> id = mention[<span class="hljs-string">&#x27;wm-id&#x27;</span>]<br /> <span class="hljs-keyword">const</span> targetPostPath = mention[<span class="hljs-string">&#x27;wm-target&#x27;</span>].<span class="hljs-title function_">replace</span>(<span class="hljs-string">&#x27;https://www.petergoes.nl&#x27;</span>, <span class="hljs-string">&#x27;&#x27;</span>)<br /> <span class="hljs-keyword">const</span> outputFolder = path.<span class="hljs-title function_">join</span>(mentionsFolder, targetPostPath)<br /><br /> fs.<span class="hljs-title function_">writeFileSync</span>(<br /> path.<span class="hljs-title function_">join</span>(outputFolder, <span class="hljs-string">`<span class="hljs-subst">${id}</span>.json`</span>),<br /> <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(mention, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),<br /> { <span class="hljs-attr">encoding</span>: <span class="hljs-string">&#x27;utf-8&#x27;</span> }<br /> )<br />}</code></pre> <p>I left out some checks here and there.<br /> This is the full <a href="https://github.com/petergoes/petergoes.nl/blob/main/_automations/store-webmentions.mjs">store-webmentions.mjs</a><br /> file.</p> <p>Any webmention is stored as a json file in the <code>mentions/</code> folder. If a mention exists, it gets overwritten with the new content. Updating<br /> that mention.</p> <h3 id="the-github-action" tabindex="-1">The Github Action</h3> <p>Lets look at the actions .yml file.</p> <pre class="language-yaml hljs-block"><code class="language-yaml hljs-block"><span class="hljs-attr">name:</span> <span class="hljs-string">Webmentions</span><br /><br /><span class="hljs-attr">on:</span><br /> <span class="hljs-attr">schedule:</span><br /> <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">&quot;0 0 * * *&quot;</span><br /><br /><span class="hljs-attr">jobs:</span><br /> <span class="hljs-attr">webmentions:</span><br /> <span class="hljs-attr">steps:</span><br /> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Check</span> <span class="hljs-string">out</span> <span class="hljs-string">repository</span><br /> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@master</span><br /><br /> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Node.js</span><br /> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@master</span><br /><br /> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span><br /> <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span><br /><br /> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Fetch</span> <span class="hljs-string">webmentions</span><br /> <span class="hljs-attr">env:</span><br /> <span class="hljs-attr">WEBMENTION_IO_TOKEN:</span> <span class="hljs-string">&lt;&lt;WEBMENTION_IO_TOKEN&gt;&gt;</span><br /> <span class="hljs-attr">run:</span> <span class="hljs-string">node</span> <span class="hljs-string">./_automations/store-webmentions.mjs</span><br /><br /> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Create</span> <span class="hljs-string">Pull</span> <span class="hljs-string">Request</span><br /> <span class="hljs-attr">id:</span> <span class="hljs-string">cpr</span><br /> <span class="hljs-attr">uses:</span> <span class="hljs-string">peter-evans/create-pull-request@v3</span><br /> <span class="hljs-attr">with:</span><br /> <span class="hljs-attr">token:</span> <span class="hljs-string">&lt;&lt;GITHUB_TOKEN&gt;&gt;</span><br /> <span class="hljs-attr">commit-message:</span> <span class="hljs-string">Update</span> <span class="hljs-string">WebMentions</span><br /> <span class="hljs-attr">assignees:</span> <span class="hljs-string">petergoes</span><br /> <span class="hljs-attr">title:</span> <span class="hljs-string">Update</span> <span class="hljs-string">Webmentions</span><br /> <span class="hljs-attr">body:</span> <span class="hljs-string">Update</span> <span class="hljs-string">Webmentions</span></code></pre> <p>This is the <a href="https://github.com/petergoes/petergoes.nl/blob/main/.github/workflows/webmentions.yml">full webmentions.yml file</a>.</p> <p>I set the schedule to run the action every day at midnight. New mentions are<br /> automatically pulled in at least once a day.</p> <p>When there are new mentions, the <a href="https://github.com/peter-evans/create-pull-request">Create Pull Request</a><br /> action commits these changes and generates a Pull Request. If there are no new<br /> mentions, it does nothing.</p> <p>This works fine, but we can take it one step further.</p> <h2 id="run-the-github-action-when-a-new-mention-is-received" tabindex="-1">Run the Github Action when a new mention is received</h2> <p>Instead of doing this once a day, it would be nice if this action runs anytime<br /> a new mention is posted. The <a href="http://webmention.io/">webmention.io</a> service provides the option to call a<br /> webhook when a new mention is posted. Nice!</p> <p>Github Actions can be configured so that they are triggerd via a webhook. Sounds like<br /> a match made in heaven.</p> <p>To set this up, I followed the guide <a href="https://mainawycliffe.dev/blog/github-actions-trigger-via-webhooks">GitHub Actions Trigger Via Webhooks</a>.</p> <p>Lets first configure the Github Action to run when a webhook is called:</p> <pre class="language-diff hljs-block"><code class="language-diff hljs-block"> on:<br /><span class="hljs-addition">+ repository_dispatch:</span><br /><span class="hljs-addition">+ types: [Received Webmention]</span><br /> schedule:<br /> - cron: &quot;0 0 * * *&quot;</code></pre> <p>To run the action, I need to make a POST request to: <code>https://api.github.com/repos/petergoes/petergoes.nl/dispatches</code>.<br /> The body of the request needs to contain a type I defined in the .yml file, and<br /> I have to include a token in the <code>Authorization</code> header.</p> <p>The full request looks like this:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-title function_">fetch</span>(<br /> <span class="hljs-string">&#x27;https://api.github.com/repos/petergoes/petergoes.nl/dispatches&#x27;</span>,<br /> {<br /> <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;POST&#x27;</span>,<br /> <span class="hljs-attr">headers</span>: {<br /> <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`token &lt;GITHUB_TOKEN&gt;`</span><br /> },<br /> <span class="hljs-attr">body</span>: <span class="hljs-string">&#x27;{ &quot;event_type&quot;: &quot;Received Webmention&quot; }&#x27;</span><br /> }<br />)</code></pre> <p>That does not match the data structure I get from the <a href="http://webmentions.io/">webmentions.io</a> webhook…</p> <h3 id="use-a-netlify-function-to-call-the-github-action" tabindex="-1">Use a Netlify function to call the Github Action</h3> <p>The last piece of the puzzel is a Netlify function which receives the webhook<br /> request from <a href="http://webmention.io/">webmention.io</a>, when that is valid, it calls the endpoint on Github<br /> to trigger the Github Action</p> <p>The Netlify function looks like this:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">const</span> fetch = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;node-fetch&#x27;</span>)<br /><br /><span class="hljs-built_in">exports</span>.<span class="hljs-property">handler</span> = <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> (<span class="hljs-params">event, context</span>) {<br /><br /> <span class="hljs-comment">// The payload from webmention.io is configured to contain a secret</span><br /> <span class="hljs-keyword">const</span> { secret } = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(event.<span class="hljs-property">body</span>)<br /><br /> <span class="hljs-comment">// Make sure that it is known to the Netlify Function so no one else can call</span><br /> <span class="hljs-comment">// this function</span><br /> <span class="hljs-keyword">if</span> (secret !== process.<span class="hljs-property">env</span>.<span class="hljs-property">WEBMENTION_IO_SECRET</span>) {<br /> <span class="hljs-keyword">return</span> { <span class="hljs-attr">statusCode</span>: <span class="hljs-number">401</span> }<br /> }<br /><br /> <span class="hljs-comment">// Trigger the Github Action</span><br /> <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<br /> <span class="hljs-string">&#x27;https://api.github.com/repos/petergoes/petergoes.nl/dispatches&#x27;</span>,<br /> {<br /> <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;POST&#x27;</span>,<br /> <span class="hljs-attr">headers</span>: {<br /> <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`token &lt;GITHUB_TOKEN&gt;`</span><br /> },<br /> <span class="hljs-attr">body</span>: <span class="hljs-string">&#x27;{ &quot;event_type&quot;: &quot;Received Webmention&quot; }&#x27;</span><br /> }<br /> )<br /><br /> <span class="hljs-keyword">return</span> { <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>, <span class="hljs-attr">body</span>: <span class="hljs-string">&#x27;ok&#x27;</span> }<br />}</code></pre> <p>See <a href="https://github.com/petergoes/petergoes.nl/blob/main/.netlify/functions/trigger-pr-workflow.js">the full Netlify Function</a> here</p> <p>And thats it! That are a lot of steps. But when you post a webmention, or like the<br /> tweet on Twitter, you should see a PR in the <a href="https://github.com/petergoes/petergoes.nl/pulls">Pull Request section on GitHub</a><br /> in a few minutes!</p> With great power comes great responsibility 2021-05-15T10:00:39Z https://www.petergoes.nl/blog/with-great-power-comes-great-responsibility/ <p>When you build a UI for a product (app, website, anything else), it is intended to be used by people. People come in all shapes, sizes and abilities. I used to think that making an UI accessible is something you should aim for. It was morally the right thing to do.</p> <p>Recently I started out to become better at making my work better accessible by doing research into people that use assistive technology. You can <a href="https://toegankelijkheidsonderzoek.petergoes.nl/">follow along with my research here</a> (in Dutch for now). In the first interview, it became clear to me.</p> <h2 id="we-have-the-power-to-influence-other-peoples-lives" tabindex="-1">We have the power to influence other peoples lives</h2> <p>When building a product, we allow people to do their jobs. We build the interface through which others do their work.</p> <p>We can add a feature that makes someone perform a task more efficiently. The less time it takes to use our UI, the more effective that person can be in their job.</p> <p>But the other way around is equally true. If we make it harder for someone to use our product, we make them less effective.</p> <h2 id="accessibility-is-not-a-nice-to-have%2C-it-is-your-responsibility" tabindex="-1">Accessibility is not a nice to have, it is your responsibility</h2> <p>Making your UI accessible, is allowing someone who relies on assistive technology to work <strong>at all</strong>.</p> <p>By not taking care of accessibility, a team takes away the self-sustainability of a person. It prevents them from having a job.</p> <p>We as designers/developers have the power to make, or break, someones career. Thats a lot of power, and an equal amount of responsibility.</p> Binding behaviour to HTML with Web Components as Progressive Enhancement 2021-04-28T17:48:36Z https://www.petergoes.nl/blog/binding-behaviour-to-html-with-web-components-as-progressive-enhancement/ <p>For a couple of years now, I have been working with front-end frameworks almost exclusively. At <a href="https://www.voorhoede.nl/">De Voorhoede</a>, we adopted the componentised way of working early on. We moved from binding JavaScript to the DOM via <code>data-*</code> attributes on to using Angular.JS and from there to React and Vue.</p> <p>Each step of the way felt like a great step forwards. The concept of components spoke to me because I could focus on a single feature and have all relevant parts close by. The HTML, CSS and JavaScript moved closer and closer together, making it easier to work on isolated features / UI elements. The tooling became better and better. For the bigger projects that we do, this way of working fits us good.</p> <p>But the downside is that it requires a lot of build steps, and results in pretty heavy JavaScript bundles.</p> <p>For smaller (or simpeler) sites, that do not require a lot of JavaScript (like this website), a large Front-end framework is overkill. The question becomes again: “How do you manage JavaScript when you use a HTML templating language?”</p> <p>The folks over at <a href="https://grrr.nl/">Grrr</a> (checkout <a href="https://grrr.tech/">Grrr’s tech blog</a>!), have released<br /> <a href="https://github.com/grrr-amsterdam/hansel">Hansel</a> to deal with this. I have used that for a couple of projects, and I like the philosophy behind it. But for this website, I wanted a build step free developer experience. By using a small set of features of the Custom Elements spec, I think I hit the sweet spot.</p> <h2 id="componentised-ui-with-a-html-templating-engine" tabindex="-1">Componentised UI with a HTML templating engine</h2> <p>To explain the concept, I’ll use the <a href="https://github.com/petergoes/petergoes.nl/tree/main/_includes/components/tag-list"><code>tag-list</code> component</a> from the <a href="https://www.petergoes.nl/bookmarks">/bookmarks</a> page. On this page, I’ll store bookmarks, which have <em>tags</em> on them. The tags are not yet displayed when I write this though.</p> <p>Besides the big list of all bookmarks, I wanted a way to filter them by tag. Eleventy gave me a hard time in doing this during build time. Because of that, I wanted to do it with JavaScript.</p> <p>First, lets look at the template side:</p> <pre class="language-twig hljs-block"><code class="language-twig hljs-block"><span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">my-tag-list</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;tag-list&quot;</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">details</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">summary</span>&gt;</span>All tags (</span><span class="hljs-template-variable">{{ tags.length }}</span><span class="language-xml">)<span class="hljs-tag">&lt;/<span class="hljs-name">summary</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;tag-list__list&quot;</span>&gt;</span><br /> </span><span class="hljs-template-tag">{%</span> <span class="hljs-name">for</span> tag <span class="hljs-keyword">in</span> tags <span class="hljs-template-tag">%}</span><span class="language-xml"><br /> <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;tag-list__item&quot;</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span><br /> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;tag&quot;</span><br /> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;/bookmarks/tags/</span></span></span><span class="hljs-template-variable">{{ tag.slug | slug }}</span><span class="language-xml"><span class="hljs-tag"><span class="hljs-string">/&quot;</span><br /> &gt;</span><br /> </span><span class="hljs-template-variable">{{ tag.label }}</span><span class="language-xml"><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span><br /> </span><span class="hljs-template-tag">{%</span> <span class="hljs-name">endfor</span> <span class="hljs-template-tag">%}</span><span class="language-xml"><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">details</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">my-tag-list</span>&gt;</span></span></code></pre> <p>A <code>details</code>/<code>summary</code> combination is used to show / hide the tags. Nunjucks templating is used to render the tags. For each tag, a link to an overview page of that tag is rendered.</p> <p>But to see anything when you clink on that link, you’ll have to have JavaScript enabled. Not only enabled, but it should be run in a browser which can run ES6. Because the page <code>/bookmarks/tags/:tagName</code> requirers it.</p> <p>I want to hide the whole tag list from users that do not have JavaScript enabled, but display it to users that do.</p> <p>The CSS for this logic looks like this:</p> <pre class="language-css hljs-block"><code class="language-css hljs-block"><span class="hljs-selector-class">.tag-list</span> {<br /> <span class="hljs-attribute">min-height</span>: <span class="hljs-built_in">var</span>(--line-height);<br />}<br /><br /><span class="hljs-selector-class">.tag-list</span> <span class="hljs-selector-tag">details</span> {<br /> <span class="hljs-attribute">display</span>: none;<br />}<br /><br /><span class="hljs-selector-class">.tag-list--show</span> <span class="hljs-selector-tag">details</span> {<br /> <span class="hljs-attribute">display</span>: initial;<br />}</code></pre> <p>First I define the <code>.tag-list</code> class. It gets a <code>min-height</code> because I want to reserve space for the <code>summary</code> when it will appear. That way, it does reflow the page when the component is loaded.</p> <p>Next, I hide the whole <code>details</code> element. Users without JavaScript will not know it’s there.</p> <p>But then I define the <code>.tag-list--show</code> selector which resets the hiding of the <code>details</code> element. I need to apply that class with JavaScript.</p> <p>This is how I do that with JavaScript:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">class</span> <span class="hljs-title class_">MyTagList</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">HTMLElement</span> {<br /> <span class="hljs-title function_">connectedCallback</span>(<span class="hljs-params"></span>) {<br /> <span class="hljs-variable language_">this</span>.<span class="hljs-property">classList</span>.<span class="hljs-title function_">add</span>(<span class="hljs-string">&#x27;tag-list--show&#x27;</span>)<br /> }<br />}<br /><br />customElements.<span class="hljs-title function_">define</span>(<span class="hljs-string">&#x27;my-tag-list&#x27;</span>, <span class="hljs-title class_">MyTagList</span>)</code></pre> <p>Here I define the Custom Element <code>MyTagList</code>. When the element is mounted into the DOM, the <code>connectedCallback</code> fires. Because this is a Custom Element, the <code>this</code> refers to the <em>instance</em> of <code>my-tag-list</code> in the DOM. Within the <code>connectedCallback</code> I can do as much DOM manipulation as I please. It is guaranteed that all the DOM inside the <code>my-tag-list</code> element is ready.</p> <p>Using a Custom Element here provides me with a couple of benefits:</p> <h2 id="benefits" tabindex="-1">Benefits</h2> <ul> <li>I don’t have to write the boilerplate code of <code>querySelect</code>-ing elements matching a data attribute and triggering functions</li> <li>When elements are dynamically added to the page, I don’t have to rerun that boilerplate code</li> <li>By using Custom Elements, I need to provide the <code>type=&quot;module&quot;</code> attribute when I load the script. This means, that older browsers do not run that code at all. I don’t have to worry about transpiling my code back to ES5.</li> </ul> <h2 id="closing-thoughts" tabindex="-1">Closing thoughts</h2> <p>This is a small example, but I hope it illustrates the use case. I am sure there are downsides to this method but, for now, I like it and want to try it out on more projects.</p> <p>What do you think? Did I mis anything?</p> Why I signed the Overlay Factsheet 2021-03-13T00:00:00Z https://www.petergoes.nl/blog/why-i-signed-overlay-factsheet/ <p>Today I submitted the <a href="https://github.com/karlgroves/overlayfactsheet/pull/113">PR</a><br /> to sign the <a href="https://overlayfactsheet.com/">Overlay Fact Sheet</a>. I did not do<br /> this lightly. In this post, I explain why I did.</p> <h2 id="what-the-factsheet-is-all-about" tabindex="-1">What the factsheet is all about</h2> <p>To quote the conclusion of the Overlay Factsheet:</p> <blockquote> <p>Accessibility on the Web is a big challenge, both for owners of websites and<br /> for the users of those websites. The invention of novel approaches to<br /> resolving this challenge is to be commended. However, in the case of overlays</p> <ul> <li>especially those which attempt to add widgets that present assistive<br /> features - the challenge is not being met. Even more problematic are the<br /> deceptive marketing provided by some overlay vendors who promise that<br /> implementing their product will give their customer’s sites immediate<br /> compliance with laws and standards.</li> </ul> <p>No overlay product on the market can cause a website to become fully compliant<br /> with any existing accessibility standard and therefore cannot eliminate legal<br /> risk.</p> <ul> <li><a href="https://overlayfactsheet.com/">overlayfactsheet.com</a></li> </ul> </blockquote> <h2 id="why-i-signed" tabindex="-1">Why I signed</h2> <p>I view front-end development as a <strong>craft</strong>. I take pride in code that is readable and maintainable, in interfaces that make sense to its users. I try my best to become good in what I do. It takes commitment and dedication to reach that. And like with anything that takes dedication to master, a good front-end is not cheap.</p> <p>Front-end development is not a matter of ticking boxes, or putting colour onto various elements. You should be able to look past your own experiences, think about someone else; the user of your work, the browser that user is using and the context in which the user is operating.</p> <p>A part of the code that we write can be automatically checked and fixed. But for a pretty big portion that is not an option. It is the human part of our work. Our ability to research how our users interact with our product, translate that into a <em>good</em> and <em>accessible</em> user interface, can never be automated. Because making your product accessible, comes down to building it so that it <strong>makes sense to your user</strong>. That is, by definition, subjective. And computers can not make choices in subjective matters.</p> <p>Therefore, tools that promise to make a website accessible with a single line of code, can <strong>never</strong> fix issues that <strong>require</strong> human thinking.</p> <p>When they <em>do</em> make that promise, they false advertise.<br /> And degrading the craft of front-end development with it.</p> Learn about Web Components CSS selectors by server side rendering 2019-04-27T00:00:00Z https://www.petergoes.nl/ <h2 id="new-css-selectors-in-web-components" tabindex="-1">New CSS selectors in Web Components</h2> <p>One of the most useful features of the Shadow DOM specification, is <strong>scoped CSS</strong>. The styles defined in the Shadow DOM don’t leak out to the containing page. But within the Shadow DOM you CAN get a sense of the surrounding context of the Custom Element. To make the latter possible, some new selectors have been introduced. In this post, I will explain how I learned about some of these new selectors, focusing on the <code>:host()</code>, <code>:host-context()</code> and <code>::slotted()</code>.</p> <p>In a previous blog posts I focussed on <a href="https://www.petergoes.nl/blog/my-stab-at-rendering-shadow-dom-server-side">serialising the HTML of the Shadow DOM</a>. But these new CSS selectors should also be transformed and placed in the main document. Because, as long as the Web Component did not load on the client yet, its styles will not show up.</p> <p>So somehow I needed to transform the selectors from the Shadow DOM styles and move these styles into the main document (or Light DOM). That process taught me quite a lot about how these new selectors work!</p> <h2 id="a-quick-recap-of-serialising-the-html" tabindex="-1">A quick recap of serialising the HTML</h2> <p>To serialise the Shadow DOM I go through this process:</p> <ol> <li>Launch Puppeteer and load a page containing Web Components</li> <li>For every Web Component: <ol> <li>Find the rendered Shadow DOM</li> <li>Find the Light DOM contained in the element</li> <li>Move the Light DOM into a <code>&lt;template&gt;</code> element</li> <li>Move the (rendered) Shadow DOM in the place of the Light DOM</li> <li>Add a couple of attributes indicating the state of the serialising process</li> </ol> </li> <li>Add a rehydrate script to the page which will run on the client undoing all the work above</li> </ol> Differentiate SSR states in a Web Component 2019-04-15T00:00:00Z https://www.petergoes.nl/blog/differentiate-ssr-states-in-a-web-component/ <h2 id="the-problem" tabindex="-1">The problem</h2> <p>After my <a href="https://www.petergoes.nl/blog/my-stab-at-rendering-shadow-dom-server-side">previous blog post</a>, I ended up in a discussion with <a href="https://twitter.com/harmenjanssen">@harmenjanssen</a> in which he made the following point:</p> <blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Most components will need Javascript to function, *after* needing Javascript to render, is what I mean. So using SSR to implement the JS *rendering* would leave the user in a sort of semi-enhanced experience?<br />At least I need some examples that show otherwise.</p>&mdash; Harmen Janssen (@harmenjanssen) <a href="https://twitter.com/harmenjanssen/status/1110196955672645632?ref_src=twsrc%5Etfw">March 25, 2019</a></blockquote> <script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <p>This got me thinking. I am used to writing Vue components. I use <a href="https://nuxtjs.org/">Nuxt</a> to server side render my applications and I did not encounter much issues like Harmen described.</p> <p>Sure, we have created a <code>&lt;no-ssr&gt;</code> Vue component at <a href="https://www.voorhoede.nl/">De Voorhoede</a>, but generally speaking, I don’t think about it all that much. Most of the time it Just Works™️.</p> <p>To understand the point of view of Harmen, let’s dive into <a href="https://grrr.tech/posts/hansel/">a blog post</a> he wrote recently. I encourage you to read the post, it’s really good. But the gist of it is this. By using the <a href="https://hiddedevries.nl/en/blog/2015-04-03-progressive-enhancement-with-handlers-and-enhancers">enhancer pattern</a> you enhance a piece of html with JavaScript to make it interactive.</p> <p>I don’t know the stack Harmen uses, but my guess would be that a cms outputs html. The client side (vanilla) JavaScript then kicks in and enhances that html. My assumption is that the cms is not using a front-end framework like Vue or React to build that html. It is a vastly different approach to outputting html via a front-end framework like Vue or React where the same JavaScript runs both on the server and client (and JavaScript is the main language to write the component in).</p> <p>In my attempt to server side render Web Components, the process is like this:</p> <ol> <li>Write the component</li> <li>Fire up a puppeteer instance and enhance it via server side JavaScript</li> <li>Send the rendered component to the client</li> <li>On the client, the component gets hydrated letting client side JavaScript take over</li> </ol> <p>If you compare this flow to the workflow of enhancing html client side, my process enhances the component too early. As long as the client does not have the JavaScript downloaded, <strong>the components html is enhanced, but does not have JavaScript yet to back it up</strong>. That is the “semi-enhanced experience” Harmen is talking about.</p> <p>Another thing that is quite different compared to the enhancer pattern, is that by using Vue or React, JavaScript is your templating language. Where on a typical server rendered site, the html would be written in php, nunjucks or something else. In that case JavaScript is only run client side and is only used to add behaviour. In the way I try to render a Web Component server side, I also use it as the templating language to create the html.</p> <p>How does a front-end framework like Vue handle the issue of the “semi-enhanced experience”? Why did I not encounter this issue more often before?</p> <h2 id="life-cycle-methods-in-vue" tabindex="-1">Life cycle methods in Vue</h2> <p>I take Vue as an example here since I have the most experience with it, but the concepts apply to React as well.</p> <p>A Vue component has the concept of life cycle methods. During various stages of its life, various methods are run. Let’s look at the <code>mounted()</code> method.<br /> The <a href="https://vuejs.org/v2/api/#mounted">documentation</a> of the <code>mounted</code> method tells us:</p> <blockquote> <p>Called after the instance has been mounted</p> </blockquote> <p>So, when the component is mounted in the DOM, this method is called. But a little further down, the documentation states:</p> <blockquote> <p><em>This hook is not called during server-side rendering.</em></p> </blockquote> <p>So the <code>mounted</code> method only runs client side. This gives you the ability to make a difference between what is rendered server side, and what happens client side.</p> <p>Further more, since Vue handles its components server side as well as client side, it can treat them differently based on the context.</p> <p>When compared to the enhancer pattern, there are quite a lot of states a component can be in, in Vue. To name a few:</p> <ul> <li>before-anything-happens server side</li> <li>rendered server side</li> <li>rendered server side on the client</li> <li>mounted on the client</li> </ul> <p>Looking at the enhancer pattern there are only two states:</p> <ul> <li>non enhanced client side</li> <li>enhanced client side</li> </ul> <p>As it turns out, I did encounter the issue before, but because Vue as a framework makes working with components both on server and client side so straight forward, I do not have to think about the differences between server and client environments. That is kind of the whole reason to go for a universal JavaScript approach in the first place.</p> <p>In my attempt to render Web Components server side, I did not take the work Vue does here into account. Because I don’t treat server and client environments as two separate environments, I actually spin up a client IN the server to render the output.</p> <h2 id="life-cycle-hooks-in-web-components" tabindex="-1">Life cycle hooks in Web Components</h2> <p>But wait a minute, Web Components have life cycle hooks of their own. They are called <a href="https://developers.google.com/web/fundamentals/web-components/customelements#reactions">Custom Element Reactions</a>. There is a <code>constructor</code> and a <code>connectedCallback</code> among others. The <code>connectedCallback</code> maps to Vues <code>mounted</code> pretty close. From the documentation:</p> <blockquote> <p>Called every time the element is inserted into the DOM</p> </blockquote> <p>That does not help me all that much though. I am still using an actual client when rendering server side. So although the <code>mounted</code> hook is not called server side in Vue. <code>connectedCallback</code> <strong>does</strong> get called in my implementation.</p> <p><em>sigh…</em></p> <p>So what are the options now? I really want this server side render thing I am building to <em>not</em> introduce any new api by creating some sort of base class which you have to extend in order to get server side rendering working. It should work on any Web Component.</p> <h2 id="using-what-i-already-have" tabindex="-1">Using what I already have</h2> <p>But then I looked at what I am already doing to the component and realised, I have the answer right here. In fact. I already wrote it!</p> <p>When I serialise a component server side, I include an attribute to the element: <code>data-ssr=&quot;serialized&quot;</code>. When the hydration script takes over that attribute is updated to: <code>data-ssr=&quot;hydrated&quot;</code>. So now I <em>do</em> know which state my component is in!</p> <p>Lets look at an example:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">class</span> <span class="hljs-title class_">LifeCycle</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">HTMLElement</span> {<br /> <span class="hljs-keyword">static</span> <span class="hljs-keyword">get</span> <span class="hljs-title function_">observedAttributes</span>() {<br /> <span class="hljs-keyword">return</span> [<span class="hljs-string">&#x27;data-ssr&#x27;</span>];<br /> }<br /><br /> <span class="hljs-title function_">connectedCallback</span>(<span class="hljs-params"></span>) {<br /> <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">attachShadow</span>({ <span class="hljs-attr">mode</span>: <span class="hljs-string">&#x27;open&#x27;</span> });<br /> <span class="hljs-variable language_">this</span>.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">`<br /> &lt;h1&gt;I am server side rendered&lt;/h1&gt;<br /> `</span><br /> }<br /><br /> <span class="hljs-title function_">attributeChangedCallback</span>(<span class="hljs-params">name, oldValue, newValue</span>) {<br /> <span class="hljs-keyword">if</span> (name === <span class="hljs-string">&#x27;data-ssr&#x27;</span> &amp;&amp; newValue === <span class="hljs-string">&#x27;hydrated&#x27;</span>) {<br /> <span class="hljs-variable language_">this</span>.<span class="hljs-property">shadowRoot</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">&#x27;h1&#x27;</span>).<span class="hljs-property">innerText</span> = <span class="hljs-string">&#x27;I am client side renderd&#x27;</span><br /> }<br /> }<br />}<br /><span class="hljs-variable language_">window</span>.<span class="hljs-property">customElements</span>.<span class="hljs-title function_">define</span>(<span class="hljs-string">&#x27;x-life-cycle&#x27;</span>, <span class="hljs-title class_">LifeCycle</span>);</code></pre> <p>What happens here: I listen for changes to the <code>data-ssr</code> attribute. When it does change I know I am on the client because the hydrate script sets it to <code>hydrated</code>.</p> <p>Now I can update the component like I would in a <code>mounted</code> method in a Vue component.</p> <h2 id="closing-thoughts" tabindex="-1">Closing thoughts</h2> <p>Is this an ideal solution? <strong>No</strong> far from it. The <code>data-ssr</code> attribute is something that I came up with, and it only works if you render the component server side with <em>my</em> tool. It is by no means standardised.</p> <p>But, it gets the job done. And as long as there is no standardised way of server side rendering a Web Component, we will be tied to custom implementations like this one.</p> <p>So, now that I got that out of my head, I am going to focus on serialising the css of the Web Component. That is something I am struggling with for quite some time now.</p> <p>Do you have any comments? Please reach out at <a href="https://twitter.com/petergoes">@petergoes</a></p> My stab at rendering Shadow Dom server side 2019-03-24T00:00:00Z https://www.petergoes.nl/blog/my-stab-at-rendering-shadow-dom-server-side/ <h2 id="what-is-the-shadow-dom-and-why-is-it-important%3F" tabindex="-1">What is the Shadow Dom and why is it important?</h2> <p>The <a href="https://www.w3.org/TR/shadow-dom/">Shadow Dom</a> is a specification that <a href="https://caniuse.com/#search=shadow%20dom">CanIUse</a> summarises as:</p> <blockquote> <p>Method of establishing and maintaining functional boundaries between DOM trees and how these trees interact with each other within a document, thus enabling better functional encapsulation within the DOM &amp; CSS. - <a href="https://caniuse.com/">CanIUse.com</a></p> </blockquote> <p>An element can have its own DOM which is hidden or inaccessible from the outside. Think of the <code>&lt;video&gt;</code> element. All controls; the play button, the volume controls, the play head, are all DOM elements. You can’t see or modify them because they live in the Shadow Dom of the element.<br /> The Shadow Dom in a <code>&lt;video&gt;</code> element has a mode of <code>'closed'</code>.</p> <p>A Web Component that you write can also have its own Shadow Dom. As a component author you can set its mode to <code>'closed'</code> (like the <code>&lt;video&gt;</code> element) or <code>'open'</code>. When it’s <code>'open'</code> you can inspect it in your DevTools or interact with it via JavaScript. It is important to note that the Shadow Dom gets created when the Web Component is connected, and that it is not part of the DOM that gets downloaded with rest of the HTML. In other words, when the browser downloads an HTML file, the Shadow Dom is not included.</p> <h2 id="why-do-i-want-to-render-a-web-components-shadow-dom-server-side%3F" tabindex="-1">Why do I want to render a Web Components Shadow Dom server side?</h2> <p>The kind of projects I typically do are static site generated websites. Which can be enhanced with JavaScript when that is loaded, but as much as all functionality should be usable without JavaScript.</p> <p>If I approach Web Components the same way that I would with a Vue Component, most of the markup I write ends up in the Shadow Dom. And by default, that won’t get shipped with the HTML file. I would NEED the Web Component to be downloaded and parsed in order for the Shadow Dom to exist. That kind of goes against the idea of not relying on JavaScript.</p> <h2 id="serialising-the-shadow-dom" tabindex="-1">Serialising the Shadow Dom</h2> <p>So, I need to come up with a way to transfer the Shadow Dom with the rest of the HTML. In my previous blog post (<a href="https://www.petergoes.nl/blog/experimenting-with-rendering-web-components">Experimenting with rendering Web Components</a>) I explain that I took a lot of inspiration from <a href="https://twitter.com/treshugart">@treshugart</a>’s <a href="https://github.com/skatejs/skatejs/tree/master/packages/ssr">@skatejs/ssr</a>.</p> <p>Let me explain my approach, which is quite similar, but I can afford to use<br /> <a href="https://github.com/GoogleChrome/puppeteer">Puppeteer</a> because I pre-render all my pages during build time.</p> <h3 id="the-problem" tabindex="-1">The problem</h3> <p>Lets look at the Shadow Dom of an <code>&lt;x-hello&gt;</code> Web Component:</p> <pre class="language-html hljs-block"><code class="language-html hljs-block"><span class="hljs-tag">&lt;<span class="hljs-name">x-hello</span>&gt;</span><br /> World <span class="hljs-comment">&lt;!-- Light DOM --&gt;</span><br /> #shadow-root(open) <span class="hljs-comment">&lt;!-- Thing made up by DevTools --&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span> <span class="hljs-comment">&lt;!-- Element in the Shadow DOM --&gt;</span><br /> Hello<br /> <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">slot</span>&gt;</span> <span class="hljs-comment">&lt;!-- Special element to host Light Dom --&gt;</span><br /> &lt;#text&gt; <span class="hljs-comment">&lt;!-- reference to Light DOM --&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">slot</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span><br /> !<br /> <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">x-hello</span>&gt;</span></code></pre> <p>As you can see DevTools visualises the Shadow DOM, but that is there so you can inspect it. It does not actually exist.</p> <p>As a quick reference, as a user of the <code>&lt;x-hello&gt;</code> element this is how it looks in the source code:</p> <pre class="language-html hljs-block"><code class="language-html hljs-block"><span class="hljs-tag">&lt;<span class="hljs-name">x-hello</span>&gt;</span>World<span class="hljs-tag">&lt;/<span class="hljs-name">x-hello</span>&gt;</span></code></pre> <p>As you can see from the Shadow Dom example, the word <em>World</em> is between<br /> <code>&lt;strong&gt;</code> tags, so it is displayed in bold. The word <em>Hello</em> is also within the Shadow DOM. It’s rendered, but when the Web Component has connected.</p> <p>And like I said before, the Shadow DOM is not part of the HTML. So how do we let a non JavaScript browser know that it should render <em>World</em> in bold, or <em>Hello</em> at all?</p> <h3 id="breaking-down-the-problem" tabindex="-1">Breaking down the problem</h3> <p>I have the luxury that I can use <a href="https://github.com/GoogleChrome/puppeteer">Puppeteer</a>. Puppeteer is a headless Chrome browser, and Chrome happens to have support for Shadow DOM.</p> <p>I can fire up a Puppeteer instance, load up my page, and then have it do things. Things like this:</p> <p><small>Quick note, I will explain only the concepts here using simplified code. If you want to take a look at the actual code, checkout my progress thus far in the repo <a href="https://github.com/petergoes/ssr-web-components/tree/0d1b6d5121faf4009a70fed2e657b2c034307695">here</a>. All references to the files in the repo, I will do at the current commit at the time of writing. It is not necessarily the most recent version of the file.</small></p> <p>First I need to get access to the <code>load</code>ed page</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;puppeteer&#x27;</span>)<br /><span class="hljs-keyword">const</span> serialize = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./lib/serialize&#x27;</span>)<br /><span class="hljs-keyword">const</span> hydrate = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;./lib/hydrate&#x27;</span>)<br /><br />(<span class="hljs-keyword">async</span> (fileName) =&gt; {<br /> <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.<span class="hljs-title function_">launch</span>()<br /> <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.<span class="hljs-title function_">newPage</span>()<br /> <br /> page.<span class="hljs-title function_">on</span>(<span class="hljs-string">&#x27;load&#x27;</span>, <span class="hljs-keyword">async</span> onLoad (...args) =&gt; {<br /> <span class="hljs-comment">/* Do things with the loaded HTML */</span><br /> <br /> <span class="hljs-comment">/* Get the modified HTML */</span><br /> <br /> <span class="hljs-comment">/* Write the modified HTML to a new file */</span><br /> })<br /><br /> <span class="hljs-keyword">await</span> page.<span class="hljs-title function_">goto</span>(<span class="hljs-string">&#x27;file://&#x27;</span> + path.<span class="hljs-title function_">join</span>(__dirname, fileName))<br />})(<span class="hljs-string">&#x27;/public/index.html&#x27;</span>)</code></pre> <p>Lets fill in the blanks here</p> <h4 id="1.-do-things-with-the-loaded-html" tabindex="-1">1. Do things with the loaded HTML</h4> <p>This is the meat of the whole process. First lets get into the browser context</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">await</span> page.$eval(<span class="hljs-string">&#x27;html&#x27;</span>, serialize)</code></pre> <p><code>serialize</code> is a module I required from the <code>lib/</code> folder earlier and this is what it does (<a href="https://github.com/petergoes/ssr-web-components/blob/0d1b6d5121faf4009a70fed2e657b2c034307695/lib/serialize.js">here</a> you can find the whole file):</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = <span class="hljs-keyword">function</span> <span class="hljs-title function_">serialize</span> (rootNode) {<br /> <br /> <span class="hljs-keyword">function</span> <span class="hljs-title function_">serializeNode</span>(<span class="hljs-params">node</span>) {<br /> <span class="hljs-comment">/* will explain later */</span><br /> } <br /> <br /> [...rootNode.<span class="hljs-title function_">querySelectorAll</span>(<span class="hljs-string">&#x27;*&#x27;</span>)]<br /> .<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">element</span> =&gt;</span> <span class="hljs-regexp">/-/</span>.<span class="hljs-title function_">test</span>(element.<span class="hljs-property">nodeName</span>))<br /> .<span class="hljs-title function_">forEach</span>(serializeNode)<br />}</code></pre> <ol> <li>I get all the elements in the current <code>rootNode</code> (when it first runs, that would be <code>&lt;html&gt;</code></li> <li>I filter out all elements that do NOT have a <code>-</code> in their <code>nodeName</code>. Custom Elements are required to have a <code>-</code> in their tag name.</li> <li>For each Custom Element I run the <code>serializeNode</code> function</li> </ol> <p>** <code>serializeNode</code>: **</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">function</span> <span class="hljs-title function_">serializeNode</span> (node) {<br /> <span class="hljs-keyword">const</span> lightDomNodes = node.<span class="hljs-property">childNodes</span><br /> <span class="hljs-keyword">const</span> lightDomHtml = node.<span class="hljs-property">innerHTML</span><br /> <span class="hljs-keyword">const</span> templateDom = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">&#x27;template&#x27;</span>)<br /> <span class="hljs-keyword">const</span> scriptData = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">&#x27;script&#x27;</span>)<br /> <span class="hljs-keyword">const</span> slot = node.<span class="hljs-property">shadowRoot</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">&#x27;slot&#x27;</span>)<br /> <span class="hljs-keyword">const</span> attributesProperties = node.<span class="hljs-title function_">getAttributeNames</span>()<br /> .<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">name</span> =&gt;</span> name !== <span class="hljs-string">&#x27;data-ssr&#x27;</span>)<br /> .<span class="hljs-title function_">reduce</span>(<span class="hljs-function">(<span class="hljs-params">obj, name</span>) =&gt;</span> {<br /> <span class="hljs-keyword">return</span> {...obj, [name]: node[name] }<br /> }, {})<br /><br /> templateDom.<span class="hljs-title function_">setAttribute</span>(<span class="hljs-string">&#x27;type&#x27;</span>, <span class="hljs-string">&#x27;ssr-light-dom&#x27;</span>)<br /> templateDom.<span class="hljs-property">innerHTML</span> = lightDomHtml<br /><br /> scriptData.<span class="hljs-title function_">setAttribute</span>(<span class="hljs-string">&#x27;type&#x27;</span>, <span class="hljs-string">&#x27;ssr-data&#x27;</span>)<br /> scriptData.<span class="hljs-property">innerHTML</span> = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(attributesProperties)<br /> <br /> <span class="hljs-comment">// ...</span><br />}</code></pre> <ol> <li>Get a reference to the current Light DOM (elements as well as <code>innerHTML</code>)</li> <li>Create <code>template</code> and <code>script</code> tags</li> <li>Get a reference to the <code>&lt;slot&gt;</code> element</li> <li>Get a JSON object with all attributes with their value</li> </ol> <p>Then:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">function</span> <span class="hljs-title function_">serializeNode</span> (node) {<br /> <span class="hljs-comment">// ...</span><br /><br /> <span class="hljs-comment">/* move light nodes into shadowDom */</span><br /> lightDomNodes.<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">lightNode</span> =&gt;</span> slot.<span class="hljs-property">parentNode</span>.<span class="hljs-title function_">insertBefore</span>(lightNode, slot))<br /><br /> <span class="hljs-comment">/* move shadowDom into root node */</span><br /> node.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">childNodes</span>.<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">shadowNode</span> =&gt;</span> node.<span class="hljs-title function_">appendChild</span>(shadowNode))<br /><br /> <span class="hljs-comment">/* remove slot element */</span><br /> <span class="hljs-keyword">if</span> (slot) {<br /> slot.<span class="hljs-property">parentNode</span>.<span class="hljs-title function_">removeChild</span>(slot)<br /> }<br /><br /> <span class="hljs-comment">/* serialize custom element child nodes */</span><br /> <span class="hljs-title function_">serialize</span>(node)<br /><br /> <span class="hljs-comment">/* add original lightDom as template */</span><br /> <span class="hljs-keyword">if</span> (templateDom.<span class="hljs-property">innerHTML</span> !== <span class="hljs-string">&#x27;&#x27;</span>) {<br /> node.<span class="hljs-title function_">appendChild</span>(templateDom)<br /> }<br /><br /> <span class="hljs-comment">/* add the data as script */</span><br /> <span class="hljs-keyword">if</span> (scriptData.<span class="hljs-property">innerHTML</span> !== <span class="hljs-string">&#x27;{}&#x27;</span>) {<br /> node.<span class="hljs-title function_">appendChild</span>(scriptData)<br /> }<br /><br /> node.<span class="hljs-title function_">setAttribute</span>(<span class="hljs-string">&#x27;data-ssr&#x27;</span>, <span class="hljs-string">&#x27;serialized&#x27;</span>)<br />}</code></pre> <ol> <li>Move Light DOM before the <code>&lt;slot&gt;</code> element of the Shadow DOM. The browser will also (sort of) move the Light DOM into the Shadow DOM. So let’s mimic that. I don’t place it inside of the <code>&lt;slot&gt;</code> because I will remove the <code>&lt;slot&gt;</code> later on</li> <li>Move the (current) Shadow DOM (with the Light DOM added to it) into the Light DOM. We need to send the Shadow DOM over the wire. By placing its contents inside the Light DOM, its contents no longer are part of the Shadow DOM but become Light DOM</li> <li>Remove the obsolete <code>&lt;slot&gt;</code> element</li> <li>Trigger the same routine for the now-in-Light-DOM-elements as they might contain other Custom Elements</li> <li>Add the original Light DOM in a <code>&lt;template&gt;</code> element. We need to preserve the original Light DOM because in the browser we should be able to reset everything to its original state</li> <li>Add the (JSON) object containing all data as well to it to will gets send down the wire</li> <li>Mark this Custom Element as <code>'serialized'</code> so the <code>hydrate</code> script in the browser knows this elements needs some work.</li> </ol> <p>I move the original Light DOM inside of a <code>&lt;template&gt;</code> element, because the browser won’t touch anything inside a <code>&lt;template&gt;</code>. In other words, it won’t render duplicate content.</p> <h4 id="get-the-modified-html" tabindex="-1">Get the modified HTML</h4> <p>Now that I am done with modifying the HTML in the browser context, let’s get back in the Puppeteer script. I need the modified HTML:</p> <pre class="language-js hljs-block"><code class="language-js hljs-block">page.<span class="hljs-title function_">on</span>(<span class="hljs-string">&#x27;load&#x27;</span>, <span class="hljs-keyword">async</span> (...args) =&gt; {<br /> <span class="hljs-comment">// ...</span><br /> <span class="hljs-keyword">const</span> pageContent = <span class="hljs-keyword">await</span> page.<span class="hljs-title function_">content</span>()<br /> <span class="hljs-comment">// ...</span><br />})</code></pre> <p>Now I have the modified HTML in the Puppeteer context</p> <h4 id="write-the-modified-html-to-a-new-file" tabindex="-1">Write the modified HTML to a new file</h4> <p>Lets write the modified HTML to the target file</p> <pre class="language-js hljs-block"><code class="language-js hljs-block">page.<span class="hljs-title function_">on</span>(<span class="hljs-string">&#x27;load&#x27;</span>, <span class="hljs-keyword">async</span> (...args) =&gt; {<br /> <span class="hljs-comment">// ...</span><br /> fs.<span class="hljs-title function_">writeFile</span>(<br /> path.<span class="hljs-title function_">join</span>(__dirname, fileName.<span class="hljs-title function_">replace</span>(<span class="hljs-string">&#x27;.html&#x27;</span>, <span class="hljs-string">&#x27;.ssr.html&#x27;</span>)),<br /> pageContent.<span class="hljs-title function_">replace</span>(<span class="hljs-string">&#x27;&lt;/body&gt;&#x27;</span>, <span class="hljs-string">`<span class="hljs-subst">${hydrate}</span>&lt;/body&gt;`</span>),<br /> {<span class="hljs-attr">encoding</span>: <span class="hljs-string">&#x27;utf8&#x27;</span>},<br /> <span class="hljs-keyword">async</span> (err) =&gt; {<br /> <span class="hljs-keyword">await</span> browser.<span class="hljs-title function_">close</span>();<br /> }<br /> )<br />})</code></pre> <ol> <li>Replace the current file extension to <code>*.ssr.html</code> so we know that this file is the modified version</li> <li>Yet another slight mutation to the HTML: include the <code>hydrate</code> script.</li> <li>When all is done, close the browser</li> </ol> <p>So! Thats that! We now have a server side rendered version of all Web Components in a Light DOM fashion. Note that I did not do anything with CSS yet. I focused on serialising the HTML!! The CSS part is something I still have to figure out.</p> <p>But the browser has all these capabilities. It would be a shame to not use them. Lets have a look at the hydrate script to see how the process can be reverted client side</p> <h3 id="hydrate-client-side" tabindex="-1">Hydrate client side</h3> <pre class="language-js hljs-block"><code class="language-js hljs-block"><span class="hljs-keyword">function</span> <span class="hljs-title function_">hydrate</span>(<span class="hljs-params"></span>) {<br /> [...<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">querySelectorAll</span>(<span class="hljs-string">&#x27;[data-ssr=&quot;serialized&quot;]&#x27;</span>)]<br /> .<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> {<br /> <span class="hljs-keyword">const</span> lightDom = el.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">&#x27;[type=&quot;ssr-light-dom&quot;]&#x27;</span>)<br /> <span class="hljs-keyword">const</span> lightDomContent = lightDom.<span class="hljs-property">content</span><br /> <span class="hljs-keyword">const</span> dataElement = el.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">&#x27;[type=&quot;ssr-data&quot;]&#x27;</span>)<br /> <span class="hljs-keyword">const</span> dataContent = dataElement.<span class="hljs-property">innerText</span><br /> <span class="hljs-keyword">const</span> data = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(dataContent)<br /><br /> el.<span class="hljs-property">childNodes</span>.<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">node</span> =&gt;</span> {<br /> <span class="hljs-keyword">if</span> (node !== lightDom) {<br /> node.<span class="hljs-property">parentElement</span>.<span class="hljs-title function_">removeChild</span>(node)<br /> }<br /> })<br /><br /> lightDomContent.<span class="hljs-property">childNodes</span>.<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">node</span> =&gt;</span> el.<span class="hljs-title function_">appendChild</span>(node))<br /> lightDom.<span class="hljs-property">parentElement</span>.<span class="hljs-title function_">removeChild</span>(lightDom)<br /> dataElement.<span class="hljs-property">parentElement</span>.<span class="hljs-title function_">removeChild</span>(dataElement)<br /> <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(data).<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">key</span> =&gt;</span> {<br /> el[key] = data[key]<br /> })<br /><br /> el.<span class="hljs-title function_">setAttribute</span>(<span class="hljs-string">&#x27;data-ssr&#x27;</span>, <span class="hljs-string">&#x27;hydrated&#x27;</span>)<br /> }<br /> )<br />}<br /><br /><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = <span class="hljs-string">`&lt;script&gt;<span class="hljs-subst">${hydrate.toString()}</span>; hydrate();&lt;/script&gt;`</span></code></pre> <p>That is the script at once. I simplified it a bit by removing some is-this-value-non-empty-checks. The original source is <a href="https://github.com/petergoes/ssr-web-components/blob/0d1b6d5121faf4009a70fed2e657b2c034307695/lib/hydrate.js">here</a> This is going on:</p> <ol> <li>Get all elements on the page with a <code>[data-ssr=&quot;serialized&quot;]</code> attribute.</li> <li>For each element do:</li> <li>Get a reference to the <code>&lt;template&gt;</code> and <code>&lt;script&gt;</code> elements and their contents, in the current Web Component element</li> <li>Remove all elements except the <code>&lt;template&gt;</code>. Removing all would-be-Shadow-DOM</li> <li>Move all elements in the <code>&lt;template&gt;</code> element (original Light DOM) back into their original place</li> <li>Remove the now empty <code>&lt;template&gt;</code> element and <code>&lt;script&gt;</code> element</li> <li>Loop over the data object to add all properties to the Web Components properties. This makes sure Array and Object properties are set via JavaScript</li> <li>Update the <code>data-ssr</code> attribute to <code>'hydrated'</code> to indicate that this Web Component has been hydrated</li> </ol> <h3 id="things-i-did-not-cover" tabindex="-1">Things I did not cover</h3> <p>As stated before, I only focused on serialising the HTML. CSS is something I did not cover at all. But even then, the HTML part is not fully complete yet. As you might know, Web Components can have multiple <code>&lt;slot&gt;</code> elements. The process thus far only covers a Web Component with a single <code>&lt;slot&gt;</code>!</p> <p>And that’s all there is to it. I am sure I can improve this code quite a lot. But it gets me started.</p> <p>If you want to have a look at the full code, check out the repo <a href="https://github.com/petergoes/ssr-web-components/tree/0d1b6d5121faf4009a70fed2e657b2c034307695">here</a>.</p> <p>Do you have any comments? Reach out to me at <a href="https://twitter.com/petergoes">@petergoes</a>!</p> Experimenting with rendering Web Components 2019-03-23T00:00:00Z https://www.petergoes.nl/blog/experimenting-with-rendering-web-components/ <h2 id="about-this-serie-of-posts" tabindex="-1">About this serie of posts</h2> <p>This is the start of my journey to render Web Components on the server. In it I will document the things I run in to. Things that work out. Things that do not work out. I see a great deal of potential in the adoption of web components because I believe that working with ‘vanilla’ web technologies will be more resilient, more stable, and better maintainable. Especially in the log run.</p> <p>This series will be the blogs I write in documenting my process. I have no clue when to write something or when it is done, but we’ll see!</p> <p>I have quite some experience with building React and Vue applications. Especially Vue is something that I love to work with. Because it combines a component based workflow and it <em>feels</em> really close to vanilla web technologies. Although in the end it is all compiled down to JavaScript, as a developer you write as if it were HTML, CSS and JavaScript. And to top it off, the tooling around these frameworks are top notch. Making the developer experience really really good in larger scale or complex sites / apps.</p> <p>Then, Web Components caught my attention again. I did notice it when they were first introduced in 2011. But I never thought much of it. Until I recently did an experiment with them again. See if you can convert a VueJS component into a Web Component to use it in React. Turns out, you can do that! My colleague and I blogged about it <a href="https://www.voorhoede.nl/en/blog/javascript-frameworks-meet-web-components/">here</a>. That got me all exited again, especially since I noticed that <a href="https://lit-element.polymer-project.org/">LitElement</a> and <a href="https://lit-html.polymer-project.org/">LitHtml</a> are a thing now.</p> <h2 id="the-problem-of-server-side-rendering" tabindex="-1">The problem of server side rendering</h2> <p>Server side rendering is still an issue with Web Components. React, Vue and all those other frameworks have this covered. But for the <code>shadowDom</code>, there is not a standardised way of serialising that to send it over the wire.</p> <p>There are some experiments / solutions going on. Especially the <a href="https://github.com/skatejs/skatejs/tree/master/packages/ssr">@skatejs/ssr</a> package by <a href="https://twitter.com/treshugart">@treshugart</a> is really close.</p> <p>I tried the <code>@skatejs/ssr</code> package, and I got the example sort of working. Not quite, though, and I am sure that is due to my lack of proper trying that out. I looked through the source code of the package and realised that it wasn’t all that complicated. I could quickly see the concepts of what was going on. That together with the <a href="https://www.youtube.com/watch?v=yT-EsESAmgA&amp;feature=youtu.be">talk</a> Trey Shugart gave at the Polymer summit in 2017 I was inspired to roll my own implementation. Take a look at that talk, it is really good.</p> <p>I think that rolling my own implementation is something that will teach me quite a bit about the bread and butter of Web Components. And as far as I currently am it is a fun exercise!</p> <p>If you want to check out my progress thus far, you can find the repo <a href="https://github.com/petergoes/ssr-web-components">here</a></p> Back to basics to go forward 2018-02-15T00:00:00Z https://www.petergoes.nl/blog/back-to-basics-to-go-forward/ <h2 id="my-journey-to-become-a-better-front-end-developer" tabindex="-1">My journey to become a better front-end developer</h2> <p>For a long time now I try to keep up with all the latest developments in our<br /> front-end community. Learning React, a bit of Vue, diving into ES6,7,… You<br /> know, like the rest of us.</p> <p>Learning about all these frameworks, build tools and the like, I get<br /> the feeling of missing out on a more important part of our trade. Because the<br /> ultimate goal we all strive for, is to deliver a superb experience to our users.<br /> And we do that by shipping HTML, CSS, JavaScript and a bunch of images. There is<br /> still much to learn about those fundamental parts of the web. And that is where<br /> I will focus my attention on.</p> <p>And although I know these parts reasonably well, there is always room for<br /> improvement. I want to get a deep an thorough understanding of these parts,<br /> before I learn something that is build on top of that foundation.</p> <p>I am going to approach this project like so:</p> <p><strong>Phase 1: Lay the foundation</strong></p> <ul> <li>A deeper foundational understanding of <em>JavaScript</em></li> <li>Get pretty intimate with <em>CSS</em></li> <li>Become really close friends with <em>HTML</em></li> </ul> <p><strong>Phase 2: Build the walls</strong></p> <ul> <li>Brick by brick, <em>Progressive Enhancement</em> as a second nature</li> <li>Preparing for the storm with <em>Progressive Web Apps</em></li> <li>Move things around with <em>Animations</em></li> </ul> <p><strong>Phase 3: Put on the roof</strong></p> <ul> <li>Connect all the wires with <em>HTTP</em></li> </ul> <p>These are by no means set in stone (this is what first came to mind) but for now<br /> it is my guide on this journey</p>