CINXE.COM
🤓Jecelyn Yeen 🤓
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>🤓Jecelyn Yeen 🤓</title> <subtitle>Articles, experiments, musings by Jecelyn Yeen.</subtitle> <link href="https://jec.fish/index.xml" rel="self"/> <link href="https://jec.fish"/> <updated>2022-12-27T00:00:00-00:00</updated> <id>https://jec.fish</id> <author> <name>Jecelyn Yeen</name> </author> <entry> <title>Coding - The Power Language that Everyone can Speak</title> <link href="https://jec.fish/deck/coding-the-powerful-language"/> <updated>2017-09-30T00:00:00-00:00</updated> <id>https://jec.fish/deck/coding-the-powerful-language</id> <summary>Talk about coding, how the language evolved, myths, and how you could start</summary> <category term="deck"/> <content type="html"><p>Human languages has a long history in human evolution. Thousands of years or maybe more. Coding languages are much shorter than that. However, many of us probably could not live a day without coding languages nowadays.</p> <p>The invention of coding languages is indeed important in human history. Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="f1bf94d1d6b9454cbc39f3aa5365149c" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/coding-the-powerful-language-that-everyone-can-speak">https://speakerdeck.com/jecfish/coding-the-powerful-language-that-everyone-can-speak</a> </noscript> <p>.</p> <p>It was a challenge for me to deliver the talk in less than 18 minutes (TEDx style) because a usual tech talk lasts between 30 to 60 minutes. I did quite a few rounds of practices beforehand, but was still nervous on stage, hah.</p> <p>I like how I structured my slides - the flow and the explanation. However, I don't like the delivery that much, haha. 😆 It's painful to watch myself talking. This is one of my early days speaking, probably that won't change that much still. 😂</p> <p>The transcript of the slides is available as well, get it at <a href="https://speakerdeck.com/jecfish/coding-the-powerful-language-transcript">speakerdeck.com/jecfish/coding-the-powerful-language-transcript</a>. If it's too painful to watch, just read it ok! 🤣</p> <p>There is a video recording as well, during TEDxSunwayUniversity 2017. 📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/pKTa7yzDSsE" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/pKTa7yzDSsE?autoplay=1><img src=https://img.youtube.com/vi/pKTa7yzDSsE/hqdefault.jpg alt='Coding: The Power Language that Everyone can Speak | TedxSunwayUniversity'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Coding: The Power Language that Everyone can Speak | TedxSunwayUniversity"> </iframe> </div> <p>.</p> <p>Here is a photo of me during the event.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.8359375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/coding-the-powerful-language-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/coding-the-powerful-language-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/coding-the-powerful-language-01.jpg"> <img src="https://jec.fish/assets/img/deck/coding-the-powerful-language-01.jpg" alt="With my all-time supporters - Henry and Chee Yim and the drawing of me (that does not look like me, at all 😆)"> </picture> </div><figcaption>With my all-time supporters - Henry and Chee Yim and the drawing of me (that does not look like me, at all 😆)</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261710655557472256"> twitter.com/jecfish/status/1261710655557472256 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Processor Pattern with Angular Dependency Injection</title> <link href="https://jec.fish/deck/angular-di-processor-pattern"/> <updated>2018-09-27T00:00:00-00:00</updated> <id>https://jec.fish/deck/angular-di-processor-pattern</id> <summary>How to utilise the awesome Angular Dependency Injection (particularly Injection token and multi provider) – to implement the processor pattern</summary> <category term="deck"/> <content type="html"><p>Processor Design Pattern (Command Pattern) is widely used in building enterprise grade software. In this session, I will share how to utilise the awesome Angular Dependency Injection to implement the pattern.</p> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="7ed7cab0defa44509eae2f5f30a9ed0b" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/angular-di-processor-pattern">https://speakerdeck.com/jecfish/angular-di-processor-pattern</a> </noscript> <p>.</p> <p>I did this sharing multiple times. Here is a video recording, during the International JavaScript Conference (IJS) 2018. 📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/wNH0Vs6arNc" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/wNH0Vs6arNc?autoplay=1><img src=https://img.youtube.com/vi/wNH0Vs6arNc/maxresdefault.jpg alt='Processor Pattern with Angular Dependency Injection | Jecelyn Yeen'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Processor Pattern with Angular Dependency Injection | Jecelyn Yeen"> </iframe> </div> <p>.</p> <p>Here is a photo of me during the conference.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:75%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/angular-di-processor-pattern-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/angular-di-processor-pattern-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/angular-di-processor-pattern-01.jpg"> <img src="https://jec.fish/assets/img/deck/angular-di-processor-pattern-01.jpg" alt="Two Austrians and one Malaysian - Manfred, Michael and me."> </picture> </div><figcaption>Two Austrians and one Malaysian - Manfred, Michael and me.</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1045376362792579073"> twitter.com/jecfish/status/1045376362792579073 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>以 PWA 實現 Angular 網站開發:離線、推播、Service Worker</title> <link href="https://jec.fish/deck/angular-pwa-zh"/> <updated>2018-11-16T00:00:00-00:00</updated> <id>https://jec.fish/deck/angular-pwa-zh</id> <summary>如何使用 Angular 實現 PWA 網站開發, 優化使用者體驗</summary> <category term="deck"/> <content type="html"><p>隨著 PWA 技術的成熟以及瀏覽器的支援度提高,2018 年可謂是 PWA 爆發的一年。本次分享將涵蓋 PWA 的一系列技術 - 離線、推播、Service Worker,以及如何使用 Angular 實現 PWA 網站開發,優化使用者體驗。</p> <p>台北第一届Angular开发者大会。本来打算除了开场自我介绍之外,全程都以英语为主。主要是因为没试过用英语演讲啊,日常华语交谈和正规演讲毕竟是有区别的。</p> <p>我的分享时间比较靠后。对比了前面几位讲者分享时现场观众的反应(英文vs华文),临时决定改用华语分享。演讲中间有些卡卡,因为我脑里不断在进行翻译,有些技术用语(技术术语)确实是不知道啊。😆</p> <p>第一次华语的分享,就这样献给了台北!表现得还可以,过得去,有进步空间。</p> <p>下台后,蛮多与会者来找我进行分享与交流的,看来大家对于提升用户体验还是蛮热心的。還遇到幾位大馬的同胞呢(駐台灣的馬來西亞開發者),真开心。</p> <p>簡報鏈接在此 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="25b0dd345c874761ba6847870dc49029" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/angular-pwa-ngtw">https://speakerdeck.com/jecfish/angular-pwa-ngtw</a> </noscript> <p>.</p> <p>NG-TW 2019 的主办方有录下这场分享。📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/NDBFRWhzE5I" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/NDBFRWhzE5I?autoplay=1><img src=https://img.youtube.com/vi/NDBFRWhzE5I/maxresdefault.jpg alt='以 PWA 實現 Angular 網站開發:離線、推播、Service Worker - Jecelyn Yeen'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="以 PWA 實現 Angular 網站開發:離線、推播、Service Worker - Jecelyn Yeen"> </iframe> </div> <p>.</p> <p>附上一張我在大會拍的照片。</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/angular-pwa-zh-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/angular-pwa-zh-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/angular-pwa-zh-01.jpg"> <img src="https://jec.fish/assets/img/deck/angular-pwa-zh-01.jpg" alt="第一次(也是唯一的一次)在技術大會上看到手語師(sign language interpreter @ tech conf)"> </picture> </div><figcaption>第一次(也是唯一的一次)在技術大會上看到手語師(sign language interpreter @ tech conf)</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261710946277265408"> twitter.com/jecfish/status/1261710946277265408 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div></content> </entry> <entry> <title>NGXS - State Management Made Simple</title> <link href="https://jec.fish/deck/ngxs-state-management"/> <updated>2019-01-10T00:00:00-00:00</updated> <id>https://jec.fish/deck/ngxs-state-management</id> <summary>Talk about NGXS & its concepts. NGXS took many of the concepts of Redux but reimagined them for Angular.</summary> <category term="deck"/> <content type="html"><p>While NgRx seems like the de facto state management solution for Angular applications, we have NGXS as well. Designed to be simple &amp; accessible, it took many of the concepts of Redux but reimagined them for Angular.</p> <p>In this talk, I share about why NGXS - its concepts and core features, and did a live demo!</p> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="b36831c583154a55b0da82c025bb4a3f" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/ngxs">https://speakerdeck.com/jecfish/ngxs</a> </noscript> <p>.</p> <p>There is a video recording as well, during NG-ATL (ngAtlanta) 2019 (but the demo screen is not recorded somehow). 📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/0bhfUGjn0KA" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/0bhfUGjn0KA?autoplay=1><img src=https://img.youtube.com/vi/0bhfUGjn0KA/maxresdefault.jpg alt='NGXS, State Management Made Simple - Jecelyn Yeen'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="NGXS, State Management Made Simple - Jecelyn Yeen"> </iframe> </div> <p>.</p> <p>I really like my coffee demo. In fact, I ran a few workshops using this demo. Here is the GitHub repo: <a href="https://github.com/jecfish/ngxs-coffee">ngxs-coffee</a>.</p> <p>Here is a photo of me during the conference.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:75%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/ngxs-state-management-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/ngxs-state-management-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/ngxs-state-management-01.jpg"> <img src="https://jec.fish/assets/img/deck/ngxs-state-management-01.jpg" alt="A rare conference which female speakers are more than men! 💪"> </picture> </div><figcaption>A rare conference which female speakers are more than men! 💪</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1083386398214901760"> twitter.com/jecfish/status/1083386398214901760 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>How We Built NG-MY Website</title> <link href="https://jec.fish/deck/how-we-built-ng-my"/> <updated>2019-07-06T00:00:00-00:00</updated> <id>https://jec.fish/deck/how-we-built-ng-my</id> <summary>Performance optimizations, SEO, Customized Angular CLI Build</summary> <category term="deck"/> <content type="html"><p>In this talk, we shared about the learnings and techniques we applied in building the NG-MY Website, you can apply these techniques in your projects too!</p> <p><a href="htttps://ng-my.org">NG-MY.org</a> is a speedy Single Page Application (SPA), SEO friendly website, built for the NG-MY 2019 conference. It is built using the Angular framework.</p> <p>These are the area we covered:</p> <ul> <li>How we cut cost</li> <li>How we collaborated</li> <li>How we made the website fast</li> <li>What are some techniques we used (CSS, Angular, etc)</li> </ul> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="45c0010c1f2a4fad95fe7d8d9e0d874d" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/how-we-build-ng-my-website">https://speakerdeck.com/jecfish/how-we-build-ng-my-website</a> </noscript> <p>.</p> <p>There is a video recording as well, during NG-MY Conference 2019. 📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/6l779_V4LV8" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/6l779_V4LV8?autoplay=1><img src=https://img.youtube.com/vi/6l779_V4LV8/maxresdefault.jpg alt='How We Build NG-MY Website: Performance, SEO.. | Jenning Ho, Adrian Yeong, Jecelyn Yeen | NG-MY 2019'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="How We Build NG-MY Website: Performance, SEO.. | Jenning Ho, Adrian Yeong, Jecelyn Yeen | NG-MY 2019"> </iframe> </div> <p>.</p> <p>Here is a photo of me during the conference, it was fun!</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:48.14453125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/how-we-built-ng-my-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/how-we-built-ng-my-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/how-we-built-ng-my-01.jpg"> <img src="https://jec.fish/assets/img/deck/how-we-built-ng-my-01.jpg" alt="All the Google Developer Experts (GDE) at NG-MY 2019!"> </picture> </div><figcaption>All the Google Developer Experts (GDE) at NG-MY 2019!</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261711138569334785"> twitter.com/jecfish/status/1261711138569334785 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Web Performance Optimization</title> <link href="https://jec.fish/deck/web-performance-optimization"/> <updated>2019-08-31T00:00:00-00:00</updated> <id>https://jec.fish/deck/web-performance-optimization</id> <summary>How to do image, web font, and JavaScript optimization</summary> <category term="deck"/> <content type="html"><p>Tips on how to create speedy and performant websites. You can easily apply these techniques in your website too!</p> <p>Here is what we will cover:</p> <ul> <li>Image optimization</li> <li>Web Font optimization</li> <li>JavaScript optimization</li> </ul> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="ae71f076840a410e8eaaa662d2b2ab0c" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/web-performance-optimization">https://speakerdeck.com/jecfish/web-performance-optimization</a> </noscript> <p>.</p> <p>There is a video recording as well, during the Women Who Code Connect Conference 2019. 📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/ACA3yBHBUuE" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/ACA3yBHBUuE?autoplay=1><img src=https://img.youtube.com/vi/ACA3yBHBUuE/maxresdefault.jpg alt='Web Performance Optimization - Jecelyn Yeen'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Web Performance Optimization - Jecelyn Yeen"> </iframe> </div> <p>.</p> <p>Here is a photo of me during the conference.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/web-performance-optimization-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/web-performance-optimization-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/web-performance-optimization-01.jpg"> <img src="https://jec.fish/assets/img/deck/web-performance-optimization-01.jpg" alt="WWCODEKL bunch at the conf"> </picture> </div><figcaption>WWCODEKL bunch at the conf</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261711400121937920"> twitter.com/jecfish/status/1261711400121937920 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Modern JavaScript Frameworks and SEO</title> <link href="https://jec.fish/deck/modern-js-frameworks-and-seo"/> <updated>2019-10-22T00:00:00-00:00</updated> <id>https://jec.fish/deck/modern-js-frameworks-and-seo</id> <summary>How do bots see your single page application? Sharing SEO tips for modern JavaScript frameworks.</summary> <category term="deck"/> <content type="html"><p>In this session, you will learn some facts about Google Search, its rendering pipeline and the various tools which can help you diagnose the gap between what your users see versus what Google Search sees.</p> <p>Many modern JavaScript frameworks render their HTML content on the client-side, while assuming that every environment that is accessing their pages are as modern as they are. However, many search engines – including Google Search – do not behave exactly the same as a user’s browser.</p> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="d06ec13ba312419fbc1d65b16f038082" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/js-seo">https://speakerdeck.com/jecfish/js-seo</a> </noscript> <p>.</p> <p>There is a video recording as well, during FrontCon Latvia 2019. 📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/-0DoYpj3SVc" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/-0DoYpj3SVc?autoplay=1><img src=https://img.youtube.com/vi/-0DoYpj3SVc/hqdefault.jpg alt='Modern JavaScript Frameworks and SEO by Jecelyn Yeen at FrontCon 2019'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Modern JavaScript Frameworks and SEO by Jecelyn Yeen at FrontCon 2019"> </iframe> </div> <p>.</p> <p>Here is a photo of me during the conference.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/modern-js-frameworks-and-seo-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/modern-js-frameworks-and-seo-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/modern-js-frameworks-and-seo-01.jpg"> <img src="https://jec.fish/assets/img/deck/modern-js-frameworks-and-seo-01.jpg" alt="I did a Vuex Workshop at the conference too, and all my workshop attendees... are gentlemen!"> </picture> </div><figcaption>I did a Vuex Workshop at the conference too, and all my workshop attendees... are gentlemen!</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261711618590703616"> twitter.com/jecfish/status/1261711618590703616 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Learning Functional Programming through Game</title> <link href="https://jec.fish/deck/learning-rx-js-through-game"/> <updated>2019-11-03T00:00:00-00:00</updated> <id>https://jec.fish/deck/learning-rx-js-through-game</id> <summary>Learn by building the famous trex game with RxJs</summary> <category term="deck"/> <content type="html"><p>It is a workshop on functional programming. Introducing the concept and use RxJs to recreate the famous trex game.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <img src="https://jec.fish/assets/img/deck/learning-rx-js-through-game-02.gif" alt="Chrome 404 trex game"> </picture> </div><figcaption>Chrome 404 trex game</figcaption></figure> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="f635ee81fd5543c18b820da226aacf4e" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/rxjs-trex">https://speakerdeck.com/jecfish/rxjs-trex</a> </noscript> <p>.</p> <p>Play the game here (live demo): <a href="https://rxjs-trex.netlify.app/">rxjs-trex.netlify.app</a>.</p> <p>The source code is available in two flavors:</p> <ul> <li>vanilla JavaScript: <a href="https://stackblitz.com/edit/rxjs-trex-run">stackblitz.com/edit/rxjs-trex-run</a></li> <li>Angular: <a href="https://github.com/jecfish/rxjs-trex">github.com/jecfish/rxjs-trex</a></li> </ul> <p>.</p> <p>Here is a photo of me during the talk @ GDG SEA Summit 2019. (GDG = Google Developers Group)</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:53.515625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/learning-rx-js-through-game-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/learning-rx-js-through-game-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/learning-rx-js-through-game-01.jpg"> <img src="https://jec.fish/assets/img/deck/learning-rx-js-through-game-01.jpg" alt="Me explaning the trex game"> </picture> </div><figcaption>Me explaning the trex game</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261711814808596480"> twitter.com/jecfish/status/1261711814808596480 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>如何打造高性能,快速且SEO友好的单页应用(SPA)</title> <link href="https://jec.fish/deck/spa-performance-zh"/> <updated>2019-11-24T00:00:00-00:00</updated> <id>https://jec.fish/deck/spa-performance-zh</id> <summary>为大家介绍了各种简单的效能调校的技巧,涵盖了JavaScript,文字以及图像的优化。</summary> <category term="deck"/> <content type="html"><p>如何利用Angular框架建立一个面向群众,高性能的网页呢?本次的分享为大家介绍了各种简单的效能调校技巧,涵盖了JavaScript,文字以及图像的优化。</p> <p>这是我第二次用华语在台上分享,首次在杭州演说(上一次在台北)。还是有点不习惯,毕竟不太常用中文进行正规演说。很多词汇特地事先翻译了一下,练习了一下。现场观众的反应蛮热烈的,确实令人鼓舞。😃</p> <p>简报链接在此 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="4ac34b0f11a9484e86239b17cf00fe62" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/ru-he-da-zao-gao-xing-neng-qie-seoyou-hao-de-dan-ye-ying-yong-spa">https://speakerdeck.com/jecfish/ru-he-da-zao-gao-xing-neng-qie-seoyou-hao-de-dan-ye-ying-yong-spa</a> </noscript> <p>.</p> <p><a href="https://ng-china.org/">NG-CHINA 2019</a> 的主办方有录下这场演说。📹</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/M84OHIWdFX0" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/M84OHIWdFX0?autoplay=1><img src=https://img.youtube.com/vi/M84OHIWdFX0/maxresdefault.jpg alt='如何打造高性能,快速且SEO友好的SPA网页 - Jecelyn Yeen'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="如何打造高性能,快速且SEO友好的SPA网页 - Jecelyn Yeen"> </iframe> </div> <p>在此附上一张我在大会拍的照片。</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/spa-performance-zh-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/spa-performance-zh-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/spa-performance-zh-02.jpg"> <img src="https://jec.fish/assets/img/deck/spa-performance-zh-02.jpg" alt="生平第一次脸被印在这么一块大板上,要好好纪念。把Angular程序猿也带上。"> </picture> </div><figcaption>生平第一次脸被印在这么一块大板上,要好好纪念。把Angular程序猿也带上。</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261711976993943552"> twitter.com/jecfish/status/1261711976993943552 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div></content> </entry> <entry> <title>Introducing New Web APIs and Stylings</title> <link href="https://jec.fish/deck/new-web-apis-and-stylings"/> <updated>2019-12-09T00:00:00-00:00</updated> <id>https://jec.fish/deck/new-web-apis-and-stylings</id> <summary>What are the new and upcoming web APIs & shiny new CSS features?</summary> <category term="deck"/> <content type="html"><p>Really ah? The web is so &quot;geng&quot; (incredibly powerful) meh? This talk will cover the new and upcoming web APIs &amp; shiny new CSS features to build beautiful, fast, and powerful web apps.</p> <p>The full title of this talk was actually &quot;Web Can Do This Meh?! Introducing New Web APIs &amp; Stylings ✨&quot;. 💯 made in <strong>#malaysia</strong>, haha.</p> <p>Here are the slides 👉🏼</p> <script async="" class="speakerdeck-embed" data-id="6836e4b92aae481a9c64d87a4093f9ac" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/new-web-api-and-stylings">https://speakerdeck.com/jecfish/new-web-api-and-stylings</a> </noscript> <p>.</p> <p>This talk was delivered in <a href="https://devfest.gdgkl.dev/schedule/121">DevFest KL 2019</a>. No video recorded. Here is a photo of me during the conference.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:66.69921875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/new-web-apis-and-stylings-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/new-web-apis-and-stylings-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/new-web-apis-and-stylings-01.jpg"> <img src="https://jec.fish/assets/img/deck/new-web-apis-and-stylings-01.jpg" alt="Groupie with the speakers and cute dino!"> </picture> </div><figcaption>Groupie with the speakers and cute dino!</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261712164282232832"> twitter.com/jecfish/status/1261712164282232832 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Becoming a GDE - What, Why, and How</title> <link href="https://jec.fish/deck/becoming-a-gde"/> <updated>2020-05-10T00:00:00-00:00</updated> <id>https://jec.fish/deck/becoming-a-gde</id> <summary>Talk about GDE the program & sharing my 4 years' experience in the program.</summary> <category term="deck"/> <content type="html"><p>I was a Google Developer Expert (GDE) on Angular and Web technologies from 2016 to 2020. Here is my sharing on the GDE program - what is it about, how to become one, and my 4 years experience in it.</p> <p>Oh, I am no longer a GDE anymore. Why is that? Read on! 👀</p> <p>Here's the slides:</p> <script async="" class="speakerdeck-embed" data-id="2e0da86de886408c8ad2a1ca9c7f992e" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/becoming-a-gde">https://speakerdeck.com/jecfish/becoming-a-gde</a> </noscript> <h2>Official GDE program information</h2> <ul> <li>GDE program details <a href="https://developers.google.com/community/experts">here</a></li> <li>GDE directory <a href="https://developers.google.com/community/experts/directory">here</a></li> </ul> <h2>GDE community resources</h2> <ul> <li>Aaron Frosty, Angular GDE: <a href="https://medium.com/@frosty/preparing-to-become-a-gde-752b551c88df">Preparing to be a GDE</a></li> <li>Stephen Fluin, Developer relations, Googler: <a href="https://fluin.io/blog/how-to-become-an-angular-gde">How to Become an Angular GDE</a></li> <li>Wajat Karim, Android GDE: <a href="https://wajahatkarim.com/2020/02/gde/">Becoming a GDE in Android</a></li> </ul> <h2>How's my experience with the program?</h2> <p>It's been a <strong>fruitful experience</strong>. Throughout these four years, I visited various countries, spoke in different conferences, and met and collaborated with my fellow GDEs.</p> <p>The program has widened my horizons and challenged me to keep on learning &amp; growing. Here's a review of my past 4 years of community involvement, partly enabled by the GDE program.</p> <h3>Year 2016</h3> <p>First year in the program. Visited both the US &amp; attended Chrome Dev Summit (CDS) for the very first time! How excited I was to meet the Chrome team members and developers in real life. 🤩 Checking out what's available in the booths is equally exciting. For me, this experience set the standard of what a good tech conference experience should be.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:62.5%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-07.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-07.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-07.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-07.jpg" alt="Paul Irish talks about Chrome DevTools at CDS"> </picture> </div><figcaption>Paul Irish talks about Chrome DevTools at CDS</figcaption></figure> <p>Also did a video shooting for the first time as well, so nervous! It was the <a href="https://youtu.be/kEieXe-3DGU">web developer portrait</a> series by Google. Walking on the street, talking to myself and NGs for... quite a few times.</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/kEieXe-3DGU" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/kEieXe-3DGU?autoplay=1><img src=https://img.youtube.com/vi/kEieXe-3DGU/maxresdefault.jpg alt='Web Developer Portrait - Jecelyn Yeen'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Web Developer Portrait - Jecelyn Yeen"> </iframe> </div> <p>Continued to host local meetups and workshops as I always do. Slowly received more invitations to speak in cross state local events. Also continued to publish online tutorials on <a href="https://scotch.io/@jecelyn">scotch.io</a> and <a href="https://medium.com/@jecfish">Medium</a>.</p> <h3>Year 2017</h3> <p>Attended <a href="https://events.google.com/io/">Google I/O</a>, the annual developer conference for the first time. Sitting at the front row during the keynote! Met my fellow <a href="https://www.womenwhocode.com/">Women Who Code</a> from around the world during the conference. (Can you spot me? 🕵🏼♀️)</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-01.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-01.jpg" alt="Women Who Code at Google IO"> </picture> </div><figcaption>Women Who Code at Google IO</figcaption></figure> <p>Also attended <a href="https://2017.ng-conf.org/">ng-conf</a>, the original Angular conference @ Salt Lake City, US. Thanks to the organizer <a href="https://twitter.com/aaronfrost?lang=en">@Frosty</a> (Angular GDE) for sponsoring my ticket, which I could not afford. The conference was very well-organized, entertaining and insightful. This amazing experience inspired me to start planning a conference to bring this wonderful experience closer to home.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-08.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-08.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-08.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-08.jpg" alt="Raul, Jorge & me (Angular GDEs) at ng-conf"> </picture> </div><figcaption>Raul, Jorge & me (Angular GDEs) at ng-conf</figcaption></figure> <p>The same year, I got accepted to my first two overseas conferences @ <a href="https://codecamp.ro/">Codecamp, Romania</a> &amp; <a href="https://youtu.be/rIpzhgz5Y_o">DevFest, Cebu, Phillipines</a>! Big crowd, both with over 800+ attendees. Oh, I also did a sharing in Phnom Penh, Cambodia.</p> <h3>Year 2018</h3> <p>As part of my plan towards organizing an international conference, I took up the role to organize <a href="https://sunwayecho.wordpress.com/2018/07/25/google-i-o-extended-kuala-lumpur-2018/">Google I/O Extended in Kuala Lumpur</a>. A lot of first times for me personally. From drafting sponsorship proposal and budgeting to event scheduling and printing merchandise. I'm grateful to have gotten valuable advice from other GDEs and help from the volunteers.</p> <p>We had 400 attendees. The turn up rate was 96%, which was very good for Malaysian standards. Our efforts on promoting a responsible culture paid off.</p> <p>At the same time, I've unlocked more overseas speaking too @ Germany, Taiwan &amp; US. Good content matters, but the GDE title did (probably) put me in a better position, I guess.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:44.7265625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-02.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-02.jpg" alt="Google IO Extended KL 2018"> </picture> </div><figcaption>Google IO Extended KL 2018</figcaption></figure> <h3>Year 2019</h3> <p>2019 is indeed a busy year for me. Full of conference travels, and finally realizing a long dream of mine - <strong><a href="https://ng-my.org/">NG-MY</a></strong>, the first Angular conference in the region!</p> <p>Challenging indeed. Although I gained some experiences last year, this time it is fully self-funded. The stress of 💵 is real, since the event scale is bigger because we planned to invite international speakers. With the support from my fellow Angular GDEs (speaking, sponsoring, sharing contacts and advice) and my awesome <a href="https://2019.ng-my.org/team">team</a>, we made it!</p> <p>It's a successful one. Good mix of international and regional speakers, 400+ attendees from 30+ countries, fun and loads of useful content. We also sponsored about 40 scholarships to help the local &amp; regional attendees come together.</p> <p>Oh, did I mention that I travelled to 12 countries, 25+ cities in the same year, and spoke in 25+ events? It was a crazy year!</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:74.51171875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-03.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-03.jpg" alt="NG-MY 2019"> </picture> </div><figcaption>NG-MY 2019</figcaption></figure> <h3>Year 2020</h3> <p>After an eventful 2019, I decided to take a break in early 2020. Oh, COVID-19! 🦠 It's a challenging year ahead for many of us. 😔 I hope we, the human race, win this battle together.</p> <p>Another big decision for me was... <strong>I joined Google</strong> as a Developer Advocate for Google Chrome. I will be relocating to Munich, Germany, but it was delayed. Memorable start indeed during this pandemic.</p> <p>Thanks to technology, I still got to meet my teammates virtually. Looking forward to working together with the talented team, continuing to bring you better developer experiences and productivity on the DevTools.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:70.80078125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-06.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-06.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-06.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-06.jpg" alt="I joined Google!"> </picture> </div><figcaption>I joined Google!</figcaption></figure> <h2>Summary</h2> <p>Looking back, it was an amazing 4 years! I am glad to be part of this big GDE family. Gaining new friendship, unlocking new skills, learning, growing and contributing back to the community at the same time. The program opened up tremendous new opportunities for me. I might probably not have achieved all the above without the support from the program and the community.</p> <p>From 2016, when I first attended CDS, to 2020, when I joined the Chrome team. Who’d have thought? Alright, last photo 👇🏼, some of us having fun during the GDE Summit after party last year.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:75%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/becoming-a-gde-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/becoming-a-gde-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/becoming-a-gde-04.jpg"> <img src="https://jec.fish/assets/img/deck/becoming-a-gde-04.jpg" alt="GDE Summit after party with the bunch"> </picture> </div><figcaption>GDE Summit after party with the bunch</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1259888881765240832"> twitter.com/jecfish/status/1259888881765240832 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Links to My Previous Write-Ups</title> <link href="https://jec.fish/blog/past-write-ups"/> <updated>2020-05-13T00:00:00-00:00</updated> <id>https://jec.fish/blog/past-write-ups</id> <summary>Links to my old write-ups and where to find them.</summary> <category term="blog"/> <content type="html"><p>** <em>Drum roll</em> **</p> <p>Finally, I rolled out my own website! Have been avoiding doing that for a long long time (because I am lazy). 😂</p> <p>Anyway, it's here - <a href="https://jec.fish/">jec.fish</a> would be the new home for my blogging moving forward.</p> <p>The old articles are still available, it will stay where they are now. 😃 (I haven't think about what to do with them yet)</p> <p>Here are the links to my old articles:</p> <ul> <li><a href="https://scotch.io/@jecelyn">scotch.io/@jecelyn</a>: It's a good platform for tech publishing. Chris (<a href="https://twitter.com/intent/follow?screen_name=chrisoncode">@chrisoncode</a>), the founder is a really nice person to work with.</li> <li><a href="https://medium.com/@jecfish">medium.com/@jecfish</a>: I like Medium clap feature. Its paywall announcement has triggered quite some negative responses, tech community particularly. Many flocked to other platforms since then (e.g. <a href="https://dev.to/">DEV.to</a>). I wasn't leaving back then, but I am now. 😁</li> </ul> <p>Thanks for reading! And thanks for follow my writings. 🙇🏻♀️</p> <p>Shameless plug - Follow me on Twitter <a href="https://twitter.com/jecfish">@jecfish</a> for updates on tech tips and new write-ups!</p> <!-- <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/"> twitter.com/jecfish/status/ </a> </blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> --> </content> </entry> <entry> <title>Building Personal Static Site with Eleventy</title> <link href="https://jec.fish/blog/building-my-static-site-with-11ty"/> <updated>2020-05-14T00:00:00-00:00</updated> <id>https://jec.fish/blog/building-my-static-site-with-11ty</id> <summary>A blog series on tips and tricks of building static site with 11ty.</summary> <category term="blog"/> <content type="html"><p>I built this site with Eleventy (aka. 11ty), a modern static site generator. Why did I choose 11ty? How to set it up? Is it a good choice? Are there any gotchas? Read on!</p> <p>As a long time web developer 👩🏼💻, I've never used any site generator before, not even WordPress... for the past 10+ years. Only vanilla JavaScript, frameworks &amp; jQuery. The web dev scene is so diverse, right? 😄</p> <p>(If you just want to see the code, here's the GitHub repo <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>.)</p> <h2>Why 11ty?</h2> <p>Honestly, I thought about rolling my own site from scratch using frameworks (Angular, Preact), because why not? 😆 Then I defeat myself with my own laziness.</p> <p>Let's just pick a modern, shiny <a href="https://jamstack.org/">JAM stack</a> or static site generator from the market!</p> <p>The immediate &quot;framework-ish&quot; choices for me are <a href="https://scully.io/">Scully (Angular)</a>, <a href="https://nuxtjs.org/">Nuxt (Vue)</a> and <a href="https://www.gatsbyjs.org/">Gatsby (React)</a>. Nope, if I am going to use frameworks, I want to build from scratch, hah! (What weird logic 😂)</p> <p>I recalled several people mentioning <a href="https://www.11ty.dev/">11ty</a>. Did a quick check on the <a href="https://www.11ty.dev/">official site</a>, <a href="https://www.11ty.dev/docs/getting-started/">getting started</a> and the <a href="https://www.11ty.dev/docs/">documentation</a>. Although the documentation looks a bit overwhelming, it seems legit and easy to start. One interesting &amp; convincing discovery for me was that <a href="https://www.11ty.dev/#built-with-eleventy">web.dev</a>, <a href="https://developer.chrome.com/devsummit/">Chrome Dev Summit 2019</a>, <a href="https://eslint.org/">ESLint</a>, and <a href="https://v8.dev/">Google V8 blog</a> are built with 11ty!</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:59.86328125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/building-my-static-site-with-11ty-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/building-my-static-site-with-11ty-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/building-my-static-site-with-11ty-01.jpg"> <img src="https://jec.fish/assets/img/blog/building-my-static-site-with-11ty-01.jpg" alt="web.dev is built with 11ty"> </picture> </div><figcaption>web.dev is built with 11ty</figcaption></figure> <p>Googling further, I found this step-by-step <a href="https://keepinguptodate.com/pages/2019/06/creating-blog-with-eleventy/">tutorial</a> by <a href="https://twitter.com/jon_keeping">Jon Keeping</a>. I followed through the steps to build a basic blog. The tutorial gave a good overview on what 11ty provides. In short - <strong>minimal setup yet customizable through several out-of-the-box features</strong>.</p> <p>As a developer that is familiar with Angular and Vue, the out-of-the-box features are similar to these frameworks' concept of filter, pipe, component, and directive. I feel comfortable with it.</p> <p>Sorry WordPress, Hugo, Jekyll, and Hexo! I did not really do detailed research and comparison.</p> <p>Building a personal blog is not a big life decision anyway. The tooling choice is personal. Pick a comfortable one and just go for it. If it fails, just dump it and pick another lah~</p> <p>That being said, I like the 11ty experience so far!</p> <h2>My 11ty configuration</h2> <p>Although 11ty works with zero config out of the box, that's not what I want. The good news is we can customize that. I will go through a few of my settings.</p> <h3>Setup folder structure</h3> <p>This is the standard folder structure that I want my project to be.</p> <pre class="language-text"><code class="language-text">- assets /* images, etc */<br>- src /* all source files */<br>- dist /* all files to be deployed */<br>- package.json <br>- .eleventy.js /* 11ty config file */</code></pre> <p>Out of the box, the 11ty configuration is different. We can customize that in the config file <code>.eleventy.js</code>.</p> <pre class="language-js"><code class="language-js"><span class="token comment">/* .eleventy.js */</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <br> <span class="token comment">// set copy asset folder to dist</span><br> eleventyConfig<span class="token punctuation">.</span><span class="token function">addPassthroughCopy</span><span class="token punctuation">(</span><span class="token string">'assets'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token comment">// set input and output folder</span><br> <span class="token keyword">return</span> <span class="token punctuation">{</span><br> dir<span class="token operator">:</span> <span class="token punctuation">{</span> input<span class="token operator">:</span> <span class="token string">'src'</span><span class="token punctuation">,</span> output<span class="token operator">:</span> <span class="token string">'dist'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br></code></pre> <p>More configuration options can be found in the <a href="https://www.11ty.dev/docs/config/">documentation</a>.</p> <h3>Pick template languages</h3> <p>11ty supports quite a number of template languages, full list of languages available <a href="https://www.11ty.dev/docs/languages/">here</a>. That being said, each language support and documentation varies, though. You can actually mix and use all of the template languages 😆, but let's deduce, pick, and focus.</p> <p>I decided to go with:</p> <ul> <li><a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> - a templating language open sourced by Mozilla, usually with file extension <code>.njk</code>.</li> <li><a href="https://daringfireball.net/projects/markdown/syntax">Markdown</a> - Markdown is everywhere, with file extension <code>.md</code>.</li> </ul> <p>Another common option is Shopify's <a href="https://shopify.github.io/liquid/">Liquid</a>. After reading both docs, Nunjucks obviously has more advanced features (e.g. <a href="https://mozilla.github.io/nunjucks/templating.html#extends">extends</a> and <a href="https://mozilla.github.io/nunjucks/templating.html#macro">macro</a>). Honestly, I'm not sure if I need these features, but no harm going with that, given both languages' syntax are quite similar. By the way, you may also use <code>EJS, PUG, HAML, Mustache, Handlebars</code> and more.</p> <p>Let's update our configuration file to reflect that.</p> <pre class="language-js"><code class="language-js"><span class="token comment">/* .eleventy.js */</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <br> <span class="token comment">// set copy asset folder to dist</span><br> eleventyConfig<span class="token punctuation">.</span><span class="token function">addPassthroughCopy</span><span class="token punctuation">(</span><span class="token string">'assets'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token comment">// set input and output folder</span><br> <span class="token keyword">return</span> <span class="token punctuation">{</span><br> dir<span class="token operator">:</span> <span class="token punctuation">{</span> input<span class="token operator">:</span> <span class="token string">'src'</span><span class="token punctuation">,</span> output<span class="token operator">:</span> <span class="token string">'dist'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> dataTemplateEngine<span class="token operator">:</span> <span class="token string">'njk'</span><span class="token punctuation">,</span><br> markdownTemplateEngine<span class="token operator">:</span> <span class="token string">'njk'</span><br> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <h3>Setting up NPM scripts</h3> <p>Let's take a look at my <code>package.json</code> and see some of the helper scripts I have.</p> <pre class="language-json"><code class="language-json"><span class="token comment">/* .package.json */</span><br><br><span class="token punctuation">{</span><br> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"jec-fyi"</span><span class="token punctuation">,</span><br> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"clean"</span><span class="token operator">:</span> <span class="token string">"npx del dist"</span><span class="token punctuation">,</span><br> <span class="token property">"serve"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=dev npx eleventy --serve"</span><span class="token punctuation">,</span><br> <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"npm run serve"</span><span class="token punctuation">,</span><br> <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=dev npx eleventy"</span><span class="token punctuation">,</span><br> <span class="token property">"build:prod"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=prod npx eleventy"</span><span class="token punctuation">,</span><br> <span class="token property">"debug"</span><span class="token operator">:</span> <span class="token string">"DEBUG=* npx eleventy"</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token property">"devDependencies"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"@11ty/eleventy"</span><span class="token operator">:</span> <span class="token string">"^0.10.0"</span><span class="token punctuation">,</span><br> <span class="token property">"del-cli"</span><span class="token operator">:</span> <span class="token string">"^3.0.0"</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>A very clean start indeed, only two dev dependencies. 😆</p> <p>Here're the explanation of the setup above:</p> <ol> <li>I always like my project to have a <code>start</code> script, to indicate the project starting point.</li> <li>Setting up <code>clean</code> script to completely clear the output directory.</li> <li>To build an 11ty project, the <code>eleventy</code> command is all you need. To run a local server, just add <code>--serve</code> behind it.</li> <li>In my script, I use <code>ELEVENTY_ENV</code> environment variable to indicate the <code>dev</code> and <code>prod</code>. I'll need this to enable / disable some of my code. It's entirely optional, and you may not need this. (Alternatively, you can use a <code>.env</code> file or set it in your terminal / cmd)</li> </ol> <p>Take note that the <code>eleventy</code> command will not clean your output directory, but update the files within the directory instead. If you want a clean build every time, add the two scripts below:</p> <pre class="language-json"><code class="language-json"><span class="token comment">/* .package.json */</span><br><br><span class="token punctuation">{</span><br> ...<br> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"prestart"</span><span class="token operator">:</span> <span class="token string">"npm run clean"</span><span class="token punctuation">,</span><br> <span class="token property">"prebuild"</span><span class="token operator">:</span> <span class="token string">"npm run clean"</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>I did not add the two scripts above because I'll run my build on my CI/CD server (will write about <code>GitHub Actions</code> in a coming post), it would be a clean build every time. It didn't bother me that much in local (sometimes it does, heh).</p> <div class="notes"> <p>Extra notes:</p> <ul> <li><code>npx</code> is a built-in command in npm, allowing you to run local packages' commands. More details <a href="https://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner">here</a> <br></li> <li>npm provides <code>pre</code> and <code>post</code> hook for commands. e.g. in our setup, when you run <code>npm start</code>, it will first run <code>prestart</code> automatically.</li> </ul> </div> <p>Cool! Let's run <code>npm install</code> then <code>npm start</code> now. We are good to go!</p> <h3>Building first page</h3> <p>Let's start building a page to see if this works. Create an <code>index.njk</code> file in the <code>src</code> folder.</p> <p>Assuming you are running the <code>npm start</code> command, 11ty will watch your <code>src</code> folder for changes.</p> <pre class="language-html"><code class="language-html"><br><span class="token comment">&lt;!-- index.njk --></span><br>---<br>title: Home page <br>date: 2020-05-10<br>---<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> {% set greeting = 'hello' %}<br> <br> Yay, {{ greeting | upper }} world!<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br><br></code></pre> <p>Now check the <code>dist</code> folder, and you can see <code>index.html</code> file is generated. Open browser and hit <code>localhost:8080</code>. You should see <code>Yay, HELLO world!</code> showing on screen with <code>Home page</code> as the title.</p> <p>From the example above, you can already see some of the features provided by 11ty and Nunjucks.</p> <ol> <li><code>.njk</code> file will be rendered into <code>.html</code> file in <code>dist</code> folder.</li> <li>Nunjucks uses <code>{% %}</code> and <code>{{ }}</code> syntax.</li> <li>The top part enclosed with <code>---</code> is called <a href="https://www.11ty.dev/docs/data-frontmatter/">Front Matter</a>. It's a way for us to set and pass data around. (Will explore further in upcoming posts)</li> <li><code>upper</code> is something we refer to as <code>filter</code>. There are quite some number of built-in filters, check <a href="https://mozilla.github.io/nunjucks/templating.html#builtin-filters">Nunjucks filters</a> and <a href="https://www.11ty.dev/docs/filters/">11ty fitlers</a>. In the coming post, we will explore how to create our own filter! 😃</li> </ol> <h2>IDE Extensions &amp; Settings</h2> <p>I am using VSCode. These are the few VSCode plugins that can help boost your development productivity. After all, who likes to keep typing <code>{% %}</code>, hah.</p> <ul> <li><strong>Nunjucks</strong> by ronnidc - code highlight for <code>.njk</code> files.</li> <li><strong>Nunjucks Snippets</strong> by luwenjiechn - provide snippets like <code>var</code>, <code>njk</code>.</li> <li><strong>Nunjucks Template Formatter</strong> by Nanda Okitavera - right click <code>format</code> your <code>.njk</code> files.</li> <li><strong>Prettier - Code formatter</strong> by Esben Petersen - generally, this is a good extension to format your codes - JavaScript, Markdown, CSS and more.</li> </ul> <div class="notes"> <p>Gotcha:</p> <p>There is a bug in the <strong>Nunjucks by ronnidc</strong> extension as of the time of writing. Syntax autocomplete (Emmet) doesn't work, refer to this <a href="https://github.com/ronnidc/vscode-nunjucks/issues/7">GitHub issue</a>. There is a workaround, add the below setting in your vscode <code>settings.json</code> file manually (shortcut to open the file: <code>CMD + OPTION + P</code> &gt; <code>Open Settings (JSON)</code>) and restart your IDE.</p> <pre class="language-js"><code class="language-js"><span class="token comment">/* vscode settings.json */</span><br><br><span class="token string">"emmet.includeLanguages"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"njk"</span><span class="token operator">:</span> <span class="token string">"html"</span><span class="token punctuation">,</span><br> <span class="token string">"nunjucks"</span><span class="token operator">:</span> <span class="token string">"html"</span><br><span class="token punctuation">}</span></code></pre> </div> <h2>Alrighty, what's next?</h2> <p>That's all for now. We have successfully set up the project. That's how my site <a href="https://jec.fish/">jec.fish</a> was set up as well. In the coming posts, I plan to write about:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you. I highly recommend you to try out 11ty yourself! The development experience has been great so far.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>Happy coding!</p> <p><em><small>The <a href="https://jec.fish/">jec.fish</a> website itself isn't open source yet, but it will soon! Some housekeeping is needed before that. 😛 </small></em></p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1260845853482250240"> twitter.com/jecfish/status/1260845853482250240 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Setting up GitHub Actions and Firebase Hosting</title> <link href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting"/> <updated>2020-05-15T00:00:00-00:00</updated> <id>https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting</id> <summary>Automate deployment with GitHub Actions and Firebase Hosting</summary> <category term="blog"/> <content type="html"><p>Both Firebase Hosting &amp; GitHub Actions offer generous free quota (with limit of course), perfect for our personal project!</p> <p>Let's use these tools to set up our hosting and automate our deployment workflow.</p> <p>Feel free to skip any sections (that you already know) or dive into the source code <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> straight away!</p> <p>This is the second post of the series. Here is the previous post - <a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a>. It's fine to continue reading this though. The basic setup is similar among projects.</p> <div class="notes"> <p>Refer to official documentation for latest pricing:</p> <ul> <li><a href="https://firebase.google.com/pricing">Firebase pricing</a></li> <li><a href="https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions">GitHub Actions Pricing</a></li> </ul> </div> <h2>Setting up Firebase Hosting</h2> <div class="notes"> <p>Please complete these two prerequisites before we continue:</p> <p>a. Sign up &amp; login to <a href="https://console.firebase.google.com/">console.firebase.google.com</a>.<br> b. Create a project, follow the steps on screen. Give your project a nice name.</p> </div> <p>In your project, open up the terminal and follow the steps below:</p> <ol> <li>Run <code>npm install firebase-tools -D</code> in the terminal. This will install Firebase CLI as our project's dev dependency.</li> <li>Run <code>npx firebase login</code> to sign in using your Google / Firebase account.</li> <li>Run <code>npx firebase init hosting</code> - Firebase provides a lot of other features, but we only need hosting for now.</li> <li>Select <code>Use an existing project</code> and choose the project created in prerequisite step b.</li> <li>It will prompt you 2-3 more questions along the way. Just press enter or answer anything. You will be fine, no worries 😆. Not convinced? Follow the my answers below lol.</li> </ol> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:41.30859375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-github-actions-and-firebase-hosting-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-github-actions-and-firebase-hosting-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-github-actions-and-firebase-hosting-03.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-github-actions-and-firebase-hosting-03.jpg" alt="Firebase prompts"> </picture> </div><figcaption>Firebase prompts</figcaption></figure> <h2>Why the steps?</h2> <p>The steps above will eventually create two files in the project - <code>.firebaserc</code> and <code>firebase.json</code>. We can edit both (that's why no worries, heh).</p> <p>Let's look into each of these files.</p> <p><code>.firebaserc</code> stores the Firebase project name. In case you chose the wrong project in step 4 earlier, you may update it here.</p> <pre class="language-json"><code class="language-json"><span class="token comment">// .firebaserc</span><br><br><span class="token punctuation">{</span><br> <span class="token property">"projects"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"default"</span><span class="token operator">:</span> <span class="token string">"[your_firebase_project_name]"</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br></code></pre> <p>Next, <code>firebase.json</code> stores our project configuration. Replace your file with the code below. 👇🏼</p> <pre class="language-json"><code class="language-json"><span class="token comment">// firebase.json</span><br><br><span class="token punctuation">{</span><br> <span class="token property">"hosting"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"public"</span><span class="token operator">:</span> <span class="token string">"dist"</span><span class="token punctuation">,</span><br> <span class="token property">"cleanUrls"</span><span class="token operator">:</span> <span class="token string">"true"</span><span class="token punctuation">,</span><br> <span class="token property">"ignore"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br> <span class="token string">"firebase.json"</span><span class="token punctuation">,</span><br> <span class="token string">"**/.*"</span><span class="token punctuation">,</span><br> <span class="token string">"**/node_modules/**"</span><span class="token punctuation">,</span><br> <span class="token punctuation">]</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>A few explanations on the configuration above,</p> <ul> <li><code>public</code> indicates the folder that we will deploy / upload to Firebase Hosting, our deployment folder is <code>dist</code>.</li> <li><code>ignore</code> as the name indicates, ignore any files (do not deploy) that matches the provided names.</li> <li><code>cleanUrls</code> allows us to control our <code>.html</code> extension. (Extra explanation needed)</li> </ul> <div class="notes"> <p>Suppose we have the following html document:</p> <pre class="language-text"><code class="language-text">- about.html<br>- licenses.html</code></pre> <p>When the user navigates to our pages in browser, the URLs would be:</p> <pre class="language-text"><code class="language-text">- your_domain.com/about.html<br>- your_domain.com/licenses.html</code></pre> <p>Urgh, not that pretty right? It would be nice if our URLs looked prettier, like:</p> <pre class="language-text"><code class="language-text">- your_domain.com/about<br>- your_domain.com/licenses</code></pre> <p>That's what the <code>cleanUrls</code> setting does. Refer to the <a href="https://firebase.google.com/docs/hosting/full-config#control_html_extensions">Firebase documentation</a> for more details.</p> </div> <h2>Setting up NPM scripts</h2> <p>Alright, Firebase Hosting is ready. Next, add our deployment scripts in <code>package.json</code>:</p> <pre class="language-json"><code class="language-json"><span class="token comment">/* .package.json */</span><br><br><span class="token punctuation">{</span><br> ...<br> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> ...<br> <span class="token property">"clean"</span><span class="token operator">:</span> <span class="token string">"npx del dist"</span><span class="token punctuation">,</span><br> <span class="token property">"prebuild"</span><span class="token operator">:</span> <span class="token string">"npm run clean"</span><span class="token punctuation">,</span><br> <span class="token property">"build:prod"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=prod npx eleventy"</span><span class="token punctuation">,</span><br> <span class="token property">"predeploy"</span><span class="token operator">:</span> <span class="token string">"npm run build:prod"</span><span class="token punctuation">,</span><br> <span class="token property">"deploy"</span><span class="token operator">:</span> <span class="token string">"npx firebase deploy"</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>All set! Let's try to run <code>npm run deploy</code> in your terminal. The below tasks will be executed in sequence:</p> <ol> <li>Delete the <code>dist</code> folder if it exists.</li> <li>11ty will build and compile your <code>src</code> files to the <code>dist</code> folder.</li> <li>Firebase will deploy all the files in the <code>dist</code> folder to its hosting service.</li> </ol> <p>As mentioned in the previous <a href="https://jec.fish/blog/building-my-static-site-with-11ty">post</a>, the <code>prebuild</code> task is optional. You may not need that. Also, if you prefer to run the build and deploy scripts separately, feel free to remove the <code>predeploy</code> script.</p> <h2>Nice, so where's my url?</h2> <p>Your URL should appear in the terminal upon successful deployment. Otherwise, go to your <a href="https://console.firebase.google.com/">Firebase console</a>, select the project and click on the <code>Hosting</code> menu. At the page, you should see the URL Firebase has created for you.</p> <p>Copy the URL and open it in the browser. You should see <code>HELLO world!</code> on screen.</p> <p>If you bought a domain (e.g. my domain is <a href="https://jec.fish/">jec.fish</a>), you can start setting it up by clicking on the <code>Add custom domain</code> button. Follow the instructions on screen and you'll be fine.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:47.36328125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-github-actions-and-firebase-hosting-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-github-actions-and-firebase-hosting-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-github-actions-and-firebase-hosting-04.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-github-actions-and-firebase-hosting-04.jpg" alt="Add custom domain in Firebase Hosting"> </picture> </div><figcaption>Add custom domain in Firebase Hosting</figcaption></figure> <h2>Setting up Github Actions</h2> <p>As a developer, we don't want to run the deployment step manually every time. Let's use GitHub Actions to automate that!</p> <p>In your project folder,</p> <ol> <li>Create a new folder <code>.github</code>.</li> <li>Create a <code>workflows</code> folder under <code>.github</code>.</li> <li>Create a <code>main.yml</code> file in the <code>workflows</code> folder. You can rename the file freely, not necessary <code>main</code>.</li> </ol> <h3>First step to automate our deployment</h3> <p>First, we need to decide when to deploy. In our case, the project is small. It's a personal website.</p> <p>We want to deploy when either condition is met:</p> <ul> <li>Whenever changes pushed to <code>master</code> branch</li> <li>Everyday, 9pm, Malaysia time.</li> </ul> <h3>Configure our workflow</h3> <p>Let's configure our <code>main.yml</code> to reflect that.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># main.yml</span><br><br><span class="token key atrule">name</span><span class="token punctuation">:</span> CI <span class="token comment"># Give it any name</span><br><br><span class="token key atrule">on</span><span class="token punctuation">:</span><br> <span class="token key atrule">push</span><span class="token punctuation">:</span><br> <span class="token key atrule">branches</span><span class="token punctuation">:</span><br> <span class="token punctuation">-</span> master<br> <span class="token key atrule">schedule</span><span class="token punctuation">:</span><br> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> <span class="token string">'0 13 * * *'</span> <span class="token comment"># daily 9pm MYT</span><br></code></pre> <p>The configuration is quite expressive itself. The schedule is using the cron syntax. If you want to learn more about it, go to <a href="https://crontab.guru/">crontab.guru</a> (keep pressing the <code>random</code> link on screen to learn further 😆).</p> <p>Another gotcha: you might be wondering why <code>13:00</code> is <code>9pm MYT</code>. Well, GitHub does not support timezones yet, so it uses UTC time. Do your own math or use <a href="https://www.worldtimebuddy.com/?pl=1&amp;lid=100,1735161&amp;h=100">worldtimebuddy.com</a> to do the conversion beforehand. If you'd like to have built-in timezones, go upvote the feature in <a href="https://github.community/t5/GitHub-Actions/Timezone-support-for-scheduled-actions/td-p/42170">GitHub Community</a>!</p> <h3>Define our build steps</h3> <p>Next, let's add in our step-by-step instructions in <code>main.yml</code>. We want to:</p> <ol> <li>Run the deployment tasks on Linux (Windows and macOS are available too, but more expensive though).</li> <li>Checkout the source code from the repository.</li> <li>Install Node.js.</li> <li>Run <code>npm install</code> to install dependencies.</li> <li>Run the <code>deploy</code> script we created earlier to build and deploy to Firebase (<code>npm run deploy</code>).</li> </ol> <p>Convert the above steps into <code>main.yml</code>.</p> <pre class="language-yml"><code class="language-yml"><br><span class="token comment"># main.yml</span><br><span class="token punctuation">...</span><br><br><span class="token key atrule">jobs</span><span class="token punctuation">:</span><br> <span class="token key atrule">build</span><span class="token punctuation">:</span><br> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest<br><br> <span class="token key atrule">steps</span><span class="token punctuation">:</span><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Checkout branch<br> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v1<br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Use Node.js<br> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>node@v1<br> <span class="token key atrule">with</span><span class="token punctuation">:</span><br> <span class="token key atrule">node-version</span><span class="token punctuation">:</span> <span class="token number">12.16</span><br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Install dependencies<br> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm install<br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Build &amp; deploy<br> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm run deploy<br> <span class="token key atrule">env</span><span class="token punctuation">:</span><br> <span class="token key atrule">CI</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br> <span class="token key atrule">FIREBASE_TOKEN</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.YOUR_SECRET <span class="token punctuation">}</span><span class="token punctuation">}</span><br></code></pre> <p>The steps are quite expressive itself. A few explanations on the fields.</p> <ul> <li><code>name</code> - It's good to name each step, but it's optional</li> <li><code>run</code> - The command to execute in terminal</li> <li><code>uses</code> - The pre-built actions you want to use, if any. Find them in <a href="https://github.com/marketplace?type=actions&amp;query=">GitHub Marketplace</a>, pick and utilize those.</li> <li><code>env</code> - Specify the environment variables for the step. (e.g. we set <code>ELEVENTY_ENV</code> in our npm script, it's ok to put it here instead).</li> <li><code>FIREBASE_TOKEN</code> env variable - Firebase needs this token to authorize the deployment (because we can't run <code>npx firebase login</code> remotely). Generate the token beforehand by running <code>npx firebase login:ci</code> command in your machine. Detailed instructions <a href="https://github.com/firebase/firebase-tools#using-with-ci-systems">here</a></li> <li>The <code>secrets.YOUR_SECRET</code> - The token is sensitive information, so we need a safe place to store it. Copy the token and create a new secret in GitHub Repository &gt; Settings &gt; Secrets. Name it <code>YOUR_SECRET</code> or feel free to rename it whatever you want (remember to update the variable name in <code>main.yml</code> if you do so).</li> </ul> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:43.75%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-github-actions-and-firebase-hosting-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-github-actions-and-firebase-hosting-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-github-actions-and-firebase-hosting-01.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-github-actions-and-firebase-hosting-01.jpg" alt="Add a new secret in GitHub repo > Settings > Secrets"> </picture> </div><figcaption>Add a new secret in GitHub repo > Settings > Secrets</figcaption></figure> <h3>Run &amp; test our GitHub Actions</h3> <p>All good! Let's push your changes to the <code>master</code> branch. Open your GitHub repository page, and view your deployment progress in the <code>Actions</code> tab.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:37.5%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-github-actions-and-firebase-hosting-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-github-actions-and-firebase-hosting-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-github-actions-and-firebase-hosting-02.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-github-actions-and-firebase-hosting-02.jpg" alt="See your GitHub Actions in action"> </picture> </div><figcaption>See your GitHub Actions in action</figcaption></figure> <p>You can drill down further to see the status of each step, the steps shown on screen are the <code>name</code> we defined in <code>main.yml</code> (give your steps meaningful names).</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:52.05078125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-github-actions-and-firebase-hosting-05.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-github-actions-and-firebase-hosting-05.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-github-actions-and-firebase-hosting-05.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-github-actions-and-firebase-hosting-05.jpg" alt="Add a new secret in GitHub repo > Settings > Secrets"> </picture> </div><figcaption>Add a new secret in GitHub repo > Settings > Secrets</figcaption></figure> <div class="notes"> <p>Explore further:</p> <p>You may want a totally different configuration - for example, you might want to run the build on every branch or whenever someone creates a PR. You might have more than one workflow as well. In some projects, you might even want to build on different OS (e.g. Windows, Mac) and software versions (e.g. NPM, Android project simultaneously).</p> <p>GitHub Actions can support complex use cases, pretty powerful. Just try to play around and configure that.</p> </div> <h3>Bonus: Two extra handy configurations</h3> <p>Thanks for reading so far ! Here are the two handy configurations for you.</p> <h4>Skipping the build</h4> <p>Other popular deployment tools (e.g. Travis or Circle CI) support <code>[skip-ci]</code>. We can type <code>[skip-ci]</code> in our commit message or PR title to stop the build intentionally.</p> <p>This doesn't come with GitHub Actions by default, but we can write an extra line to support that easily.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># main.yml</span><br><span class="token punctuation">...</span><br><br><span class="token key atrule">jobs</span><span class="token punctuation">:</span><br> <span class="token key atrule">build</span><span class="token punctuation">:</span><br> <span class="token key atrule">if</span><span class="token punctuation">:</span> <span class="token string">"!contains(github.event.head_commit.message, '[skip-ci]')"</span> <span class="token comment"># add this line</span><br> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest<br><span class="token punctuation">...</span></code></pre> <h4>Cache your dependencies</h4> <p><code>npm install</code> takes time. The more dependencies we have, the slower our build is. It's good to cache them for faster subsequent builds. Let's add in one more action for that.</p> <p>GitHub supports more complex cache settings too, refer to its <a href="https://github.com/actions/cache">documentation</a> (with instant copy-pastable example 😍).</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># main.yml</span><br><span class="token comment"># place after the "Checkout branch" step</span><br><br><span class="token punctuation">...</span><br><br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Retrieve npm cache (if any)<br> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/cache@v1<br> <span class="token key atrule">with</span><span class="token punctuation">:</span><br> <span class="token key atrule">path</span><span class="token punctuation">:</span> ~/.npm<br> <span class="token key atrule">key</span><span class="token punctuation">:</span> npm<span class="token punctuation">-</span>packages<br><br><span class="token punctuation">...</span></code></pre> <p>.</p> <p>Combine all the steps above. Here is our full <code>main.yml</code> file!</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># main.yml</span><br><br><span class="token key atrule">name</span><span class="token punctuation">:</span> CI<br><br><span class="token key atrule">on</span><span class="token punctuation">:</span><br> <span class="token key atrule">push</span><span class="token punctuation">:</span><br> <span class="token key atrule">branches</span><span class="token punctuation">:</span><br> <span class="token punctuation">-</span> master<br> <span class="token key atrule">schedule</span><span class="token punctuation">:</span><br> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> <span class="token string">'0 13 1/1 * *'</span><br><br><span class="token key atrule">jobs</span><span class="token punctuation">:</span><br> <span class="token key atrule">build</span><span class="token punctuation">:</span><br> <span class="token key atrule">if</span><span class="token punctuation">:</span> <span class="token string">"!contains(github.event.head_commit.message, '[skip-ci]')"</span><br> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest<br><br> <span class="token key atrule">steps</span><span class="token punctuation">:</span><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Checkout branch<br> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v1<br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Retrieve npm cache (if any)<br> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/cache@v1<br> <span class="token key atrule">with</span><span class="token punctuation">:</span><br> <span class="token key atrule">path</span><span class="token punctuation">:</span> ~/.npm<br> <span class="token key atrule">key</span><span class="token punctuation">:</span> npm<span class="token punctuation">-</span>packages<br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Use Node.js<br> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>node@v1<br> <span class="token key atrule">with</span><span class="token punctuation">:</span><br> <span class="token key atrule">node-version</span><span class="token punctuation">:</span> <span class="token number">12.16</span><br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Install dependencies<br> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm install<br><br> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Build &amp; deploy<br> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm run deploy<br> <span class="token key atrule">env</span><span class="token punctuation">:</span><br> <span class="token key atrule">CI</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br> <span class="token key atrule">FIREBASE_TOKEN</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.FIREBASE_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span><br></code></pre> <h2>Alrighty, what's next?</h2> <p>Yay! We have set up hosting and automate the deployment successfully. 🎉 That's how my site <a href="https://jec.fish/">jec.fish</a> was set up as well. I have been using GitHub Actions and Firebase for several projects, so far so good.</p> <div class="notes"> <p>Alternative:</p> <p>If you are looking for alternatives for hosting and automation, try <a href="https://www.netlify.com/">Netlify</a>! They offer free quota too. The developer experience in Netlify is better than both Firebase Hosting and Github Actions, IMHO, especially for people without prior experience in hosting and deployment. Much easier to get started. I like it.</p> <p>However, Netlify has a less generous free quota 😆 (check out their <a href="https://www.netlify.com/pricing/">pricing plan</a>), but it is enough for personal projects. I am not sure if it can support complex deployment scenarios like GitHub Actions can.</p> </div> <p>.</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1261173954661638144"> twitter.com/jecfish/status/1261173954661638144 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Customizing File Structure, URLs and Browsersync</title> <link href="https://jec.fish/blog/customizing-file-structure-urls-browsersync"/> <updated>2020-05-21T00:00:00-00:00</updated> <id>https://jec.fish/blog/customizing-file-structure-urls-browsersync</id> <summary>How to use various 11ty data features to achieve the desired file structure and URLs</summary> <category term="blog"/> <content type="html"><p>Find out how I customize Eleventy to achieve my opinionated requirements about the structure of source files, output files and URLs.</p> <style> /* markdown table generator: https://www.tablesgenerator.com/markdown_tables */ /* css gist: https://gist.github.com/tuzz/3331384 */ table { padding: 0; margin-bottom: 20px; border-collapse: collapse; } table tr { margin: 0; padding: 0; } table tr th { font-weight: bold; text-align: left; margin: 0; padding: 6px 14px; border-top: 2px solid #D8DEE9; } table tr td { text-align: left; margin: 0; padding: 6px 14px; border-top: 2px solid #D8DEE9; border-bottom: 2px solid #D8DEE9; } .dark-mode table tr th, .dark-mode table tr td { border-color: #4C566A; } </style> <p>Eleventy offers zero configuration (file structure and URLs) out-of-the-box. It adopts the concept of <a href="https://www.11ty.dev/docs/permalinks/">Cool URIs don't change</a> - the generated URLs will follow its filename by default, with no file extensions. For example, if you have an <code>about-me.html</code> source file, the URL for it would be <code>/about-me/</code>.</p> <p>This is great but not exactly what I want because:</p> <ul> <li>I have a slightly different folder setup.</li> <li>Some of my files follow specific naming conventions.</li> <li>That means URL might be different from its source file name.</li> <li>I don't like how the output files are structured by default.</li> </ul> <div class="notes"> <p>Feel free to dive into the source code <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> straight away. This is the 3rd post of the series. Here is the previous post - <a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a>.</p> </div> <h2>Organizing source files</h2> <p>I organized all source files under subfolders. General pages like <a href="https://jec.fish/licenses">/licenses</a>, <a href="https://jec.fish/blog">/blog</a> listing, <a href="https://jec.fish/404">/404</a>, even the <a href="https://jec.fish/">home page</a> are placed under a folder named <code>root</code>.</p> <p>For content pages like <a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">blog posts</a> and <a href="https://jec.fish/deck/coding-the-powerful-language">presentation decks</a>, they will be organized:</p> <ul> <li>under the folder of its content category</li> <li>each content source file name will be prefixed with ISO created date (YYYY-MM-DD), for source file sorting purposes.</li> </ul> <p>For instance, the &quot;<a href="https://jec.fish/deck/web-performance-optimization">Web Performance Optimization</a>&quot; presentation is placed under the <code>deck</code> folder and the file name would be &quot;2019-08-31-web-performance-optimization.md&quot;.</p> <p>Below is an overview of how source files are structured:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># Source file structure</span><br><br>src<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> index.njk<br> <span class="token punctuation">-</span> licenses.njk<br> <span class="token punctuation">-</span> blog.njk<br> <span class="token punctuation">-</span> blog<br> <span class="token punctuation">-</span> 2020<span class="token punctuation">-</span>05<span class="token punctuation">-</span>19<span class="token punctuation">-</span>post<span class="token punctuation">-</span>one.md</code></pre> <p>In future, I might add in a new content category named <code>read</code>. I will then add a <code>read.njk</code> in the <code>root</code> folder and create a <code>read</code> folder for all my reading reviews.</p> <p>* If you are wondering about the file extensions here, I use markdown <code>.md</code> file for content writing and Nunjucks <code>.njk</code> file for general pages that need more html manipulation.</p> <h2>How does 11ty process our source files?</h2> <p>Eleventy will process our source files (both <code>.njk</code> and <code>.md</code>) into <code>.html</code> pages by default. Base on the above file structure, this would be the output:-</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># Output file structure by default</span><br><br>dist<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> index.html<br> <span class="token punctuation">-</span> licenses<br> <span class="token punctuation">-</span> index.html<br> <span class="token punctuation">-</span> blog<br> <span class="token punctuation">-</span> index.html<br> <span class="token punctuation">-</span> blog<br> <span class="token punctuation">-</span> 2020<span class="token punctuation">-</span>05<span class="token punctuation">-</span>19<span class="token punctuation">-</span>post<span class="token punctuation">-</span>one<br> <span class="token punctuation">-</span> index.html</code></pre> <p>Each source file will output 2 items:</p> <ul> <li>a folder with the same name as its source file</li> <li>an <code>index.html</code> file in that folder</li> </ul> <p>.</p> <p>Based on the above output files, you can probably guess the website URLs! Here is an overview of the URLs:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># Website URLs by default</span><br><br><span class="token punctuation">-</span> /root/<br><span class="token punctuation">-</span> /root/licenses/<br><span class="token punctuation">-</span> /root/blog/<br><span class="token punctuation">-</span> /blog/2020<span class="token punctuation">-</span>05<span class="token punctuation">-</span>19<span class="token punctuation">-</span>post<span class="token punctuation">-</span>one/</code></pre> <p>Erm... that doesn't look like what I want.</p> <h2>What is my expected output?</h2> <p>There is nothing wrong with the default output, 11ty processes our files naively based on our input structure by default. However, that is not what I want.</p> <p>Here are my requirements:</p> <ul> <li>Each source file should output just <strong>ONE HTML</strong> file, no folder.</li> <li><strong>NO DATE</strong> information in the URLs.</li> <li>URLs should be <strong>CLEAN</strong>. (Explain in a bit)</li> </ul> <p>Here is the output I'd like:-</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># My expected output</span><br><br>dist<br> <span class="token punctuation">-</span> index.html<br> <span class="token punctuation">-</span> licenses.html<br> <span class="token punctuation">-</span> blog.html<br> <span class="token punctuation">-</span> deck.html<br> <span class="token punctuation">-</span> blog<br> <span class="token punctuation">-</span> post<span class="token punctuation">-</span>one.html <span class="token comment"># no date</span></code></pre> <p>What do I mean by <strong>&quot;URLs should be clean&quot;</strong>? I prefer URLs with no trailing slash and no file extension. For example:-</p> <ul> <li>😍 /about-me</li> <li>🙁 /about-me/</li> <li>☹️ /about-me.html</li> </ul> <p>I prefer the first URL format. However, with the above output files, when the user browses to <code>licenses</code> page, for example, she will land on <a href="https://jec.fish/blog/jec.fish/licenses.html">/licenses.html</a>, oops! 😥</p> <p>No worryies though, we've got that covered in the <a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">previous post</a>. We have updated the server settings (Firebase <code>cleanURLs</code>) to eliminate file extensions when users browse to our page, so <code>/licenses</code> it is. No file extension, no trailing slash. 😍</p> <p>So what we need to do now is play with 11ty's configurations to generate the above output.</p> <div class="notes"> <p><strong>TLDR: Does website URLs matter? Trailing slash, file extension or date.</strong></p> <p>No, it doesn't impact the searchability (aka SEO) of your site, and users might not even care about or notice that.</p> <p>But! <em>It matters to me</em>. This is my site, the trailing slash and extension hurt my eyes. 😆 I don't want date information in URLs because it is not meaningful (I might update the content from time to time).</p> <p>Coincidentally, my colleague - Jake did a <a href="https://twitter.com/jaffathecake/status/1261252780796383233?s=20">Twitter poll on the same topic</a> few days ago. 😆 <a href="https://twitter.com/mathias/status/1261392833992429570">Mathias replied</a> on it as well. My preference (and the majority) is same as Mathias: prefer <strong>links without trailing slash</strong>.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/customizing-file-structure-urls-browsersync-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/customizing-file-structure-urls-browsersync-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/customizing-file-structure-urls-browsersync-01.jpg"> <img src="https://jec.fish/assets/img/blog/customizing-file-structure-urls-browsersync-01.jpg" alt="Jake and Mathias tweet about web urls, and my replies to it."> </picture> </div><figcaption>Jake and Mathias tweet about web urls, and my replies to it.</figcaption></figure> </div> <h2>What are the solutions then?</h2> <p>Now that we understand the requirements, let's solve that! After reading the documentation multiple times and performing various testing. I found a few ways to achieve the desired result. Let us go through it one by one, from basic to pro. 😎</p> <h3>First pass: Adding permalink in each file</h3> <p>It is quite easy to change the output filename. We just need to update the <code>permalink</code> on the file's Front Matter Data</p> <p>Let's look at our <code>licenses.njk</code> file as an example.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- root/licenses.njk --></span><br><br>---<br>title: Licenses<br>permalink: licenses.html<br>---<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> Hello licenses page.<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br><br></code></pre> <p>The <code>---</code> top section is something we called <strong>Front Matter Data</strong> (briefly mentioned in <a href="https://jec.fish/blog/building-my-static-site-with-11ty">1st post</a>). 11ty wil preprocess the Front Matter Data before the template.</p> <p>There are some built-in Front Matter Data that we can use. <a href="https://www.11ty.dev/docs/permalinks/">Permalink</a> is one of them, and it is a special one (further explained later) because <code>permalink</code> field gives us a way to change the output file format and location. In our case, <code>src/root/licenses.njk</code> will now output as <code>dist/licenses.html</code>, without <code>root</code> in it and no extra folder!</p> <p>More information on Front Matter Data is available in the <a href="https://www.11ty.dev/docs/data-frontmatter/">documentation</a>.</p> <h3>Second pass: Use 11ty supplied data in permalink</h3> <p>Editing permalink in file manually is prone to typing errors. We can improve our manual typing slightly by utilizing some of the <strong>Eleventy Supplied Data</strong>. Here is <a href="https://www.11ty.dev/docs/data-eleventy-supplied/">the list of supplied data</a> that we can use.</p> <p>We will be using the <a href="https://www.11ty.dev/docs/data-eleventy-supplied/#filepathstem">page.filePathStem</a> supplied data. It provides us the full filename - without the date prefix and extension.</p> <p>Here are the <code>filePathStem</code> value for each source files:</p> <table> <thead> <tr> <th><code>source file</code></th> <th><code>filePathStem</code></th> </tr> </thead> <tbody> <tr> <td><code>root/index.njk</code></td> <td><code>/root/index</code></td> </tr> <tr> <td><code>root/licenses.njk</code></td> <td><code>/root/licenses</code></td> </tr> <tr> <td><code>root/blog.njk</code></td> <td><code>/root/blog</code></td> </tr> <tr> <td><code>blog/2020-05-19-post-one.md</code></td> <td><code>/blog/post-one</code></td> </tr> </tbody> </table> <p>Look at the blog post, the date information is gone. One more thing we need to solve though. For the general files, we want the output to be without <code>/root</code>. We achieve that by using the Nunjucks built-in <a href="https://mozilla.github.io/nunjucks/templating.html#replace">replace</a> filter.</p> <p>Let's update our <code>permalink</code> to use the <code>page.filePathStem</code>:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- root/licenses.njk --></span><br><br>---<br>permalink: "{{ page.filePathStem | replace('/root/', '/') }}.html"<br>---<br><br>...<br></code></pre> <p>There you go. Remember I mentioned earlier that <code>permalink</code> is a special one? It is because it allows us to write code. The logic will be interpolated in later stages.</p> <h3>Third pass: Setting permalink per directory</h3> <p>Well, both the first and second pass require us to update each file manually, so it is time-wasting! Is there a better way? Let's go up one level, where we can set the <code>permalink</code> data per directory by creating a <a href="https://www.11ty.dev/docs/data-template-dir/">Directory Data File</a> per folder.</p> <p>Here is the new file structure.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># File structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> root.11tydata.js <span class="token comment"># add this</span><br> <span class="token punctuation">-</span> index.njk<br> <span class="token punctuation">-</span> licenses.njk<br> <span class="token punctuation">-</span> blog.njk<br> <span class="token punctuation">-</span> blog<br> <span class="token punctuation">-</span> blog.11tydata.js <span class="token comment"># add this</span><br> <span class="token punctuation">-</span> 2020<span class="token punctuation">-</span>05<span class="token punctuation">-</span>19<span class="token punctuation">-</span>post<span class="token punctuation">-</span>one.njk </code></pre> <p>Please note that the Directory Data file name:</p> <ul> <li>Must be the same as the directory name</li> <li>Must ends with <code>.11tydata.js</code> (you configure it)</li> </ul> <p>Okay, let's update both of our Directory Data files.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// root/root.11tydata.js</span><br><span class="token comment">// blog/blog.11tydata.js</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> permalink<span class="token operator">:</span> <span class="token string">"{{page.filePathStem | replace('/root/', '/')}}.html"</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <p>It's okay for us to use the same code. In fact, you can shorten the <code>permalink</code> in <code>blog.11ty.data.js</code> to just <code>{{page.filePathStem}}.html</code> if you want to, but I prefer to use the same logic.</p> <p>With this setting, each file under the same directory will get the <code>permalink</code> value injected automatically. We don't need to update every file manually.</p> <p>In case you have a file in the directory that needs a special <code>permalink</code>, you can still override it using the <code>Front Matter Data</code> in each file. For example, let's say we add a RSS file in the <code>root</code> folder <code>atom.njk</code>. We want the output to be <code>index.xml</code> - no <code>/root</code> in name and the file extension should be <code>xml</code>.</p> <p>Here is how we can do it. 👇🏼 The priority in the file <code>Front Matter Data</code> is higher than the <code>Directiry Template Data</code> and hence value get overridden.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// root/atom.njk</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> permalink<span class="token operator">:</span> index<span class="token punctuation">.</span>xml<br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <h3>Final pass: Do it once, setting permalink globally</h3> <p>Still, I wasn't happy with the per-directory approach, as you probably do too. I want to find a way to do it just once and forget about it.</p> <p>After reading the documentation - <a href="https://www.11ty.dev/docs/data-computed/#real-world-example">real world example</a> and <a href="https://www.11ty.dev/docs/data-template-dir/">data precedence</a> a few times, and through various testing, I found a way to do it.</p> <p>11ty accepts a global data folder <code>_data</code> (customizable). We can place our global data or function here (will talk more about this in the coming post).</p> <p>There is one special file we can add into this folder - the <code>Computed Data File</code>. Name it as <code>eleventyComputed.js</code> (must be this name). Let's create the folder and file.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># file structure</span><br><span class="token comment"># add _data folder and `eleventyComputed.js`</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _data<br> <span class="token punctuation">-</span> eleventyComputed.js<br><span class="token punctuation">...</span></code></pre> <p>Here is the code for our <code>eleventyComputed.js</code> file.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// _data/eleventyComputed.js</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> permalink<span class="token operator">:</span><br> <span class="token string">'{% set p = page.filePathStem | replace("/root/", "/") %}'</span> <span class="token operator">+</span><br> <span class="token string">'{{ permalink or (p + ".html)" }}'</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <div class="notes"> <p>Notes for Nunjucks newbies:</p> <ul> <li>Nunjucks uses <code>{% %}</code> as the template syntax.</li> <li>Nunjucks uses <code>{{ }}</code> for interpolation.</li> <li>The built-in <a href="https://mozilla.github.io/nunjucks/templating.html#set">set</a> tag is used to create/modify a variable.</li> </ul> </div> <p>The code is slightly different from our <code>Data Directory Data</code>, we add an <code>or</code> statement here:-</p> <ul> <li>if <code>permalink</code> already exists, we will not override the value</li> <li>if <code>permalink</code> is empty, we will populate that with our value</li> </ul> <p>Basically, the <code>permalink</code> value in the global <code>_data/eleventyComputed.js</code> directory has the highest priority of all. It will override the value in all <code>Data Directory Data</code> and <code>Front Matter Data</code> files. Refer to the <a href="https://www.11ty.dev/docs/data-computed/#advanced-details">advanced details</a> section in the documentation.</p> <p>We don't want the <code>permalink</code> to be overridden if we have set it somewhere else for a special case (e.g. the RSS file). Therefore, we add an <code>or</code> statement to check that.</p> <p>Voila! No more per file nor per directory settings. Just one global computed data to rule it all. 😃</p> <h2>One more thing... configure our localhost</h2> <p>If you run <code>npm start</code> to serve the project now, it will show a &quot;page not found&quot; error when you browse to <a href="localhost:8080/licenses">/licenses</a> page. However, it works when you browse to <a href="localhost:8080/licenses.html">/licenses.html</a>.</p> <p>What happened is that we configured our production server (Firebase Hosting) to handle the <code>cleanURLs</code> (eliminate <code>.html</code>), but we have not configured our local server yet.</p> <p>Eleventy uses Browsersync under the hood to serve our local files. The good news is we can customize that too! Let's configure that in our <code>.eleventy.js</code> configuration file.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// .eleventy.js</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br> <span class="token comment">// Browsersync config</span><br> eleventyConfig<span class="token punctuation">.</span><span class="token function">setBrowserSyncConfig</span><span class="token punctuation">(</span><br> <span class="token comment">// will add our configuration code here</span><br><br> <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token operator">...</span><br><span class="token punctuation">}</span></code></pre> <p>The configuration code is slightly lengthy, so I placed that in a separate file <a href="https://github.com/jecfish/jec-11ty-starter/blob/master/configs/browsersync.config.js">configs/browsersync.config.js</a>. (I might consider creating an npm package in future, maybe!)</p> <pre class="language-js"><code class="language-js"><span class="token comment">// configs/browsersync.config.js</span><br><br><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'url'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">output</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><br> server<span class="token operator">:</span> <span class="token punctuation">{</span><br> baseDir<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>output<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br> middleware<span class="token operator">:</span> <span class="token punctuation">[</span><br> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res<span class="token punctuation">,</span> next</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">let</span> file <span class="token operator">=</span> url<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br> file <span class="token operator">=</span> file<span class="token punctuation">.</span>pathname<span class="token punctuation">;</span><br> file <span class="token operator">=</span> file<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex">/\/+$/</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// remove trailing hash</span><br> file <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>output<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>file<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.html</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>file<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> content <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>file<span class="token punctuation">)</span><span class="token punctuation">;</span><br> res<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span><br> res<span class="token punctuation">.</span><span class="token function">writeHead</span><span class="token punctuation">(</span><span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> <span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">]</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> callbacks<span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token function-variable function">ready</span><span class="token operator">:</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> bs</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> bs<span class="token punctuation">.</span><span class="token function">addMiddleware</span><span class="token punctuation">(</span><span class="token string">'*'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> content <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>output<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/404.html</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> res<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span><br> res<span class="token punctuation">.</span><span class="token function">writeHead</span><span class="token punctuation">(</span><span class="token number">404</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></code></pre> <p>No worries, it's okay to skip reading these code, just use it! 😉 Next, update our <code>.eleventy.js</code> file to use that.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// .eleventy.js</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br> <span class="token comment">// Browsersync config</span><br> eleventyConfig<span class="token punctuation">.</span><span class="token function">setBrowserSyncConfig</span><span class="token punctuation">(</span><br> <span class="token comment">// add this line - dist is our output directory</span><br> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./configs/browsersync.config'</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token string">'dist'</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token operator">...</span><br><span class="token punctuation">}</span></code></pre> <h2>Alrighty, what's next?</h2> <p>Yay! We have set up our file structure, cleaned the URLs and configure Browsersync to support that. 🎉 We have learned about <code>Front Matter Data</code>, <code>Data File Directory</code> <code>Computed Data</code>, the global folder and Browsersync too.</p> <p>That's how my site <a href="https://jec.fish/">jec.fish</a> was set up as well.</p> <div class="notes"> <p><strong>TLDR; Does these effort worth it?</strong></p> <p>Actually... <strong>you might not need this. Seriously</strong>. Just go with the default settings and you will be happy, hah!</p> <p>I am opionated as I have mentioned. I am surprised that I can go so far and even write a whole blog post about this, basically just to:-</p> <ul> <li>make my output file structure look good</li> <li>make the URLs look good</li> </ul> <p>(No offense all, good in my definition! 😂)</p> <p>Nevertheless, I enjoy the process of exploration and testing the flexibility of Eleventy. Turns out, there is a lot of customization you can do with it.</p> </div> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1263751963562237952"> twitter.com/jecfish/status/1263751963562237952 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Automating Image Optimization Workflow</title> <link href="https://jec.fish/blog/automating-image-optimization-workflow"/> <updated>2020-05-23T00:00:00-00:00</updated> <id>https://jec.fish/blog/automating-image-optimization-workflow</id> <summary>Automate image resize, conversion and compression in bulk.</summary> <category term="blog"/> <content type="html"><p>Image resize, compression and format conversion are tedious tasks. However, it's inevitable. Having optimized images is a crucial part for web performance.</p> <p>It's our responsibility to <em>make our websites load fast</em>. I will share how to automate these tasks in our project with Imagemin, Jimp, Husky and Gulp.</p> <p>This is the 4th post of the series - <a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Website with Eleventy</a>. However, you can jump straight into this article without prior reading as this image optimization workflow setup could be applied to any projects.</p> <div class="notes"> <p>Notes:</p> <p>You may jump straight to the code, <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> on GitHub, but this post contains some useful tips on image optimization and testing, so you might not want to miss!</p> </div> <h2>What are performant images?</h2> <p>In short, performant images are:</p> <ol> <li>Images with the appropriate <strong>format</strong>. (e.g. favor <code>webp</code> than <code>jpg</code> or <code>png</code>)</li> <li>Images with appropriate <strong>compression</strong>. (e.g. drop the image quality to 80-85%)</li> <li>Images with appropriate <strong>display size</strong>. (e.g. serve smaller images for mobile, and bigger one for desktop)</li> <li>Loading lazily. (e.g. only load when user scrolls to it)</li> </ol> <h2>A brief walkthrough on the images structure</h2> <p>Most original images in <a href="https://jec.fish/">jec.fish</a> have similar traits:</p> <ul> <li>Images are in various formats. JPG mostly, but also PNG, SVG and GIF.</li> <li><strong>1200 x 675 px</strong> - Used as post cover in collection listing (e.g. <a href="https://jec.fish/blog">/blog</a>. <a href="https://jec.fish/deck">/deck</a>) and meta tag for social media.</li> <li><strong>1024 x any height px</strong> - Used in content (e.g. a <a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">blog post</a>, a <a href="https://jec.fish/deck/web-performance-optimization">presentation deck</a>)</li> </ul> <p>Here is an overview of how images are organized in the project:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># image folder</span><br><br><span class="token punctuation">-</span> assets<br> <span class="token punctuation">-</span> img<br> <span class="token punctuation">-</span> blog<br> <span class="token punctuation">-</span> aaa.jpg<br> <span class="token punctuation">-</span> bbb.png<br> <span class="token punctuation">-</span> ddd.gif<br> <span class="token punctuation">-</span> favicons<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>96.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>128.png<br> <span class="token punctuation">-</span> fff.svg<br> <span class="token punctuation">-</span> ggg<span class="token punctuation">-</span>256.jpg<br> <span class="token punctuation">-</span> hhh.jpg<br></code></pre> <h2>Task 01: Resize the images</h2> <p>Here are the requirements:</p> <ol> <li>Resize the images to <code>500px</code> width.</li> <li>Only resize images with <code>jpg</code> or <code>png</code> format.</li> <li>Output the resized files to <code>assets/img-500</code> folder.</li> <li>Make the code flexible. For example, we might want to generate images with <code>300px</code> in the future.</li> <li>Exclude files with prefix <code>favicon-</code> or suffix <code>-256</code>.</li> </ol> <p>We will be using <a href="https://gulpjs.com/">Gulp</a> as our build tool and the <a href="https://github.com/oliver-moran/jimp">Jimp</a> for our image processing library. Take note that you can use Jimp independently to resize the images, but I find it easier to automate that with Gulp.</p> <p>Let's roll up our sleeves and start by installing the packages!</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># run command</span><br>npm install gulp gulp<span class="token punctuation">-</span>cli jimp through2 <span class="token punctuation">-</span>D</code></pre> <p>.</p> <p>Next, create a Gulp file and start coding. You can give the file any name and place it anywhere in your project. I prefer to organize build tasks in <code>tasks</code> directory. I followed <a href="https://angular.io/guide/styleguide#naming">Angular naming conventions</a> - <code>feature.type.js</code> and <code>dashed-case</code> personally.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># create a gulp file</span><br><span class="token punctuation">-</span> assets<br> <span class="token punctuation">-</span> img<br><span class="token punctuation">-</span> tasks<br> <span class="token punctuation">-</span> transform<span class="token punctuation">-</span>image.gulp.js <span class="token comment"># new file</span></code></pre> <p>.</p> <p>Great, here is the code to resize our images:</p> <pre class="language-js"><code class="language-js"><span class="token comment">// tasks/transform-image.gulp.js</span><br><br><span class="token keyword">const</span> <span class="token punctuation">{</span> src<span class="token punctuation">,</span> dest<span class="token punctuation">,</span> series<span class="token punctuation">,</span> parallel <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> through2 <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'through2'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> Jimp <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'jimp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> <span class="token constant">ASSETS_DIR</span> <span class="token operator">=</span> <span class="token string">'assets'</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> <span class="token constant">EXCLUDE_SRC_GLOB</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">!(favicon*|*-256)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">resize</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">from</span><span class="token punctuation">,</span> to<span class="token punctuation">,</span> width</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> <span class="token constant">SRC</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">../</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">ASSETS_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">from</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/**/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">EXCLUDE_SRC_GLOB</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">*.{jpg,png}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">DEST</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">../</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">ASSETS_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>to<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br> <span class="token keyword">return</span> <span class="token keyword">function</span> <span class="token function">resizeImage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> quality <span class="token operator">=</span> <span class="token number">80</span><span class="token punctuation">;</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token constant">SRC</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><br> through2<span class="token punctuation">.</span><span class="token function">obj</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> _<span class="token punctuation">,</span> cb</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>file<span class="token punctuation">.</span><span class="token function">isBuffer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> img <span class="token operator">=</span> <span class="token keyword">await</span> Jimp<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span>file<span class="token punctuation">.</span>contents<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <br> <span class="token keyword">const</span> smallImg <span class="token operator">=</span> img<br> <span class="token punctuation">.</span><span class="token function">resize</span><span class="token punctuation">(</span>width<span class="token punctuation">,</span> Jimp<span class="token punctuation">.</span><span class="token constant">AUTO</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">quality</span><span class="token punctuation">(</span>quality<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <br> <span class="token keyword">const</span> content <span class="token operator">=</span> <span class="token keyword">await</span> smallImg<br> <span class="token punctuation">.</span><span class="token function">getBufferAsync</span><span class="token punctuation">(</span>Jimp<span class="token punctuation">.</span><span class="token constant">AUTO</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> file<span class="token punctuation">.</span>contents <span class="token operator">=</span> Buffer<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token function">cb</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> file<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">dest</span><span class="token punctuation">(</span><span class="token constant">DEST</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// export the task, pass in parameters</span><br>exports<span class="token punctuation">.</span>default <span class="token operator">=</span> <span class="token function">resize</span><span class="token punctuation">(</span><span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'img-500'</span><span class="token punctuation">,</span> <span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></code></pre> <p>The code looks slightly lengthy. Let's walk through it together:-</p> <ul> <li><code>Gulp</code> provides a <code>src</code> function for us to read and loop through all the files that match the file pattern (glob). We can pass in either a string or array of globs.</li> <li>Take note of the <code>SRC</code> and <code>EXCLUDE_SRC_GLOB</code>. This is the glob pattern to select all the files we want in the <code>img</code> folder but excludes those we don't.</li> <li><code>through2</code> is a wrapper for Nodejs stream. We use it to get our image object streams for processing.</li> <li>Use <code>Jimp</code> to resize and compress the image quality to just <code>80%</code> quality from the original.</li> <li>Use the <code>Gulp</code> built-in <code>dest</code> function to write our files to the output folder.</li> <li>Finally, pass in parameters to our <code>resize</code> function and export it as the default task.</li> </ul> <p>The <code>resize</code> function actually returns the <code>resizeImage</code> function. The benefit of wrapping the code this way is that we can perform multiple <code>resizeImage</code> actions with different parameters. For example, if we need to resize the images to <code>300px</code> as well, we could export this task instead:-</p> <pre class="language-js"><code class="language-js"><span class="token comment">// export the tasks, perform 2 resize action in parallel</span><br><br>exports<span class="token punctuation">.</span>default <span class="token operator">=</span> <span class="token function">parallel</span><span class="token punctuation">(</span><br> <span class="token function">resize</span><span class="token punctuation">(</span><span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'img-500'</span><span class="token punctuation">,</span> <span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br> <span class="token function">resize</span><span class="token punctuation">(</span><span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'img-300'</span><span class="token punctuation">,</span> <span class="token number">300</span><span class="token punctuation">)</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p><code>parallel</code> is the function offered by <code>Gulp</code>. If you want to run the tasks sequentially (one by one) instead, replace <code>parallel</code> with <code>series</code>.</p> <div class="notes"> <p><strong>More about Globs</strong></p> <p>If you want to learn more about file globs, Gulp has a <a href="https://gulpjs.com/docs/en/getting-started/explaining-globs/">basic documentation</a> for that. The docs also provide links to some more advanced globbing syntax. Read the Micromatch documentation too (link provided in the docs), we use that syntax to form our glob above.</p> </div> <h2>How to run the task?</h2> <p>You can run the task by running this command:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># command to run the task</span><br>npx gulp <span class="token punctuation">-</span>f tasks/transform<span class="token punctuation">-</span>image.gulp.js</code></pre> <p>I created a script in <code>package.json</code> file to make my life easier:</p> <pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br><span class="token punctuation">{</span><br> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"transform-image"</span><span class="token operator">:</span> <span class="token string">"npx gulp -f tasks/transform-image.gulp.js"</span><br> <span class="token punctuation">}</span><br> ...<br><span class="token punctuation">}</span></code></pre> <p>I can then run the task by using the command <code>npm run transform-image</code>.</p> <h2>Task 02: Convert the images to webp</h2> <p><code>WebP</code> is a modern image format that is usually 25-35% smaller than comparable JPG and PNG images. It's supported in most browsers, but... not Safari sadly. 😌 However, we serve the <code>WebP</code> images if it is browser-supported, and fall back to <code>JPG</code> if it isn't. (Further explanation later)</p> <p>Here are the requirements for conversion:</p> <ol> <li>Convert the images in both <code>img</code> and <code>img-500</code> folders to <code>webp</code> format.</li> <li>Only convert <code>jpg</code> and <code>png</code> files.</li> <li>Output the converted files to <code>webp</code> and <code>webp-500</code> folders respectively.</li> <li>Make sure the converted files have <code>.webp</code> file extension.</li> <li>Do not hardcode. We might need to convert more images in different folders in the future.</li> <li>Exclude files with prefix <code>favicon-</code> or suffix <code>-256</code>.</li> </ol> <p>We will be using <a href="https://github.com/imagemin/imagemin">imagemin</a> and the <a href="https://github.com/imagemin/imagemin-webp">imagemin-webp</a> plugins. Let's install the packages.</p> <pre class="language-text"><code class="language-text">npm install gulp-imagemin imagemin-webp gulp-rename -D</code></pre> <p>.</p> <p>Next, let's update our <code>transform-image.gulp.js</code> file.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// tasks/transform-image.gulp.js </span><br><br><span class="token keyword">const</span> <span class="token punctuation">{</span> src<span class="token punctuation">,</span> dest<span class="token punctuation">,</span> series<span class="token punctuation">,</span> parallel <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> imageminWebp <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'imagemin-webp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> imagemin <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp-imagemin'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> rename <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp-rename'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> <span class="token constant">ASSETS_DIR</span> <span class="token operator">=</span> <span class="token string">'assets'</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> <span class="token constant">EXCLUDE_SRC_GLOB</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">!(favicon*|*-256|*-512|*-1024)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">convert</span><span class="token punctuation">(</span><span class="token keyword">from</span><span class="token punctuation">,</span> to<span class="token punctuation">,</span> extension <span class="token operator">=</span> <span class="token string">'webp'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> <span class="token constant">SRC</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">../</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">ASSETS_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token keyword">from</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/**/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">EXCLUDE_SRC_GLOB</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">*.{jpg,png}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">DEST</span> <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">../</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">ASSETS_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>to<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br> <span class="token keyword">return</span> <span class="token keyword">function</span> <span class="token function">convertWebp</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token constant">SRC</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">imagemin</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token function">imageminWebp</span><span class="token punctuation">(</span><span class="token punctuation">{</span> quality<span class="token operator">:</span> <span class="token number">80</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><br> <span class="token function">rename</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br> extname<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>extension<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">dest</span><span class="token punctuation">(</span><span class="token constant">DEST</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// export the tasks</span><br>exports<span class="token punctuation">.</span>default <span class="token operator">=</span> <span class="token function">parallel</span><span class="token punctuation">(</span><br> <span class="token function">convert</span><span class="token punctuation">(</span><span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'webp'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br> <span class="token function">convert</span><span class="token punctuation">(</span><span class="token string">'img-500'</span><span class="token punctuation">,</span> <span class="token string">'webp-500'</span><span class="token punctuation">)</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br></code></pre> <p>The code structure is similar to the previous task - read, process and output, but with one extra step at the end: we rename the file with the new extension. (e.g. <code>aaa.jpg</code> will be renamed to <code>aaa.webp</code>).</p> <h2>Can we run both tasks together?</h2> <p>Yes, you can. Update our <code>export</code> task to:</p> <pre class="language-js"><code class="language-js"><span class="token comment">// update the tasks to run all tasks</span><br><br>exports<span class="token punctuation">.</span>default <span class="token operator">=</span> <span class="token function">series</span><span class="token punctuation">(</span><br> <span class="token function">resize</span><span class="token punctuation">(</span><span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'img-500'</span><span class="token punctuation">,</span> <span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br> <span class="token function">parallel</span><span class="token punctuation">(</span><br> <span class="token function">convert</span><span class="token punctuation">(</span><span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'webp'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br> <span class="token function">convert</span><span class="token punctuation">(</span><span class="token string">'img-500'</span><span class="token punctuation">,</span> <span class="token string">'webp-500'</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br></code></pre> <p>The above code means: wait for the image resize task to complete, then start the two image conversion tasks in parallel.</p> <h2>Not good enough...</h2> <p>The above code achieves the purpose. However, there is one drawback. The process will take longer when the number of images grow. It is because the above tasks will always process all the images in the provided folders, <strong>regardless of whether the files are already processed or generated</strong>.</p> <p>Let's make it better! We could use <code>gulp-changed</code> to compare the file's last modified date. If the file already exists in the output folder, we won't process it again.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// tasks/transform-image.gulp.js</span><br><br><span class="token keyword">const</span> changed <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp-changed'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// Modify resizeImage() function</span><br><span class="token operator">...</span><br> <span class="token keyword">return</span> <span class="token keyword">function</span> <span class="token function">resizeImage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> quality <span class="token operator">=</span> <span class="token number">80</span><span class="token punctuation">;</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token constant">SRC</span><span class="token punctuation">)</span><br> <span class="token comment">// add this line</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">changed</span><span class="token punctuation">(</span><span class="token constant">DEST</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><br><span class="token comment">// Modify convertWebp() function</span><br><span class="token operator">...</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token constant">SRC</span><span class="token punctuation">)</span><br> <span class="token comment">// add this line</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">changed</span><span class="token punctuation">(</span><span class="token constant">DEST</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> extension<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">.</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>extension<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><span class="token operator">...</span></code></pre> <p>For the <code>resizeImage</code> function, both input and output filenames are the same, so we just need to call the <code>changed</code> function to compare. However, <code>convertWebp</code> function changes the file extension from JPG to WebP. We need to pass in an additional parameter to the <code>changed</code> function to make it work.</p> <p>Check the <a href="https://github.com/sindresorhus/gulp-changed">gulp-changed</a> documentation if you want to compare the files with other methods.</p> <p>Now, try to run the task again. It should convert just the newly added images. Great! We saved our time and the Earth (less processing power 😆) successfully.</p> <h2>But... I don't want to run the command manually every time</h2> <p>Sure. You can set up a new GitHub Actions to do so (read my 2nd post on <a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a>).</p> <p>However, I prefer to do it locally, because I need to visualize the images during development or writing time (also to save some build minutes on GitHub 😛).</p> <p>One option is to create a <code>prestart</code> NPM script to run the <code>transform-image</code> every time before starting the local server.</p> <pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br><br><span class="token punctuation">{</span><br> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"prestart"</span><span class="token operator">:</span> <span class="token string">"npm run transform-image"</span><br> ...<br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>Another option is to run the tasks every time before we <code>git push</code> or <code>git commit</code> our changes. To do this, you can use the <code>Husky</code> package.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># install Husky</span><br>npm install husky <span class="token punctuation">-</span>D</code></pre> <p>Once installed, add this configuration to your <code>package.json</code>:</p> <pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br><br><span class="token punctuation">{</span><br> <span class="token property">"husky"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"hooks"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"pre-push"</span><span class="token operator">:</span> <span class="token string">"npm run transform-image"</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>Test it! The <code>transform-image</code> task will run every time you push your code.</p> <h2>Serving responsive images in HTML</h2> <p>Instead of using the <code>&lt;img&gt;</code> tag in HTML, we can wrap it with the <code>&lt;picture&gt;</code> element to serve responsive images. For example, with our output files above, our HTML could be:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>picture</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>(max-width: 500px)<span class="token punctuation">"</span></span><br> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/assets/webp-500/hhh.webp<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>image/webp<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>(min-width: 501px)<span class="token punctuation">"</span></span> <br> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/assets/webp/hhh.webp<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>image/webp<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>(max-width: 500px)<span class="token punctuation">"</span></span><br> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/assets/img-500/hhh.jpg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/assets/img/hhh.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>caption<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>picture</span><span class="token punctuation">></span></span></code></pre> <p><a href="https://web.dev/native-lazy-loading/">Native image lazy loading</a> support has landed in <a href="https://caniuse.com/#feat=loading-lazy-attr">majority of the browsers</a>! Not yet in Safari... 😌</p> <p>We can add the <code>loading =&quot;lazy&quot;</code> tag in the <code>img</code> to signal the browser to load the image lazily.</p> <p>Here is the short explanation on what the code above did. 👉🏼</p> <p>If the browser supports WebP, it will serve images:</p> <ul> <li>from the <code>/webp-500</code> folder if the screen size is small. (within <code>500px</code>)</li> <li>from the <code>/webp</code> folder if the screen size is over <code>500px</code>.</li> </ul> <p>If WebP format is not supported, the browser will use the JPG images instead, either from <code>/img-500</code> or <code>/img</code> depending on the screen size.</p> <p>Please note that the above code works in all browsers.</p> <p>There are many ways you can configure the <code>source</code> tag, whether to serve responsive images by media size, device pixel ratio, sizes, etc. Read more in this <a href="https://www.smashingmagazine.com/2014/05/responsive-images-done-right-guide-picture-srcset/">post</a> here by <a href="https://twitter.com/etportis">Eric</a> on Smashing Magazine!</p> <div class="notes"> <p><strong>But... the HTML code is lengthy</strong></p> <p>Indeed, and we don't want to write the same code over and over again. In the coming post, I will share how to create a reusable function to do that, plus anti content jumping when the image is loading. Stay tuned! (hint: by creating <code>shortcode</code> in Eleventy)</p> </div> <h2>Bonus 1/3: What are the alternatives?</h2> <h3>What if I want to transform an image one-off manually?</h3> <p>Sure, use this website <a href="https://squoosh.app/">squoosh.app</a> to resize, compress and convert the image format! Drop in an image, and select the options you need and download the processed image.</p> <p>In fact, I use it quite often myself. Pretty handy.</p> <h3>How to do this on demand (on the fly)?</h3> <p>Yes. You can use <a href="http://thumbor.org/">Thumbor</a> (open source project, host it yourself) to do it. Read the <a href="https://web.dev/use-thumbor/">web.dev</a> article for details.</p> <p>Another one would be <a href="https://cloudinary.com/">Cloudinary</a>, and they offer free quota. It is user friendly and supports image format conversion on the fly!</p> <p>Last one is to roll your own API with the libraries above (<code>imagemin</code> and <code>Jimp</code>)!</p> <h2>Bonus 2/3: How to know which image is serving currently?</h2> <p>When serving responsive images, you might want to test if the right images are served. You can use DevTools to do so (Of course I use Chrome DevTools 😆).</p> <p>Say you want to test on a single image. You can hover to the image element in the DevTools <code>Element</code> panel, and it will show you a pop-up, showing which is the <code>currentSrc</code> of the image.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:40.52734375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/automating-image-optimization-workflow-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/automating-image-optimization-workflow-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/automating-image-optimization-workflow-04.jpg"> <img src="https://jec.fish/assets/img/blog/automating-image-optimization-workflow-04.jpg" alt="View current image src in Element panel"> </picture> </div><figcaption>View current image src in Element panel</figcaption></figure> <p>If you want to examine the images in bulk, open the <code>Network</code> Panel, filter network requests by <code>Img</code> type, check the URLs or further <code>filter</code> by format or filename.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:55.375%"> <img src="https://jec.fish/assets/img/blog/automating-image-optimization-workflow-02.gif" alt="Filtering network request in Network Panel"> </picture> </div><figcaption>Filtering network request in Network Panel</figcaption></figure> <p>How about test serving images in different device pixel ratios (DPR)? Toggle the <code>Device</code> toolbar, and add the DPR selection.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:48.25%"> <img src="https://jec.fish/assets/img/blog/automating-image-optimization-workflow-03.gif" alt="Loading image by DPR"> </picture> </div><figcaption>Loading image by DPR</figcaption></figure> <p>Another easy way is to right click the image, open it in a new tab and check the URL. No DevTools needed! 😆</p> <h2>Bonus 3/3: How's other sites serving their images?</h2> <p>Let's learn from one of the best image websites. Try inspect <a href="https://unsplash.com/">unsplash.com</a> with DevTools. (right click on the photo &gt; select &quot;Inspect&quot;).</p> <p>Guess how many <code>srcset</code> they have for the one image? <strong>20</strong>.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:57.2265625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/automating-image-optimization-workflow-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/automating-image-optimization-workflow-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/automating-image-optimization-workflow-01.jpg"> <img src="https://jec.fish/assets/img/blog/automating-image-optimization-workflow-01.jpg" alt="Unsplash has 20 versions of the same image"> </picture> </div><figcaption>Unsplash has 20 versions of the same image</figcaption></figure> <p>Instagram and Pinterest have lesser <code>srcset</code>. Pick and set the appropriate one for your site. 😃 The best thing about the web is it's open. Inspect and learn!</p> <h2>Alrighty, what's next?</h2> <p>Yay! We have learnt how to optimize images, serve them responsively and test it. 🎉 On behalf of the web residents, thanks for saving our data, hah! 🙇🏻♀️</p> <p>This is how I optimize the images in my site <a href="https://jec.fish/">jec.fish</a> as well. I have a presentation (slides and video) on <a href="https://jec.fish/deck/web-performance-optimization">web optimization - images, fonts and JavaScript</a>, do check it out.</p> <p>.</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1264060210563276800"> twitter.com/jecfish/status/1264060210563276800 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Setting up SEO and Google Analytics</title> <link href="https://jec.fish/blog/setting-up-seo-and-google-analytics"/> <updated>2020-05-24T00:00:00-00:00</updated> <id>https://jec.fish/blog/setting-up-seo-and-google-analytics</id> <summary>How to setup SEO, Google Analytics, structured data and sitemap in Eleventy.</summary> <category term="blog"/> <content type="html"><p>Let's dive into how to make our website discoverable and more attractive when sharing in social media - with a cover image, proper title and short description.</p> <style> /* markdown table generator: https://www.tablesgenerator.com/markdown_tables */ /* css gist: https://gist.github.com/tuzz/3331384 */ table { padding: 0; margin-bottom: 20px; border-collapse: collapse; } table tr { margin: 0; padding: 0; } table tr th { font-weight: bold; text-align: left; margin: 0; padding: 6px 14px; border-top: 2px solid #D8DEE9; } table tr td { text-align: left; margin: 0; padding: 6px 14px; border-top: 2px solid #D8DEE9; border-bottom: 2px solid #D8DEE9; } .dark-mode table tr th, .dark-mode table tr td { border-color: #4C566A; } </style> <p>This is the 5th post of the series - <a href="https://jec.fish/blog/building-my-static-site-with-11ty">building personal static site with 11ty</a>. GitHub Repo is here - <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> if you are a code-first person. 😉</p> <h2>Let's set up some global data before we start</h2> <p>We touched on global data in the <a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">previous post</a>. Global data is data which, once defined, it is accessible in all project template files (e.g. Markdown, Nunjucks).</p> <p>There are some common data we need for all pages, so let's create a new file under the default global data folder <code>_data</code>.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># folder structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _data<br> <span class="token punctuation">-</span> env.js <span class="token comment"># new file</span><br></code></pre> <p>Here is what our <code>env.js</code> file looks like.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_data/env.js</span><br><br><span class="token keyword">const</span> environment <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">ELEVENTY_ENV</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> <span class="token constant">PROD_ENV</span> <span class="token operator">=</span> <span class="token string">'prod'</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> prodUrl <span class="token operator">=</span> <span class="token string">'https://your-production.url'</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> devUrl <span class="token operator">=</span> <span class="token string">'http://localhost:8080'</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> baseUrl <span class="token operator">=</span> environment <span class="token operator">===</span> <span class="token constant">PROD_ENV</span> <span class="token operator">?</span> prodUrl <span class="token operator">:</span> devUrl<span class="token punctuation">;</span><br><span class="token keyword">const</span> isProd <span class="token operator">=</span> environment <span class="token operator">===</span> <span class="token constant">PROD_ENV</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> folder <span class="token operator">=</span> <span class="token punctuation">{</span><br> assets<span class="token operator">:</span> <span class="token string">'assets'</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> dir <span class="token operator">=</span> <span class="token punctuation">{</span><br> img<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>folder<span class="token punctuation">.</span>assets<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/img/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> siteName<span class="token operator">:</span> <span class="token string">'your site name'</span><span class="token punctuation">,</span><br> author<span class="token operator">:</span> <span class="token string">'your name'</span><span class="token punctuation">,</span><br> environment<span class="token punctuation">,</span><br> isProd<span class="token punctuation">,</span><br> folder<span class="token punctuation">,</span><br> base<span class="token operator">:</span> <span class="token punctuation">{</span><br> site<span class="token operator">:</span> baseUrl<span class="token punctuation">,</span><br> img<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dir<span class="token punctuation">.</span>img<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <p>The code is quite straightforward. We set the <code>ELEVENTY_ENV</code> environment variable to <code>dev</code> or <code>prod</code> when we build the project (we set that in NPM scripts previously).</p> <p>Depending on the value, we set other variables accordingly, then export it. These data can be accessed by any template files later. For example, we get the image base URL by calling <code>env.base.img</code>.</p> <h2>Create a base layout</h2> <p>We will be using the <a href="https://www.11ty.dev/docs/layouts/">11ty Layouts</a> feature. A Layout is a reusable piece. As you know, we need to set the SEO (meta tags) and Google Analytics on every page. As a lazy developer, we don't want to copy-paste the same code every time. Layout can help with that.</p> <p><code>_includes</code> is the default layout directory (customizable). Let's create our first layout.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># folder structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _includes<br> <span class="token punctuation">-</span> base.layout.njk <span class="token comment"># create this file</span><br></code></pre> <div class="notes"> <p>File naming:</p> <p>Please note that you can give your layout file any names. I followed <a href="https://angular.io/guide/styleguide#naming">Angular naming conventions</a> personally - naming files with <code>feature.type.js</code> and <code>dashed-case</code>. I find it easier to search through the source files later.</p> </div> <p>Here is the base layout code to start with.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token doctype">&lt;!DOCTYPE html></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <br> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> {{ content | safe }}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br></code></pre> <p>This <code>base.layout.njk</code> would be our skeleton (or you may call it &quot;master layout&quot;) for all other templates. The <code>title</code> data will be provided by the child template later. The <code>content</code> data is the child template’s content. We escape the content by using the built-in <code>safe</code> filter.</p> <p>Let's see how we can use this layout in our template file. the layout. Let's update <code>index.njk</code>:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/index.njk --></span><br>---<br>layout: base.layout.njk<br>title: Home Page<br>---<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>strong</span><span class="token punctuation">></span></span>Hello Eleventy!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">></span></span><br></code></pre> <p>We set the page layout to our newly created <code>base.layout.njk</code>. Open the page in browser now, you should see:</p> <ul> <li>The page title shown as &quot;Home Page&quot;.</li> <li>The page shows <strong>Hello Eleventy!</strong> in bold - instead of literally &quot;&lt;strong&gt;Hello Eleventy!&lt;/strong&gt;&quot; because we use the <code>safe</code> filter to escape the content in the base layout.</li> </ul> <div class="notes"> <p><strong>Protip: Setting layout per directory</strong></p> <p>We can set the <code>layout</code> value in the Directory Data File <code>root.11tydata.js</code>. By doing this, we do not need to set <code>layout</code> in every template. All templates under the <code>root</code> directory will use <code>base.layout.njk</code> by default! (We covered this in the <a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">previous post</a>.</p> </div> <h2>Let's add Google Analytics</h2> <p>Let's start adding Google Analytics. Once you set up an account with <a href="http://analytics.google.com/">Google Analytics</a>, copy the JavaScript code snippet to our base layout.</p> <p>The code should look something like this:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br>...<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token comment">&lt;!-- add these code in the head section --></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">async</span> <br> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>https://www.googletagmanager.com/gtag/js?id=your_tracking_id<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> window<span class="token punctuation">.</span>dataLayer <span class="token operator">=</span> window<span class="token punctuation">.</span>dataLayer <span class="token operator">||</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br> <span class="token keyword">function</span> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> dataLayer<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>arguments<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">'js'</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">'config'</span><span class="token punctuation">,</span> <span class="token string">'your_tracking_id'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br> ...<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br>...</code></pre> <h3>Erm... Not good enough</h3> <p>The above code works, but is not good enough because:</p> <ul> <li>We don't want to track the activities during development. We should enable that for production only.</li> <li>We might have different tracking id for different environments. For example, the beta environment. Hard-coded the tracking id here might not be ideal.</li> </ul> <p>Let's enhance that, update our code above to the following:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br>...<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> {% if env.isProd %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">async</span> <br> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>https://www.googletagmanager.com/gtag/js?id={{ env.tracking.gtag }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> window<span class="token punctuation">.</span>dataLayer <span class="token operator">=</span> window<span class="token punctuation">.</span>dataLayer <span class="token operator">||</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br> <span class="token keyword">function</span> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> dataLayer<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>arguments<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">'js'</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token function">gtag</span><span class="token punctuation">(</span><span class="token string">'config'</span><span class="token punctuation">,</span> <span class="token string">'{{ env.tracking.gtag }}'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br> {% endif %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br>...<br></code></pre> <p>With the above changes, the analytics code will be added to the page only if it's in production (<code>env.isProd</code>). We have also replaced the hardcoded tracking id with a new global data <code>env.tracking.gtag</code>. Add that in your <code>env.js</code>.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_data/env.js</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> <span class="token operator">...</span><br> tracking<span class="token operator">:</span> <span class="token punctuation">{</span><br> gtag<span class="token operator">:</span> <span class="token string">'your_tracking_id'</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <p>.</p> <p>Cool, Google Analytics configuration is done. Try to build your code in dev mode <code>npm run build</code> and production mode <code>npm run build:prod</code> to see the different outputs.</p> <h2>Creating the robots.txt file</h2> <p><a href="https://support.google.com/webmasters/answer/6062608?hl=en">Robots.txt</a> is a text file with instructions for search engine crawlers. Let's create one.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># folder structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span>robots.njk <span class="token comment"># new file </span><br></code></pre> <p>And here is the content of our <code>robots.txt</code>:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># src/root/robots.txt</span><br><br><span class="token punctuation">---</span><br><span class="token key atrule">layout</span><span class="token punctuation">:</span> <span class="token boolean important">false</span><br><span class="token key atrule">permalink</span><span class="token punctuation">:</span> robots.txt<br><span class="token punctuation">---</span><br><span class="token key atrule">User-agent</span><span class="token punctuation">:</span> *</code></pre> <p>This will create the <code>robots.txt</code> file in our output directory.</p> <p>Alternatively, you can name the file as <code>robots.txt</code>, but you might need to add the <code>addPassthroughCopy</code> setting in the <code>.eleventy.js</code> config file (covered in <a href="https://jec.fish/blog/building-my-static-site-with-11ty">first post</a>) because 11ty doesn't process text files by default.</p> <h2>Excluding pages for SEO</h2> <p>Sometimes we want to exclude pages for search index, like the <a href="https://jec.fish/404">/404</a> page. You can do that by adding the page URL in <code>robots.txt</code>.</p> <p>However, there is another way to do it. We can add a meta tag in the base layout.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> {% if ignore %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>robots<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>noindex<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span><br> {% endif %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br>...<br></code></pre> <p>For pages we don't want to index, we can set <code>ignore</code> to <code>true</code> in the page's Front Matter Data. Here is an example:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/404.njk --></span> <br>---<br>layout: base.layout.njk<br>ignore: true<br>---<br><br>Page not found. Go home!<br></code></pre> <h2>What are the tags for SEO?</h2> <p>At the minimum, we should set the <code>title</code> tag and meta tag <code>description</code>.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>description<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ desc or title }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br></code></pre> <p>In case there is no <code>desc</code> provided in the template file, we will use the <code>title</code> value as the description.</p> <p>However, minimum is not good enough for social media sharing.</p> <h3>Social media meta tags</h3> <p>There are many social media meta tags you can set depending on how you want the data to display in the platforms. Read the specific social media documentation for the updated details.</p> <p>I did the basic social media setup here:</p> <ul> <li><strong>Support Twitter and Open Graph tags</strong> - Good enough, these tags work for popular platforms like Twitter, Facebook, Slack, LinkedIn, WhatsApp and more.</li> <li><strong>Only one cover image in JPG format</strong> for all social media with size <code>1200 x 675 px</code> - each social media platform requires a specific image size for optimal display. You may consider generating different cover images for that. I aim for basic configuration. The trick is to always <strong>set the main content at the center-ish location</strong> area of the cover image, so when the image is cropped by social media, main content will still be seen. (Try sharing this post in social media and test if my theory works 😉)</li> </ul> <p>Here are the meta tags I used.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token comment">&lt;!-- Open graph --></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:title<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ title }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:description<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ desc or title }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:type<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>article<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ cover }}<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:image:width<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>1200<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:image:height<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>675<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br> <span class="token comment">&lt;!-- Twitter --></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:title<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ title }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:site<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@yourUsername<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:description<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ desc or title }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ cover }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:creator<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>@yourUsername<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br><br>...</code></pre> <p>For open graph meta tags, we need to set the width and height to make sure the cover image displays correctly every time someone shares the link (see the <a href="https://stackoverflow.com/questions/17226392/share-on-facebook-thumbnail-not-showing-for-the-first-time">issue and explanation here</a>).</p> <p>Check the documentation for <code>og:type</code> and <code>twitter:card</code>, and pick the type that is most suitable for your content. I use Twitter <code>summary_large_image</code> because a big image looks great in a tweet.</p> <p>Child templates will need to provide the <code>cover</code> data - cover image URL.</p> <p>Here is the example of all the Front Matter Data that a child template should have set.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/index.njk --></span><br><span class="token comment">&lt;!-- omit the layout if it's already set in root.11tydata.js --></span><br>---<br>layout: base.layout.njk<br>title: Home Page<br>desc: This is my home page. <br>cover: /assets/img/cover-image.jpg<br>---<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>strong</span><span class="token punctuation">></span></span>Hello Eleventy!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">></span></span><br></code></pre> <h3>Gotcha! No cover image shown in social media</h3> <p>Deploy your project. Try sharing the page URL in social media now.</p> <p>Oops, no cover image shows in the thumbnail.</p> <p>This is because the cover image URL must be absolute (e.g. https://your-site.com/assets/img/cover-image.jpg). You might thought this is an easy fix - by adding the base image url to the <code>cover</code> data like this:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/index.njk --></span><br><span class="token comment">&lt;!-- Nice try, but this is not working! --></span><br><br>---<br>title: Home Page<br>desc: This is my home page. <br>cover: "{{ env.base.img }}cover-image.jpg"<br>---<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>strong</span><span class="token punctuation">></span></span>Hello Eleventy!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">></span></span><br></code></pre> <p>Nice try, but this is not working. 😆 (Said this to myself)</p> <p>Turns out, we cannot have global data access and string interpolation in Front Matter Data (except the special case <code>permalink</code>, covered in <a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">previous post</a>). That means your <code>og:image</code> and <code>twitter:image</code> are showing <strong>{{ env.base.img }}cover-image.jpg</strong> literally in the HTML.</p> <h3>Oh no, how to get around that?</h3> <p>To solve that, you can update the meta tag to include the base image URL.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token comment">&lt;!-- Open graph --></span><br> ...<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.base.img + cover }}<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span><br> <span class="token comment">&lt;!-- Twitter --></span><br> ...<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>twitter:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.base.img + cover }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br><br>...</code></pre> <p>With that, you only need to type the shorter image name in every child template.</p> <h3>Not good enough, for lazy developer</h3> <p>Let's face it, naming is hard. Typing the image URL manually in each template is time-wasting. It would be ideal if the <code>cover</code> data is:</p> <ul> <li>auto populated, in the format of <strong>cleaned-filename.jpg</strong></li> <li>overridable in child template, in case we need to supply a different image</li> </ul> <p>Here are the expected image names:-</p> <table> <thead> <tr> <th>src</th> <th>assets/img</th> </tr> </thead> <tbody> <tr> <td><code>root/index.njk</code></td> <td><code>root/index.jpg</code></td> </tr> <tr> <td><code>root/licenses.njk</code></td> <td><code>root/licenses.jpg</code></td> </tr> <tr> <td><code>blog/2020-05-19-post-one.md</code></td> <td><code>blog/post-one.jpg</code></td> </tr> </tbody> </table> <p>.</p> <p>We can achieve this with the Global Computed Data <code>eleventyComputed.js</code>. In fact, we used that in our <a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">previous post</a>. We can apply similar techniques here.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_data/eleventyComputed.js</span><br><br><span class="token keyword">const</span> env <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./env'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> <span class="token comment">// add this cover data</span><br> <span class="token function-variable function">cover</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">let</span> img <span class="token operator">=</span> data<span class="token punctuation">.</span>cover <span class="token operator">||</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>page<span class="token punctuation">.</span>filePathStem <span class="token operator">+</span> <span class="token string">'.jpg'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> img <span class="token operator">=</span> img<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token operator">?</span> img<span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> img<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">:</span> img<span class="token punctuation">;</span><br> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>img<span class="token punctuation">,</span> env<span class="token punctuation">.</span>base<span class="token punctuation">.</span>img<span class="token punctuation">)</span><span class="token punctuation">.</span>href<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token operator">...</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre> <p>With the above code, we no longer need to enter <code>cover</code> data in every template file. Sweet! 😍</p> <h3>Social media content testing tools</h3> <p>You can test the code once it is deployed and publicly accessible. Various testing tools are offered by each platform - and each has its own fancy name...</p> <ul> <li>Twitter Card Validator - <a href="https://cards-dev.twitter.com/validator">cards-dev.twitter.com/validator</a></li> <li>Facebook Sharing Debugger - <a href="https://developers.facebook.com/tools/debug/">developers.facebook.com/tools/debug</a></li> <li>LinkedIn Post Inspector - <a href="https://www.linkedin.com/post-inspector/">linkedin.com/post-inspector</a></li> </ul> <p>So many different places to test! Yes. Once you set everything up correctly, then no more testing until the next time a social media platform decides to change their image display sizes... 😂</p> <h2>Sitemap</h2> <p>Sitemap is a good thing to have. You might not need one if you have linked your pages within your website. Crawlers are pretty good at discovering content automatically nowadays. Nevertheless, here is the code if you need to create one.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># src/root/sitemap.njk</span><br><br><span class="token punctuation">---</span><br><span class="token key atrule">layout</span><span class="token punctuation">:</span> <span class="token boolean important">false</span><br><span class="token key atrule">permalink</span><span class="token punctuation">:</span> sitemap.txt<br><span class="token punctuation">---</span><br><span class="token punctuation">{</span>%<span class="token punctuation">-</span> for item in collections.all %<span class="token punctuation">}</span><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> item.url <span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span>%<span class="token punctuation">-</span> endfor %<span class="token punctuation">}</span><br></code></pre> <p>Search engines accept sitemap in several formats. I am using a text file here, you may use xml. The <code>collections.all</code> data is an Eleventy Supplied Data. It contains all the pages we created in our project. We loop through each one and write the URL in the sitemap.</p> <div class="notes"> <p><strong>Protip: Logging and debugging</strong></p> <p>How to know what are the properties available in <code>collections.all</code> or <code>item</code>? We can use the filter <code>log</code> to examine the value. Here is how you can use it:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># src/root/sitemap.njk</span><br><br><span class="token punctuation">{</span><span class="token punctuation">{</span> collections.all <span class="token punctuation">|</span> log <span class="token punctuation">}</span><span class="token punctuation">}</span><br></code></pre> <p>Reload the page and check your dev console (where you run <code>npm start</code>). The <code>collections.all</code> data is logged.</p> <p>Pretty handy! Use <code>log</code> for debugging and discovery.</p> </div> <h3>Wait... something is not right</h3> <p>Browse to <code>/sitemap.txt</code>, there are a few things we need to fix:</p> <ul> <li><code>ignore</code> pages should be excluded - We marked <code>404</code> page as <code>ignore: true</code>. We should exclude that from our sitemap.</li> <li>URLs in sitemap should be <strong>absolute URLs</strong> - It's a sitemap requirement. Our URL now has no base URL.</li> <li>Remove <code>.html</code> file extension - The pages will get indexed by Google and show up in the search result. It's okay actually, but I don't like that. 😆 Let's remove that as well.</li> </ul> <p>Here is the fix:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># src/root/sitemap.njk</span><br><br><span class="token punctuation">---</span><br><span class="token key atrule">layout</span><span class="token punctuation">:</span> <span class="token boolean important">false</span><br><span class="token key atrule">permalink</span><span class="token punctuation">:</span> sitemap.txt<br><span class="token punctuation">---</span><br><span class="token punctuation">{</span><span class="token comment"># {{ collections.all | log }} #}</span><br><span class="token punctuation">{</span>%<span class="token punctuation">-</span> for item in collections.all %<span class="token punctuation">}</span><br> <span class="token punctuation">{</span>% if not item.data.ignore %<span class="token punctuation">}</span><br> <span class="token punctuation">{</span><span class="token punctuation">{</span>env.base.site<span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">{</span><span class="token punctuation">{</span> item.url <span class="token punctuation">|</span> replace('.html'<span class="token punctuation">,</span> '') <span class="token punctuation">}</span><span class="token punctuation">}</span><br> <span class="token punctuation">{</span>% endif %<span class="token punctuation">}</span><br><span class="token punctuation">{</span>%<span class="token punctuation">-</span> endfor %<span class="token punctuation">}</span><br></code></pre> <p>Another way to fix this would be creating your own <a href="https://www.11ty.dev/docs/collections/">collection</a> to filter out the unwanted pages, but we won't cover this for now. There is a lot to learn already!</p> <p>Nice. Browse to <code>/sitemap.txt</code> page again, you should see all URLs are absolute and cleaned! Go ahead and submit that to <a href="https://search.google.com/search-console/about">Google Search Console</a> and probably the <a href="https://www.bing.com/webmaster/">Bing Webmaster Tools</a> too (which I did, because why not)!</p> <h2>Bonus: Adding Structured Data</h2> <p>Google has a <a href="https://developers.google.com/search/docs/guides/intro-structured-data">detailed explanation</a> on structured data. Adding structured data helps crawlers to understand your content more, and there would also be a possibility that your page would show up nicer in the search result.</p> <p>We can define structured data with script format <code>JSON-LD</code>. Both Google and Bing support that. We don't need structured data for every single page, only the main content (blog posts, presentation decks) will do, in my opinion (but I am not an SEO expert).</p> <p>Again, we don't want to add the <code>JSON-LD</code> script in every content page manually. One way to solve this is to do something similar to the <code>ignore</code>. We can achieve this by using a new Front Matter Data, for example <code>isSupportStructuredData</code>, to check the boolean value and toggle the script accordingly. However, let's not overuse the Front Matter.</p> <p>Using a new layout would be a better option for our case. Let's create one and I will explain later why it is better.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># folder structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _includes<br> <span class="token punctuation">-</span> writing.layout.njk <span class="token comment"># new file</span></code></pre> <p>Update all our blog posts to use the writing layout. To save time, we can just add that once in the <code>blog.11tydata.js</code> file.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/blog/blog.11tydata.js</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> layout<span class="token operator">:</span> <span class="token string">'writing.layout.njk'</span><span class="token punctuation">,</span><br> <span class="token operator">...</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <p>The good thing about layout is - it is chainable. <code>writing.layout.js</code> extends from the base layout (so we don't need to define those meta tags again 😃).</p> <p>Alright, here is our writing layout with structured data.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/writing.layout.njk --></span><br><br>---<br>layout: base.layout.njk<br>---<br><br><span class="token comment">&lt;!-- the content --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span><span class="token punctuation">></span></span>{{ content | safe }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span><br><span class="token comment">&lt;!-- the structured data --></span><br>{% set absoluteUrl = env.base.site + (page.url | replace('.html', '')) %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>application/ld+json<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> <span class="token punctuation">{</span><br> <span class="token string">"@context"</span><span class="token operator">:</span> <span class="token string">"http://schema.org"</span><span class="token punctuation">,</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"Article"</span><span class="token punctuation">,</span><br> <span class="token string">"@id"</span><span class="token operator">:</span> <span class="token string">"{{ absoluteUrl }}"</span><span class="token punctuation">,</span><br> <span class="token string">"mainEntityOfPage"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"WebPage"</span><span class="token punctuation">,</span><br> <span class="token string">"@id"</span><span class="token operator">:</span> <span class="token string">"{{ absoluteUrl }}"</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token string">"url"</span><span class="token operator">:</span> <span class="token string">"{{ absoluteUrl }}"</span><span class="token punctuation">,</span><br> <span class="token string">"headline"</span><span class="token operator">:</span> <span class="token string">"{{ desc }}"</span><span class="token punctuation">,</span><br> <span class="token string">"description"</span><span class="token operator">:</span> <span class="token string">"{{ title }}"</span><span class="token punctuation">,</span><br> <span class="token string">"audience"</span><span class="token operator">:</span> <span class="token string">"web developers and designers"</span><span class="token punctuation">,</span><br> <span class="token string">"image"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"ImageObject"</span><span class="token punctuation">,</span><br> <span class="token string">"url"</span><span class="token operator">:</span> <span class="token string">"{{ cover }}"</span><span class="token punctuation">,</span><br> <span class="token string">"height"</span><span class="token operator">:</span> <span class="token number">675</span><span class="token punctuation">,</span><br> <span class="token string">"width"</span><span class="token operator">:</span> <span class="token number">1200</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token string">"dateCreated"</span><span class="token operator">:</span> <span class="token string">"{{ date }}"</span><span class="token punctuation">,</span><br> <span class="token string">"datePublished"</span><span class="token operator">:</span> <span class="token string">"{{ date }}"</span><span class="token punctuation">,</span><br> <span class="token string">"dateModified"</span><span class="token operator">:</span> <span class="token string">"{{ date }}"</span><span class="token punctuation">,</span><br> <span class="token string">"articleSection"</span><span class="token operator">:</span> <span class="token string">"Blog"</span><span class="token punctuation">,</span><br> <span class="token string">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"Person"</span><span class="token punctuation">,</span><br> <span class="token string">"name"</span><span class="token operator">:</span> <span class="token string">"{{ env.author }}"</span><span class="token punctuation">,</span><br> <span class="token string">"image"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"ImageObject"</span><span class="token punctuation">,</span><br> <span class="token string">"url"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.img }}your_photo.jpg"</span><span class="token punctuation">,</span><br> <span class="token string">"height"</span><span class="token operator">:</span> <span class="token number">1024</span><span class="token punctuation">,</span><br> <span class="token string">"width"</span><span class="token operator">:</span> <span class="token number">1024</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token string">"url"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.site }}"</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token string">"publisher"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"Organization"</span><span class="token punctuation">,</span><br> <span class="token string">"@id"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.site }}"</span><span class="token punctuation">,</span><br> <span class="token string">"name"</span><span class="token operator">:</span> <span class="token string">"{{ env.siteName }}"</span><span class="token punctuation">,</span><br> <span class="token string">"url"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.site }}"</span><span class="token punctuation">,</span><br> <span class="token string">"logo"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token string">"@type"</span><span class="token operator">:</span> <span class="token string">"ImageObject"</span><span class="token punctuation">,</span><br> <span class="token string">"url"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.img }}your_photo.jpg"</span><span class="token punctuation">,</span><br> <span class="token string">"height"</span><span class="token operator">:</span> <span class="token number">1024</span><span class="token punctuation">,</span><br> <span class="token string">"width"</span><span class="token operator">:</span> <span class="token number">1024</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br></code></pre> <p>Structured data is lengthy. Luckily we just need to write it once in writing layout, not every post.</p> <p>Use the writing layout in our blog post. Here is how our blog post look like:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/blog/2020-05-19-post-one.md --></span><br>---<br>title: A day of my life<br>desc: Story of a relaxing day.<br>date: 2020-05-20<br>---<br><br>I do nothing and sleep all day.<br></code></pre> <p><code>Layout</code> data is inherited from <code>blog/blog.11tydata.js</code> and <code>cover</code> image is defined in global data. We don't need to add those in the templates again.</p> <p>View the script output in DevTools. Check if the data populated correctly.</p> <div class="notes"> <p><strong>Why is layout a better option in this case?</strong></p> <p>The writing layout does not only have structured content data, but it has some specific CSS for styling as well.</p> <p>It is good for us to not mix it with <code>base.layout.njk</code>. Keep the base layout clean.</p> </div> <h3>How to test it?</h3> <p>You can validate the structured data with the Google <a href="https://search.google.com/structured-data/testing-tool">structure data testing tool</a>, even during development. Select the &quot;code snippet&quot; option and paste your <code>JSON-LD</code> script there.</p> <p>There is a handy feature in Chrome DevTools to help you to copy the script easily - in the Elements tab, right click on the script element &gt; select Copy &gt; Copy element.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:50.48828125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-seo-and-google-analytics-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-seo-and-google-analytics-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-seo-and-google-analytics-01.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-seo-and-google-analytics-01.jpg" alt="Copy the element with ChromeDevTools"> </picture> </div><figcaption>Copy the element with ChromeDevTools</figcaption></figure> <p>There you go, the script is in your clipboard!</p> <h3>But.. how to know what structured data to set?</h3> <p>The fact is, I don't know either. I use DevTools to inspect the top article sites like <a href="https://medium.com/">Medium.com</a> and <a href="https://scotch.io/">Scotch.io</a> to learn how they configured the structured data. That's how I came out with the above script. 😂</p> <p>I also referred to the <a href="https://schema.org/">schema.org</a> website to see what are the fields available for each entity.</p> <h2>Bonus: Any other to-dos to enhance SEO?</h2> <p>SEO is a big topic. While I might not be an expert, here is a few things that can impact your site ranking:</p> <p>Low hanging fruits:</p> <ul> <li>Site performance - <a href="https://cognitiveseo.com/blog/22865/page-speed-seo/">Page speed is one of the ranking factor</a>. Make sure your <strong>page loads fast</strong>. Use <a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a> (Chrome DevTools &gt; Audits panel) to detect and fix potential performance issues. Other tools helped as well - <a href="https://developers.google.com/speed/pagespeed/insights">PageSpeed Insights</a> or <a href="https://www.webpagetest.org/">WebPagetest</a>.</li> <li><strong>Responsive layout</strong> - Page should be mobile friendly, and provide positive user experience (UX). Better UX leads to longer &quot;time spent on page&quot;, and thus <a href="https://www.searchenginejournal.com/seo-responsive-web-design-benefits/211264/">raise the page ranking</a>. Test your site with <a href="https://search.google.com/test/mobile-friendly">Mobile-Friendly Test</a>.</li> </ul> <p>Require more effort:</p> <ul> <li>Adding <a href="https://amp.dev/">AMP Page</a> and <a href="https://www.facebook.com/facebookmedia/solutions/instant-articles">Facebook Instant Article</a> might help as well.</li> <li>Backlink - When reputable sites link to your page, your page ranks higher because it represents a &quot;vote of confidence&quot;.</li> </ul> <h1>Bonus: Search your site URLs</h1> <p>You can search for all your site URLs to check if your pages are indexed in search engine.</p> <p>Type <code>site:your_domain_url</code> in Google or Bing. You will see a list of your indexed pages. For example, this is the result when I google <strong><a href="https://www.google.com/search?q=site%3Ajec.fish">site:jec.fish</a></strong>.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.93359375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/setting-up-seo-and-google-analytics-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/setting-up-seo-and-google-analytics-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/setting-up-seo-and-google-analytics-02.jpg"> <img src="https://jec.fish/assets/img/blog/setting-up-seo-and-google-analytics-02.jpg" alt="Google result of my website URLs"> </picture> </div><figcaption>Google result of my website URLs</figcaption></figure> <p>Alternatively, you can view it in <a href="https://search.google.com/search-console/about">Google Search Console</a> and <a href="https://www.bing.com/webmaster/">Bing Webmaster Tools</a>.</p> <h2>Alrighty, what's next?</h2> <p>Yay! We have learnt quite a bit in this post! From setting up Google Analytics, meta tags, and sitemap, to structured data, and various testing tools. Oh, we have also learnt about how to use layouts and global data!</p> <p>All these efforts are mostly one time. Get it right and you won't need to worry about it anymore (until something suddenly breaks, heh 😝).</p> <p>This is how I set up the SEO of my site <a href="https://jec.fish/">jec.fish</a> as well.</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1264474481617035264"> twitter.com/jecfish/status/1264474481617035264 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Minifying HTML, JavaScript, CSS - Automate Inline</title> <link href="https://jec.fish/blog/minifying-html-js-css"/> <updated>2020-05-27T00:00:00-00:00</updated> <id>https://jec.fish/blog/minifying-html-js-css</id> <summary>How to handle resources minification & how to inline them automatically by setting up naming convention in Eleventy.</summary> <category term="blog"/> <content type="html"><p>Let's talk about how to handle HTML, JavaScript &amp; CSS minification &amp; inline them automatically with file naming conventions in Eleventy.</p> <p>This is the 6th post of the series - <a href="https://jec.fish/blog/building-my-static-site-with-11ty">building personal static site with 11ty</a>. Feel free to jump straight to the GitHub Repo <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> if you are a code-first person. 😉</p> <h2>What do we want to achieve?</h2> <p>The main goal is to serve the optimized minified version of HTML - with mostly inline JavaScript and CSS to our users, <strong>but our source files should be organized separately</strong>.</p> <p>As a bonus, I will share the workflow of minifying external CSS and JavaScript files as well.</p> <div class="notes"> <p><strong>What do you mean by inline?</strong><br> Inline means writing CSS and JavaScript in the same HTML file using the <code>&lt;style&gt;</code> and <code>&lt;script&gt;</code> tag. (For CSS, some say that's called <strong>internal CSS</strong>, but I will refer that as inline in this post, for simplicity's purpose)</p> </div> <h3>Could you show me an example?</h3> <p>Sure! Here is the expected file structure. Notice that we have new CSS and JavaScript files created with same name as the template file:</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># file structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _includes<br> <span class="token punctuation">-</span> base.layout.njk<br> <span class="token punctuation">-</span> base.layout.css <span class="token comment"># new file</span><br> <span class="token punctuation">-</span> base.layout.js <span class="token comment"># new file</span><br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> index.njk<br> <span class="token punctuation">-</span> index.css <span class="token comment"># new file</span><br> <span class="token punctuation">-</span> index.js <span class="token comment"># new file</span></code></pre> <p>Here is a sample of the <code>base.layout.*</code> source code:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>{{ title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>{{ content | safe }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br><br></code></pre> <pre class="language-css"><code class="language-css"><span class="token comment">/* src/_includes/base.layout.css */</span><br><br><span class="token selector">h1</span> <span class="token punctuation">{</span><br> <span class="token property">background</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <pre class="language-js"><code class="language-js"><span class="token comment">/* src/_includes/base.layout.js */</span><br><br><span class="token keyword">const</span> h1 <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'h1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>h1<span class="token punctuation">.</span>innerText <span class="token operator">=</span> h1<span class="token punctuation">.</span>innerText <span class="token operator">+</span> <span class="token string">' | Surprise!'</span><span class="token punctuation">;</span><br></code></pre> <p>Here is a sample of the <code>index.*</code> source code:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/index.njk --></span><br>---<br>layout: base.layout.njk<br>title: Home Page <br>---<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Lorem Ipsum blah blah<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br></code></pre> <pre class="language-css"><code class="language-css"><span class="token comment">/* src/root/index.css */</span><br><br><span class="token selector">p</span> <span class="token punctuation">{</span><br> <span class="token property">color</span><span class="token punctuation">:</span> blue<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <pre class="language-js"><code class="language-js"><span class="token comment">/* src/root/index.js */</span><br><br><span class="token keyword">const</span> p <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'p'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>p<span class="token punctuation">.</span>innerText <span class="token operator">=</span> p<span class="token punctuation">.</span>innerText <span class="token operator">+</span> <span class="token string">' yay!'</span><span class="token punctuation">;</span><br></code></pre> <p>.</p> <p>The expected output in the <code>dist</code> folder should be the <strong>minified version</strong> of the <code>index.html</code>, shown below:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- dist/index.html --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"><br> <span class="token comment">/* from base.layout.css */</span><br> <span class="token selector">h1</span> <span class="token punctuation">{</span><br> <span class="token property">background</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><br> <span class="token comment">/* from index.css */</span><br> <span class="token selector">p</span> <span class="token punctuation">{</span><br> <span class="token property">color</span><span class="token punctuation">:</span> blue<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>Home Page | Surprise!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Lorem Ipsum blah blah yay!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> <span class="token comment">// from index.js</span><br> <span class="token keyword">const</span> p <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'p'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> p<span class="token punctuation">.</span>innerText <span class="token operator">=</span> p<span class="token punctuation">.</span>innerText <span class="token operator">+</span> <span class="token string">' yay!'</span><span class="token punctuation">;</span><br><br> <span class="token comment">// from base.layout.js</span><br> <span class="token keyword">const</span> h1 <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'h1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> h1<span class="token punctuation">.</span>innerText <span class="token operator">=</span> h1<span class="token punctuation">.</span>innerText <span class="token operator">+</span> <span class="token string">' | Surprise!'</span><span class="token punctuation">;</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br></code></pre> <h3>Why inline JavaScript and CSS?</h3> <p>When building my personal site, I asked myself:</p> <blockquote> <p><em>Will there be a lot of JavaScript and CSS in each page?</em></p> </blockquote> <p>My answer is <strong>NO</strong>. Unlike enterprise applications, there will be very minimal CSS and JS in this project.</p> <h3>Why do you want to separate the JS and CSS source code?</h3> <p>The syntax highlighting support in individual files is better than inlining them in Nunjucks or Markdown templates.</p> <p>Also, I am obsessed with file structures. 😂 I will split the code into individual files when it goes more than 10 lines usually (or 5 sometimes), but this is purely personal preference.</p> <p>That being said, we can still write CSS in the template file directly if we want.</p> <h2>Setting up the minification workflow</h2> <p>In the previous post, we used <a href="https://gulpjs.com/">Gulp</a> to set up image transformation, so we will stick to that. Let's install some packages to help us with our HTML, JS and CSS minification workflow!</p> <pre class="language-text"><code class="language-text">npm install gulp-htmlmin terser -D</code></pre> <p>Next, we will create a new file under the <code>tasks</code> folder.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># create a new task</span><br><br><span class="token punctuation">-</span> tasks<br> <span class="token punctuation">-</span> minify<span class="token punctuation">-</span>output.gulp.js <span class="token comment"># new file</span></code></pre> <p>Here is the code to handle HTML, CSS and JS minification together:</p> <pre class="language-js"><code class="language-js"><span class="token comment">// tasks/minify-output.gulp.js</span><br><br><span class="token keyword">const</span> <span class="token punctuation">{</span> src<span class="token punctuation">,</span> dest<span class="token punctuation">,</span> series <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> htmlmin <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp-htmlmin'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> jsMinify <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'terser'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>minify<span class="token punctuation">;</span><br><br><span class="token keyword">const</span> <span class="token constant">OUTPUT_DIR</span> <span class="token operator">=</span> <span class="token string">'dist'</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">minifyHtml</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">../</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">OUTPUT_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/**/*.html</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><br> <span class="token function">htmlmin</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br> <span class="token comment">// options offered by the library (lib)</span><br> collapseWhitespace<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br> useShortDoctype<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br> removeComments<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br> <span class="token comment">// lib supports inline CSS minification too</span><br> minifyCSS<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br> <span class="token comment">// lib support inline JS minification as well</span><br> <span class="token comment">// with a catch, so we need to use terser instead</span><br> <span class="token function-variable function">minifyJS</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">text<span class="token punctuation">,</span> _</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> res <span class="token operator">=</span> <span class="token function">jsMinify</span><span class="token punctuation">(</span>text<span class="token punctuation">,</span> <span class="token punctuation">{</span> warnings<span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>res<span class="token punctuation">.</span>warnings<span class="token punctuation">)</span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>res<span class="token punctuation">.</span>warnings<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>res<span class="token punctuation">.</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>text<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">throw</span> res<span class="token punctuation">.</span>error<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token keyword">return</span> res<span class="token punctuation">.</span>code<span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">dest</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">../</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">OUTPUT_DIR</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br>exports<span class="token punctuation">.</span>default <span class="token operator">=</span> <span class="token function">series</span><span class="token punctuation">(</span>minifyHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><br><br></code></pre> <p>The <a href="https://github.com/jonschlinkert/gulp-htmlmin">gulp-htmlmin</a> package uses <a href="https://twitter.com/kangax">@kangax's</a> famous <a href="https://github.com/kangax/html-minifier">html-minifier</a> library under the hood. The library supports:</p> <ul> <li>inline CSS minification (using <a href="https://github.com/jakubpawlowicz/clean-css">clean-css</a>)</li> <li>inline JavaScript minification (using <a href="https://github.com/mishoo/UglifyJS2">UglifyJS</a>)</li> </ul> <p>Take a look at the <a href="https://github.com/kangax/html-minifier">html-minifier documentation</a>, there are a lot of configuration options you can play around with (e.g. <code>collapseWhitespace</code>, <code>minifyCSS</code> are just some of the options we used above).</p> <p>One catch - the default JavaScript minification library <a href="https://github.com/mishoo/UglifyJS2">UglifyJS</a> is not actively maintained anymore and doesn't support the modern JavaScript syntax (ES6+). Therefore, we replace it with the <a href="https://github.com/terser/terser">Terser</a> library.</p> <p>Note that we will run this task after Eleventy processes our files to the output <code>dist</code> folder (unminified HTML files). We will then reprocess the HTML files, and replace the content with the minified one.</p> <p>You may add the NPM scripts below to run the task each time after a production build.</p> <pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br><span class="token punctuation">{</span><br> <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"build:prod"</span><span class="token operator">:</span> <span class="token string">"ELEVENTY_ENV=prod npx eleventy"</span><span class="token punctuation">,</span> <span class="token comment">// existing script</span><br> <span class="token property">"postbuild:prod"</span><span class="token operator">:</span> <span class="token string">"npm run minify-output"</span><span class="token punctuation">,</span><br> <span class="token property">"minify-output"</span><span class="token operator">:</span> <span class="token string">"npx gulp -f tasks/minify-output.gulp.js"</span><br> <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre> <p>.</p> <p>Great, our minification workflow has been set up successfully! Next, we need to work on inlining the JS and CSS files, BEFORE we run the minification task.</p> <h2>Inline with Nunjuck <code>include</code></h2> <p>In the <a href="https://www.11ty.dev/docs/">11ty documentation</a> site, there is a <a href="https://www.11ty.dev/docs/quicktips/">quick tips</a> section on how to handle <a href="https://www.11ty.dev/docs/quicktips/inline-css/">inline CSS</a> and <a href="https://www.11ty.dev/docs/quicktips/inline-js/">inline JS</a>.</p> <p>In short, here is how you can do it:-</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> ...<br> <span class="token comment">&lt;!-- store the css file content as a string variable --></span><br> {% set cssStr %}<br> {% include "index.css" %}<br> {% endset %}<br> <span class="token comment">&lt;!-- interpolate it in style tag --></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> cssStr | safe <span class="token punctuation">}</span><span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> ...<br> <span class="token comment">&lt;!-- store the js file content as a string variable --></span><br> {% set jsStr %}<br> {% include "index.js" %}<br> {% endset %}<br> <span class="token comment">&lt;!-- interpolate it in script tag --></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> jsStr <span class="token operator">|</span> safe <span class="token punctuation">}</span><span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br><br></code></pre> <p>The code above is pretty expressive by itself. We first include the file content and assign them to variables, then interpolate them in their respective tags.</p> <p>Nunjucks' <a href="https://mozilla.github.io/nunjucks/templating.html#include">include</a> tag allow you to pull in other file content in place. It's useful when you need to share smaller chunks of content across several files.</p> <h3>Not good enough... Let's improve it</h3> <p>The code above works. However, you have to copy-paste, edit the above code every time, and each file manually. 😥 The syntax is a bit verbose (syntax noise is real).</p> <p>I enhanced the code above a little by using Nunjucks <a href="https://mozilla.github.io/nunjucks/templating.html#macro">macro</a>.</p> <p>The <code>macro</code> tag allows us to define reusable chunks of content. It is similar to JavaScript module. Once defined, we can import and use them.</p> <h3>1st improvement - write the macros</h3> <p>Let's start by creating a <code>macro</code> file.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># file structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _includes<br> <span class="token punctuation">-</span> base.layout.njk<br> <span class="token punctuation">-</span> src.macro.njk <span class="token comment"># new file</span></code></pre> <p>.</p> <p>We will write two macros in this file. One for CSS and one for JS.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/src.macro.njk --></span><br><br><span class="token comment">&lt;!-- macro for css, accept a filename as input --></span><br>{% macro css(filename) %}<br> {% set cssStr %}<br> {% include filename %}<br> {% endset %}<br><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> cssStr | safe <span class="token punctuation">}</span><span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">></span></span><br>{% endmacro %}<br><br><span class="token comment">&lt;!-- macro for js, accept a filename as input --></span><br>{% macro js(filename) %}<br> {% set jsStr %}<br> {% include filename %}<br> {% endset %}<br><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> jsStr <span class="token operator">|</span> safe <span class="token punctuation">}</span><span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br>{% endmacro %}<br></code></pre> <p>We have two macros functions <code>css</code> and <code>js</code>. Each accepts an input parameter <code>filename</code>. Did you notice that these code are similar to the previous (except the <code>macro</code> part)? You are right. We just moved them in.</p> <p>Now, let's update our base layout to use the <code>macros</code>!</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token comment">&lt;!-- import the macro --></span><br>{% import "src.macro.njk" as src with context %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> ...<br> <span class="token comment">&lt;!-- use the css macro --></span><br> {{ src.css('base.layout.css') }}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> ...<br> <span class="token comment">&lt;!-- use the js macro --></span><br> {{ src.js('base.layout.js') }}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br><br></code></pre> <p>Does the code in our base layout look better now? 😁 We can reuse the <code>macros</code> this way for all <code>layouts</code> (e.g. <code>wiriting.layout.njk</code>) - Import the macro, then use it by passing in the filename.</p> <h3>How about our template files?</h3> <p>For each of our web page templates (e.g. <code>index.njk</code>, <code>2020-05-19-post-one.md</code>), we have an even better way to handle the css and js inline.</p> <p>In my <a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">previous post</a>, we've set up <code>base.layout.njk</code> as the master layout - all of our templates and layouts are inherited from <code>base.layout.njk</code>.</p> <p>We can inline the page specific CSS and JS in <code>base.layout.njk</code>.</p> <p>First, modify our base layout to include two more lines.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br>{% import "src.macro.njk" as src with context %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> ...<br> {{ src.css('base.layout.css') }}<br> <span class="token comment">&lt;!-- include the page template CSS --></span><br> {{ src.css(page.inputPath) }}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br> ...<br> {{ src.js('base.layout.js') }}<br> <span class="token comment">&lt;!-- include the page template JS --></span><br> {{ src.js(page.inputPath) }}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br><br></code></pre> <p>Here, we utilize the <a href="https://www.11ty.dev/docs/data-eleventy-supplied/">Eleventy Supplied Data</a> to get the value of the current <code>page.inputPath</code>.</p> <p>For example, the input path for <code>index.njk</code> would be <code>./src/root/index.njk</code>.</p> <p>The code above doesn't work (just yet) because:-</p> <ul> <li><strong>Individual JS or CSS files are optional</strong> - For example, I might not have <code>licenses.css</code> or <code>licenses.js</code> for the template <code>licenses.njk</code>. The import should be optional, no error should be thrown if no files found.</li> <li><strong>Incorrect file extension</strong> - The <code>page.inputPath</code> return files in <code>.md</code> or <code>.njk</code> extension. We need to replace that with <code>.js</code> or <code>.css</code>.</li> <li><strong>Incorrect file path</strong> - For example, the <code>index.*</code> should be <code>../root/index.*</code> instead of <code>./src/root/index.*</code>. It is because the file path should be relative to the base layout location (which we wrote our <code>import</code> macro statement). Below is our file structure again for your reference.</li> </ul> <pre class="language-yml"><code class="language-yml"><span class="token comment"># our file structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _includes<br> <span class="token punctuation">-</span> base.layout.njk<br> <span class="token punctuation">-</span> base.layout.css<br> <span class="token punctuation">-</span> base.layout.js<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> index.njk<br> <span class="token punctuation">-</span> index.css<br> <span class="token punctuation">-</span> index.js<br></code></pre> <p>.</p> <p>Alright, let's fix all the issues above in our <code>macro</code> file!</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/src.macro.njk --></span><br><br>{% macro css(filename) %}<br> <span class="token comment">&lt;!-- change relative path --></span><br> {% set path = filename | replace('./src/', '../') %}<br> <span class="token comment">&lt;!-- replace file extension --></span><br> {% set path = utils.replaceExtension(path, 'css') %}<br> <br> {% set cssStr %}<br> <span class="token comment">&lt;!-- ignore if missing --></span><br> {% include path ignore missing %}<br> {% endset %}<br><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css"><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> cssStr | safe <span class="token punctuation">}</span><span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>style</span><span class="token punctuation">></span></span><br>{% endmacro %}<br><br><br>{% macro js(filename) %}<br> <span class="token comment">&lt;!-- change relative path --></span><br> {% set path = filename | replace('./src/', '../') %}<br> <span class="token comment">&lt;!-- replace file extension --></span><br> {% set path = utils.replaceExtension(path, 'js') %}<br><br> {% set jsStr %}<br> <span class="token comment">&lt;!-- ignore if missing --></span><br> {% include path ignore missing %}<br> {% endset %}<br><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br> <span class="token punctuation">{</span><span class="token punctuation">{</span> jsStr <span class="token operator">|</span> safe <span class="token punctuation">}</span><span class="token punctuation">}</span><br> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br>{% endmacro %}<br></code></pre> <p>With the above changes, we've fixed all the 3 issues we mentioned above.</p> <p>You might be wondering where did the <code>utils.replaceExtension</code> function come from? It's a new function. Let's create that in our global <code>_data</code> file.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># file structure</span><br><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> _data<br> <span class="token punctuation">-</span> utils.js <span class="token comment"># new file</span></code></pre> <p>Here is the code:</p> <pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br> <span class="token function-variable function">replaceExtension</span><span class="token operator">:</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">file<span class="token punctuation">,</span> extension</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <br> <span class="token keyword">return</span> file<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex">/([^\.]*)$/</span><span class="token punctuation">,</span> extension<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span></code></pre> <p>We use regex to find the file extension and replace it with the expected extension.</p> <p>Viola! Try to run the build or browse to our home page now, you should see the result shown as expected!</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.8359375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/minifying-html-js-css-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/minifying-html-js-css-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/minifying-html-js-css-01.jpg"> <img src="https://jec.fish/assets/img/blog/minifying-html-js-css-01.jpg" alt="Expected home page result when viewing in browser"> </picture> </div><figcaption>Expected home page result when viewing in browser</figcaption></figure> <p>Note that we don't need to add an <code>import</code> statement in any of our page templates (e.g. <code>root/index.njk</code>, <code>blog/2020-05-19-post-one.md</code>) because we have configured that in the master base layout.</p> <p>The page template JS and CSS files will be inlined automatically, as long as the filename matches the template file. 😁</p> <h1>What if I want to inline a shared CSS file?</h1> <p>Let's say you have a css file called <code>table.css</code>. It is a shared CSS, you want to inline that for a few templates only. Here is how you can do it.</p> <p>Place the file under <code>assets/css</code> folder.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># file structure</span><br><br><span class="token punctuation">-</span> assets<br> <span class="token punctuation">-</span> css<br> <span class="token punctuation">-</span> table.css <span class="token comment"># new file</span><br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> index.njk<br> <span class="token punctuation">-</span> index.css<br> <span class="token punctuation">-</span> <span class="token punctuation">...</span></code></pre> <p>Let's say you want to inline that in <code>root/index.njk</code>. There are two ways to do it.</p> <h4>Option one: include in th template CSS file</h4> <p>Update your <code>root/index.css</code> file.</p> <pre class="language-css"><code class="language-css"><span class="token comment">/* src/root/index.css */</span><br><br><span class="token selector">h1</span> <span class="token punctuation">{</span><br> <span class="token property">background</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token punctuation">{</span>% include <span class="token string">"../../assets/css/table.css"</span> %<span class="token punctuation">}</span><br></code></pre> <p>Use the Nunjucks <code>include</code> statement in the CSS file (yes, we can). The CSS will be included accordingly.</p> <h4>Option two: include in the template file</h4> <p>To do this, you need to import the macro file.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/index.njk --></span><br><br>---<br>layout: base.layout.njk<br>title: Home Page <br>---<br><span class="token comment">&lt;!-- import and apply css --></span><br>{% import "src.macro.njk" as src with context %}<br>{% src.css("../../assets/table.css") %}<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Lorem Ipsum blah blah<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br></code></pre> <p>.</p> <p>These two options applied to shared <code>js</code> files too.</p> <h2>Bonus: Minify external CSS and JS files</h2> <p>As promised. Here is the code to minify external CSS and JS files. We will set up two more Gulp tasks to do so!</p> <p>Let's install these packages:</p> <pre class="language-text"><code class="language-text">npm install gulp-clean-css gulp-terser -D</code></pre> <p>Note that we use the same libraries under the hood - <a href="https://github.com/jakubpawlowicz/clean-css">clean-css</a> and <a href="https://github.com/terser/terser">terser</a>.</p> <p>Here are the code:</p> <pre class="language-js"><code class="language-js"><span class="token comment">// tasks/minify-output.gulp.js</span><br><br><span class="token keyword">const</span> <span class="token punctuation">{</span> src<span class="token punctuation">,</span> dest<span class="token punctuation">,</span> parallel <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">required</span><span class="token punctuation">(</span><span class="token string">'gulp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> terser <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp-terser'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> cleanCSS <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp-clean-css'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">minifyCss</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token string">'../assets/css/*.css'</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">cleanCSS</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">dest</span><span class="token punctuation">(</span><span class="token string">'dist/assets/css'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">minifyJs</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> <span class="token function">src</span><span class="token punctuation">(</span><span class="token string">'../assets/js/*.js'</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">terser</span><span class="token punctuation">(</span><span class="token punctuation">{</span> warnings<span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">dest</span><span class="token punctuation">(</span><span class="token string">'dist/assets/js'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">minifyHtml</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span><br><br><span class="token comment">// Run all the tasks in parallel</span><br><span class="token keyword">export</span><span class="token punctuation">.</span>default <span class="token operator">=</span> <span class="token function">parallel</span><span class="token punctuation">(</span>minifyHtml<span class="token punctuation">,</span> minifyCss<span class="token punctuation">,</span> minifyJs<span class="token punctuation">)</span><span class="token punctuation">;</span><br></code></pre> <p>We export the tasks and execute them all in parallel.</p> <h3>Bonus: This is not perfect</h3> <p>As for every solution, inline CSS and JS is not perfect. While the page rendering is faster (eliminates additional round trips to fetch external resources), browsers are <strong>not able to cache any inline CSS and JS</strong>. This is bad if you have a huge chunk of JS and CSS.</p> <p>On the other hand, browsers can cache external files. However, it might take a longer time to download external resources and <strong>page rendering is blocked while waiting the resource to be downloaded</strong>.</p> <p>Another solution (only for CSS), would be <strong>critical CSS</strong>. It combines the best of both world. You can use library like <a href="https://github.com/addyosmani/critical">Critical</a> to:</p> <ul> <li>extract the critical CSS from the external CSS file</li> <li>inline the critical CSS into each html</li> <li>the non-critical CSS remains in external file</li> </ul> <p>However, this does increase your workflow complexity. Read this article to understand more about <a href="https://www.smashingmagazine.com/2015/08/understanding-critical-css/">critical CSS</a>.</p> <h3>Strike a balance, make a decision</h3> <p>It is a conscious decision for me to inline most of the JS and CSS, because as I mentioned, this is a personal website, the JS and CSS are minimal (please don't believe me, inspect it with DevTools).</p> <p>I load external CSS as well. If you inspect this page with DevTools, I load two CSS file externally - <code>prism-{light,dark}.css</code> (used for syntax highlight) and <code>fonts.css</code> (use for display nicer fonts).</p> <p>I also inline a few CSS files conditionally (e.g. <code>table.css</code>) only when I need them on a particular template. (e.g. this <a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">blog post</a> has a table).</p> <p>It is your website, try to strike a balance between complexity and performance. Choose the best one, set up and forget about it (or revisit later).</p> <h2>Alrighty, what's next?</h2> <p>Yay! We have minified all our HTML, CSS and JavaScript - both inline and external. We learnt about the Nunjucks <code>macro</code> and <code>include</code> as well.</p> <p>This is how I handle the minification of this website <a href="https://jec.fish/">jec.fish</a> as well. Did you feel my site load fast (or slow)?</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1265890220001525760"> twitter.com/jecfish/status/1265890220001525760 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>How many favicons should you have in your site?</title> <link href="https://jec.fish/blog/favicons-manifest"/> <updated>2020-05-29T00:00:00-00:00</updated> <id>https://jec.fish/blog/favicons-manifest</id> <summary>How many favicon sizes and formats do you need? How about manifest? What are the tools to generate them?</summary> <category term="blog"/> <content type="html"><p>How many favicon (including app icon) sizes and formats do you need? What are the tools to generate them? Let's explore.</p> <p>Take note that there are files and meta tags that you might need together with the favicons:</p> <ul> <li>Files: <code>web.manifest</code>, <code>browserconfig.xml</code>, etc</li> <li>Meta tags: <code>msapplication-TileColor</code>, <code>mobile-web-app-capable</code> etc</li> </ul> <p>We will go through these as well.</p> <p>This is the 7th post of the series - <a href="https://jec.fish/blog/building-my-static-site-with-11ty">building personal static site with 11ty</a>. Here is the GitHub Repo <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> if you prefer to read the code first.</p> <h2>How many favicons do we need?</h2> <p>Short answer - it depends (because that's how people answer, always 😂).</p> <p>Slightly longer answer - it depends on:</p> <ul> <li>How many different platforms you want to support - iOS, Windows, web browser, etc</li> <li>Do you want to support older browsers / platforms - IE 8, Windows 8.1, iOS 6, etc</li> <li>Do you need pixel perfect icons - is it okay to let the platforms scale them?</li> </ul> <p>Longer answer - read this <a href="https://realfavicongenerator.net/blog/new-favicon-package-less-is-more/">blog post</a> by Philippe Bernard. It talks about whether we should support all favicon formats or using the &quot;less is more&quot; approach.</p> <h2>How many favicons does this website have?</h2> <p>Glad you asked. This website has:</p> <ul> <li><strong>10 favicons</strong> in total.</li> <li>Favicons are in 3 different formats - <code>.ico</code>, <code>.png</code> and <code>.svg</code>.</li> <li>The largest favicon is <code>512x512 px</code>.</li> </ul> <h2>Where can we spot the favicons?</h2> <p>The most common place we can see favicon is on a <strong>browser tab</strong>, a tiny one.</p> <p>However, favicons can also show up in multiple places, some with much bigger size. Here are some examples:</p> <ul> <li>Firefox home page - When your website is one of the top visit sites</li> <li>Mobile home screen - Android, iOS home screen</li> <li>Desktop Chrome Web Store (Apps) - When users &quot;install&quot; or add to home screen</li> <li>Desktop shortcuts - Windows Tile, Mac Launchpad &amp; Dock</li> <li>Safari pinned tab - Display on Safari and MacBook Touch Bar. Users right click &gt; select &quot;Pin Tab&quot; on your page.</li> <li>Quick Look (Mac) - User can right click on the app and select quick look to check the app version info. Users can enlarge the page to full screen (huge favicon).</li> </ul> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/favicons-manifest-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/favicons-manifest-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/favicons-manifest-01.jpg"> <img src="https://jec.fish/assets/img/blog/favicons-manifest-01.jpg" alt="Places you can spot favicons."> </picture> </div><figcaption>Places you can spot favicons.</figcaption></figure> <p>Should we care about all of them? <strong>Probably not</strong>. I mean, how often do people add a blog to home screen? (especially an unknown blog like mine 😆)</p> <p>However, there's no harm supporting that. Nevertheless, I am exploring out of curiosity.</p> <h2>Favicon design template size</h2> <p>The design template should be square in shape, or with at least <code>512x512px</code> in size. I suggest to use <code>1024px</code> or above because who knows in the future that the favicon size will grow again!</p> <h3>How do I know how many and what favicons I need?</h3> <p>There are online tools! 😃 Here are a few tools I used to generate favicons and other useful files and meta tags.</p> <h3>realfavicongenerator.net</h3> <p>Highly recommended: <a href="https://realfavicongenerator.net/">realfavicongenerator.net</a>. It is a good free tool to generate favicons to support different platforms.</p> <p>You can also pass a link to analyse your current website favicon condition and see recommendations on how to fix it.</p> <p>Upload your favicon (or just click on &quot;Demo with random image&quot;), and it will show you a preview on how your favicons will look like on different platforms. There are quite a few options you can play around as well.</p> <p>It is an all in one tool - helps you to generate all necessary <code>png</code>, <code>ico</code> and <code>svg</code> file, plus the <code>manifest</code> and <code>browserconfig.xml</code> (for Microsoft Tile) files.</p> <p>Try it!</p> <p>If you want to know the usage of each icon, check out their <a href="https://realfavicongenerator.net/faq">FAQ</a> for detailed explanation.</p> <h3>faviconit.com</h3> <p>A simple one - <a href="http://faviconit.com/">faviconit.com</a>. If you don't like to read (while the previous one offers more explanation, it might be confusing for some users), you may use this tool. This website requires you to upload a photo, then it will generate an <code>ico</code> and <code>png</code> file accordingly, along with the <code>browserconfig.xml</code>.</p> <p>However, it doesn't generate a manifest file for you. (You may not need it either)</p> <h3>splash-screens</h3> <p>If you want to provide a native like experience for your &quot;installed&quot; website, you may want to generate a splash screen when the user launches your website from the home screen. iOS requires special meta tags and links to handle that, you can use this tool - <a href="https://appsco.pe/developer/splash-screens">splash-screens</a> to generate that.</p> <p>*I did not use this for this site.</p> <h3>The 2020 Guide to FavIcons for Nearly Everyone and Every Browser</h3> <p>I read <a href="https://www.emergeinteractive.com/insights/detail/the-essentials-of-favicons">this article - The 2020 Guide to FavIcons for Nearly Everyone and Every Browser</a> as well because the title is eye-catching. It talks about a brief history of favicon, a list of current favicon sizes and deprecated favicon sizes. It also provide templates (in GitHub) for you to start design (Photoshop and Sketch).</p> <h3>My favicon setup</h3> <p>Combining all the knowledge from the links above, here are all the new files I created:-</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># file structure</span><br><br><span class="token punctuation">-</span> assets<br> <span class="token punctuation">-</span> img<br> <span class="token punctuation">-</span> favicons<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>120.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>144.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>150.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>152.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>180.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>192.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>310.png<br> <span class="token punctuation">-</span> favicon<span class="token punctuation">-</span>512.png<br> <span class="token punctuation">-</span> favicon.svg<br><span class="token punctuation">-</span> src<br> <span class="token punctuation">-</span> favicon.ico<br> <span class="token punctuation">-</span> _includes<br> <span class="token punctuation">-</span> base<span class="token punctuation">-</span>favicon.partial.njk<br> <span class="token punctuation">-</span> root<br> <span class="token punctuation">-</span> browserconfig.njk<br> <span class="token punctuation">-</span> web<span class="token punctuation">-</span>manifest.njk</code></pre> <p>.</p> <p>Notice that I create a file called <code>base-favicon.partial.njk</code>. It is because there are too many favicons to define. I prefer to put them in a separate file, then including them in the <code>base.layout.njk</code>.</p> <p>Here is the code:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> ...<br> {% include 'base-favicon.partial.njk' %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> ...<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br></code></pre> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base-favicon.partial.njk --></span><br><br><span class="token comment">&lt;!-- generics: classic --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>shortcut icon<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/favicon.ico<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>icon<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/favicon.ico<span class="token punctuation">"</span></span><br> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>16x16 32x32 48x48 64x64<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><br>{% set icons = [128, 196] %}<br><span class="token comment">&lt;!-- generics: modern --></span><br>{% for icon in icons %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>icon<span class="token punctuation">"</span></span> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{icon}}x{{icon}}<span class="token punctuation">"</span></span><br> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.base.favicons }}favicon-{{icon}}.png<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>{% endfor %}<br><br><span class="token comment">&lt;!-- iOS --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>mask-icon<span class="token punctuation">"</span></span> <span class="token attr-name">color</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.themeColor }}<span class="token punctuation">"</span></span><br> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.base.favicons }}favicon.svg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><br>{% set icons = [120, 152, 180] %}<br>{% for icon in icons %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>apple-touch-icon<span class="token punctuation">"</span></span> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{icon}}x{{icon}}<span class="token punctuation">"</span></span><br> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.base.favicons }}favicon-{{icon}}.png<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>{% endfor %}<br><br><span class="token comment">&lt;!-- Windows 8 IE 10 --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>msapplication-TileColor<span class="token punctuation">"</span></span><br> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.themeColor }}<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>msapplication-TileImage<span class="token punctuation">"</span></span><br> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.base.favicons }}favicon-144.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token comment">&lt;!-- Windows 8.1 + IE11 and above --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>msapplication-config<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>browserconfig.xml<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token comment">&lt;!-- Web App manifest --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>manifest<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/web.manifest<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>theme-color<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{ env.themeColor }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br></code></pre> <p>.</p> <p>Notice that there are some new <code>env.*</code> variables in the previous code. Let's add them in our existing global data file <code>src/_data/env.js</code>.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_data/env.js</span><br><br>baseUrl <span class="token operator">=</span> <span class="token operator">...</span><span class="token punctuation">;</span> <span class="token comment">// added previously</span><br><br>modules<span class="token punctuation">.</span>export <span class="token operator">=</span> <span class="token punctuation">{</span><br> siteName<span class="token operator">:</span> <span class="token string">'your_website_name'</span><span class="token punctuation">,</span> <br> themeColor<span class="token operator">:</span> <span class="token string">'#fffff'</span><span class="token punctuation">,</span> <span class="token comment">// replace with any color</span><br> dir<span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token operator">...</span><br> favicons<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/assets/img/favicons/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> base<span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token operator">...</span><br> favicons<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>baseUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dir<span class="token punctuation">.</span>favicons<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span></code></pre> <p>.</p> <p>Next let's work on the <code>browserconfig</code> which Windows requires:-</p> <pre class="language-xml"><code class="language-xml"><span class="token comment">&lt;!-- src/root/browserconfig.njk --></span><br><br>---<br>layout: false<br>permalink: browserconfig.xml<br>ignore: true<br>---<br><span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>browserconfig</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>msapplication</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>tile</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>square150x150logo</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{env.base.favicons}}favicon-150.png<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>square310x310logo</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>{{env.base.favicons}}favicon-310.png<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>TileColor</span><span class="token punctuation">></span></span>{{ env.themeColor }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>TileColor</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>tile</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>msapplication</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>browserconfig</span><span class="token punctuation">></span></span><br></code></pre> <p>.</p> <p>Last one. The <a href="https://web.dev/add-manifest/">web app manifest</a> is a JSON file that tells the browser about your Progressive Web App and how it should behave when installed on the user's desktop or mobile device.</p> <pre class="language-json"><code class="language-json"><span class="token comment">// src/root/web-manifest.njk</span><br><br>---<br>layout<span class="token operator">:</span> <span class="token boolean">false</span><br>permalink<span class="token operator">:</span> web.manifest<br>ignore<span class="token operator">:</span> <span class="token boolean">true</span><br>---<br><span class="token punctuation">{</span><br> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"{{ env.siteName }}"</span><span class="token punctuation">,</span><br> <span class="token property">"short_name"</span><span class="token operator">:</span> <span class="token string">"{{ env.siteName }}"</span><span class="token punctuation">,</span><br> <span class="token property">"start_url"</span><span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span><br> <span class="token property">"icons"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br> <span class="token punctuation">{</span><br> <span class="token property">"src"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.favicons }}favicon-192.png"</span><span class="token punctuation">,</span><br> <span class="token property">"sizes"</span><span class="token operator">:</span> <span class="token string">"192x192"</span><span class="token punctuation">,</span><br> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"image/png"</span><br> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">{</span><br> <span class="token property">"src"</span><span class="token operator">:</span> <span class="token string">"{{ env.base.favicons }}favicon-512.png"</span><span class="token punctuation">,</span><br> <span class="token property">"sizes"</span><span class="token operator">:</span> <span class="token string">"512x512"</span><span class="token punctuation">,</span><br> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"image/png"</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">]</span><span class="token punctuation">,</span><br> <span class="token property">"theme_color"</span><span class="token operator">:</span> <span class="token string">"{{ env.themeColor }}"</span><span class="token punctuation">,</span><br> <span class="token property">"background_color"</span><span class="token operator">:</span> <span class="token string">"{{ env.themeColor }}"</span><span class="token punctuation">,</span><br> <span class="token property">"display"</span><span class="token operator">:</span> <span class="token string">"standalone"</span><br><span class="token punctuation">}</span><br></code></pre> <h2>Bonus: How do I test?</h2> <p>You might want to test how the favicons show up in each platforms. There is no easy way to do it, you may need to test manually or create your own automation test (may not help if you want to test the photo quality).</p> <p>There are some tools could help you to test on some of it.</p> <p>To test on older browsers, like Internet Explorer, you can sign up for <a href="https://www.browserstack.com/">BrowserStack</a> or <a href="https://saucelabs.com/">Saucelabs</a>. These two testing tools support multiple Desktop OSes, mobile OSes and browser versions. It makes testing easier (don't need to have a physical device ourselves).</p> <p>However, the free trial period is short, and you will need to subscribe to continue using it.</p> <p>On the other hand, you can use Chrome DevTools to test if your web <code>manifest</code> is configured correctly.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:69.23828125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/favicons-manifest-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/favicons-manifest-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/favicons-manifest-03.jpg"> <img src="https://jec.fish/assets/img/blog/favicons-manifest-03.jpg" alt="Chrome DevTools > Application Panel > Manifest"> </picture> </div><figcaption>Chrome DevTools > Application Panel > Manifest</figcaption></figure> <h2>Bonus: Maskable icon</h2> <p>If your website is a Progressive Web App (PWA) - Maskable icons are a new icon format that gives you more control and let PWA use adaptive icons. If you supply a maskable icon, your icon can fill up the entire shape and look great on all Android devices.</p> <p>You may read <a href="https://web.dev/maskable-icon/">this article</a> for more details. This <a href="https://maskable.app/editor">maskabel.app</a> is a tool to help you create one.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:73.046875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/favicons-manifest-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/favicons-manifest-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/favicons-manifest-02.jpg"> <img src="https://jec.fish/assets/img/blog/favicons-manifest-02.jpg" alt="Normal transparent icons vs Maskable icons"> </picture> </div><figcaption>Normal transparent icons vs Maskable icons</figcaption></figure> <h2>Alrighty, what's next?</h2> <p>Yay! We have generated and set up favicons, themes, and web manifest to support almost all of the platforms.</p> <p>I wish we could have shorter, easier or a one size fit all favicon solution 😆. Keeping track on how each platform require a slightly different image size and meta tag is not fun at all.</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1266319579313143809"> twitter.com/jecfish/status/1266319579313143809 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>My Journey as a Technical Presenter</title> <link href="https://jec.fish/deck/my-journey-as-a-tech-presenter"/> <updated>2020-06-03T00:00:00-00:00</updated> <id>https://jec.fish/deck/my-journey-as-a-tech-presenter</id> <summary>6 years into tech speaking, how I got better at that. Sharing tips for people who plan to start doing technical presentations.</summary> <category term="deck"/> <content type="html"><p>Sharing my journey as a technical speaker, and some tips for people who plan to start doing technical presentations.</p> <p>Started giving a public technical presentation in Nov 2014. It wasn't a great start. However, what mattered was that I've done that, and made the first move.</p> <p>Delivered this talk on <a href="http://goo.gle/wd-academy">Women Developer Academy</a> (WDA) graduation day. WDA is a program to equip women in tech with the skills and resources, and support their need to become tech presenters and speakers.</p> <p>Here's the slides:</p> <script async="" class="speakerdeck-embed" data-id="98f916f44fea4dfaaa4809ce3877f758" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/my-journey-as-technical-presenter">https://speakerdeck.com/jecfish/my-journey-as-technical-presenter</a> </noscript> <p>.</p> <p>Both female engineers and tech speakers are still lagging in the industry. Let's #changetheratio together. 💪🏼</p> <p><strong>Representation matters.</strong> I recommend you to read Hui Jing's awesome post &quot;<a href="https://chenhuijing.com/blog/musings-on-speaking-at-conferences/#%F0%9F%91%9F">Musings on speaking at conferences</a>&quot; on how does it feels, as a Southeast Asian female speaking in tech conferences.</p> <p>.</p> <p>The talk is recorded, will update again when the link becomes available.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:63.96484375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/my-journey-as-a-tech-presenter-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/my-journey-as-a-tech-presenter-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/my-journey-as-a-tech-presenter-01.jpg"> <img src="https://jec.fish/assets/img/deck/my-journey-as-a-tech-presenter-01.jpg" alt="A screenshot during the WDA virtual graduation day"> </picture> </div><figcaption>A screenshot during the WDA virtual graduation day</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1268165205952884736"> twitter.com/jecfish/status/1268165205952884736 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Creating Filters, Shortcodes and Plugins</title> <link href="https://jec.fish/blog/creating-filters-shortcodes-plugins"/> <updated>2020-06-06T00:00:00-00:00</updated> <id>https://jec.fish/blog/creating-filters-shortcodes-plugins</id> <summary>How to create filters, shortcodes and plugins in Eleventy.</summary> <category term="blog"/> <content type="html"><p>Filters, shortcodes and plugins are among the most powerful features in 11ty. Let's understand the concepts and learn how to create them.</p> <p>This is the 8th post of the series - <a href="https://jec.fish/blog/building-my-static-site-with-11ty">building personal static site with 11ty</a>. Here is the GitHub Repo <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> if you prefer to read the code first.</p> <h2>Filters</h2> <p>Every blog post has a date. I stored the date in ISO date format <strong>yyyy-MM-dd</strong> (e.g. 2020-12-31). However, I would like to display it in <strong>MMM dd, yyyy</strong> (e.g. Dec 31, 2020) format because I find that more readable. (Scroll to the top of this post to see the date in action)</p> <p><a href="https://www.11ty.dev/docs/filters/">Filters</a> offer us a way to process our data and output it to a format we want. In our case, we will be creating a date format filter. Let's name it <code>dateDisplay</code>. (naming is hard, couldn't figure out a better name yet. 😂)</p> <pre class="language-js"><code class="language-js"><span class="token comment">// output: Dec 31, 2020</span><br><br><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token string">'2020-12-31'</span> <span class="token operator">|</span> dateDisplay <span class="token punctuation">}</span><span class="token punctuation">}</span><br></code></pre> <p>The syntax above shows how we will use the filter in our code. Next, let's write the <code>dateDisplay</code> filter function!</p> <pre class="language-js"><code class="language-js"><span class="token comment">// configs/date-display.filter.js</span><br><br><span class="token keyword">const</span> <span class="token punctuation">{</span> parseISO<span class="token punctuation">,</span> format <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'date-fns/fp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">input</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> formatter <span class="token operator">=</span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">'MMM dd, yyyy'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> date <span class="token operator">=</span> input <span class="token keyword">instanceof</span> <span class="token class-name">Date</span> <span class="token operator">?</span> input <span class="token operator">:</span> <span class="token function">parseISO</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">return</span> <span class="token function">formatter</span><span class="token punctuation">(</span>date<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre> <p>The code is quite expressive itself. The input could be a date or a string in ISO format. We are using the <a href="https://date-fns.org/">date-fns</a> package to format the date. Install it by running <code>npm install date-fns -D</code>.</p> <p>Now we have our function ready, but we have not told 11ty that this is a filter. Let's configure that in our <code>.eleventy.js</code> file.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// .eleventy.js</span><br><br><span class="token comment">// add these 2 lines</span><br><span class="token keyword">const</span> dateDisplay <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./configs/date-display.filter'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">'dateDisplay'</span><span class="token punctuation">,</span> dateDisplay<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <h2>Shortcodes</h2> <p>I embedded Youtube videos in almost all my presentation posts - <a href="https://jec.fish/deck">jec.fish/deck</a>. The code to embed the Youtube video, as copied from Youtube, would be something like this:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>iframe</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>https://www.youtube.com/embed/[your-video-id]<span class="token punctuation">"</span></span><br> <span class="token attr-name">frameborder</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">allowfullscreen</span><br> <span class="token attr-name">allow</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>iframe</span><span class="token punctuation">></span></span></code></pre> <p>It is okay for us to write (or copy paste) these code manually to each page with YouTube video. However, it's repetitive. Also, it might be harder to apply changes in the future, for example:</p> <ul> <li>to remove the <code>allowfullscreen</code> option for all videos</li> <li>to apply stylings for all videos - border, width, height, etc.</li> </ul> <p>It would be good if we can shorten the code and centralize the changes. <a href="https://www.11ty.dev/docs/shortcodes/">Shortcodes</a> could help us with that.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// output: &lt;iframe ...>&lt;/iframe></span><br><br><span class="token punctuation">{</span><span class="token operator">%</span> youtube <span class="token string">'your-video-id'</span> <span class="token operator">%</span><span class="token punctuation">}</span><br></code></pre> <p>The code above shows how's a shortcode syntax looks like. We will create a <code>youtube</code> shortcode function which accepts a video id. The output would be the HTML iframe.</p> <p>Here is how our function looks like:</p> <pre class="language-js"><code class="language-js"><span class="token comment">// configs/youtube.shortcode.js</span><br><br><span class="token keyword">const</span> outdent <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'outdent'</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">{</span> newline<span class="token operator">:</span> <span class="token string">' '</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">id</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">return</span> outdent<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br> &lt;div class="video-wrapper"><br> &lt;iframe src="https://www.youtube.com/embed/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br> frameborder="0" allowfullscreen <br> allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"><br> &lt;/iframe><br> &lt;/div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre> <p>The code is fairly self-explanatory. We wrap the iframe with additional <code>div</code> to style the YouTube iframe. We use the <code>outdent</code> function (install it with command <code>npm install outdent -D</code>) to format the output string, replacing the new lines with space.</p> <p>The reason we use outdent is because the way 11ty processes new lines is not what we expected - refer to the Eleventy <a href="https://www.11ty.dev/docs/languages/markdown/#there-are-extra-and-in-my-output">explanation here</a>. If you prefer to not install the <code>outdent</code> package, just rewrite your code above (all the HTML) in a single line. I don't like it because it is not readable during development.</p> <p>Here is the CSS to style our <code>video-wrapper</code>, to make our YouTube video display responsively (refer to <a href="https://howchoo.com/g/mgflywu4ytc/how-to-make-youtube-videos-responsive-without-js">this article</a> for details.). I placed this css in <code>src/_includes/writing.layout.css</code> because that's where my video lives in, but you can place it in any css file with video.</p> <pre class="language-css"><code class="language-css"><span class="token comment">/* Put it in any css file with video */</span><br><br><span class="token selector">.video-wrapper</span> <span class="token punctuation">{</span><br> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span><br> <span class="token property">padding-bottom</span><span class="token punctuation">:</span> 56.25%<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.video-wrapper iframe</span> <span class="token punctuation">{</span><br> <span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br> <span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br> <span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br> <span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <p>Great, now we have the function ready, configure that in <code>.eleventy.js</code> so we can use this shortcode in our template.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// .eleventy.js</span><br>eleventyConfig<span class="token punctuation">.</span><span class="token function">addShortcode</span><span class="token punctuation">(</span><br> <span class="token string">'youtube'</span><span class="token punctuation">,</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./configs/youtube.shortcode'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>I use the same concept to create <code>speakerdeck</code>, <code>img</code> and <code>twitter</code> shortcodes on this website.</p> <h2>Plugins</h2> <p>The filters and shortcodes that you've created might be useful for others. You might want to reuse it in other projects or just publish it in <a href="https://www.npmjs.com/">npm</a> so other people can benefit from it.</p> <p>We can create them as a plugin. Let me show you how we can make our <code>date-display</code> filter and <code>youtube</code> shortcode a plugin instead. 😃</p> <p>First, create a folder <code>plugins</code> and an <code>index.js</code> file in it.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># folder structure</span><br><span class="token punctuation">-</span> plugins<br> <span class="token punctuation">-</span> index.js <span class="token comment"># new file</span></code></pre> <p>Next, move the filter and shortcode to the <code>plugins</code> folder instead.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># folder structure</span><br><span class="token punctuation">-</span> plugins<br> <span class="token punctuation">-</span> index.js <span class="token comment"># new file</span><br> <span class="token punctuation">-</span> date<span class="token punctuation">-</span>display.filter.js<br> <span class="token punctuation">-</span> youtube.shortcode.js</code></pre> <p>Remove the code filter and shortcode configuration we added in <code>.eleventy.js</code> just now. Save it. Try to run <code>npm start</code> now, you will hit an error (something like &quot;unknown block tag: youtube&quot;). We will fix this in a moment. 😉</p> <p>Open our <code>plugins/index.js</code> file. Here is the code.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// plugins/index.js</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br> eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><br> <span class="token string">'dateDisplay'</span><span class="token punctuation">,</span><br> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./date-display.filter'</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> eleventyConfig<span class="token punctuation">.</span><span class="token function">addShortcode</span><span class="token punctuation">(</span><br> <span class="token string">'youtube'</span><span class="token punctuation">,</span><br> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./youtube.shortcode'</span><span class="token punctuation">)</span><br> <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre> <p>Did you notice that this plugin code is similar to our <code>.eleventy.js</code> configuration file (except the differences in <code>require</code> file paths)? It is because the theory of creating plugins is similar to <code>.eleventy.js</code> configuration.</p> <p>Now, let's update our <code>.eleventy.js</code> to use our plugins.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// .eleventy.ts</span><br>eleventyConfig<span class="token punctuation">.</span><span class="token function">addPlugin</span><span class="token punctuation">(</span><span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./plugins'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Try to start your application again. The errors are gone. Date and YouTube video are shown correctly on screen.</p> <p>Splitting your code into plugins in the same projects might not be very useful. However, if you need the same set of filters and shortcodes across multiple projects, you can export them as npm packages and reuse them across projects.</p> <p>In fact, we can also install some plugins that others have published! Here is a list of <a href="https://www.11ty.dev/docs/plugins/">community created plugins</a>.</p> <p>These are two plugins I used in this website:</p> <ul> <li><a href="https://github.com/11ty/eleventy-plugin-rss">@11ty/eleventy-plugin-rss</a> - plugins for generating RSS feed.</li> <li><a href="https://github.com/11ty/eleventy-plugin-syntaxhighlight">@11ty/eleventy-plugin-syntaxhighlight</a> - plugins for syntax highlighting in Markdown, Nunjucks and other templates.</li> </ul> <h2>Alrighty, what's next?</h2> <p>Yay! We've learnt how to create filters and shortcodes, and how to convert them into plugins to make it reusable across projects.</p> <p>Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1269196934666784768"> twitter.com/jecfish/status/1269196934666784768 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Supporting Dark Mode in Your Website</title> <link href="https://jec.fish/blog/supporting-dark-mode"/> <updated>2020-06-08T00:00:00-00:00</updated> <id>https://jec.fish/blog/supporting-dark-mode</id> <summary>A guide to implement light and dark modes on websites</summary> <category term="blog"/> <content type="html"><p>Working with and supporting dark mode is fun. However, it's not just for fun, it's a feature, and some may even say it's a necessity.</p> <p>This post will focus on how I implemented dark mode support in this website. Click on the top right toggle on this page to see it in action. 😃</p> <p>Here is the GitHub repo <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a> if you prefer to read the code straightaway.</p> <style> kbd { border: 1px solid; border-radius: 4px; padding: 0 6px; font-size: smaller; margin: 0 4px; vertical-align: text-top; } </style> <h2>What are the requirements?</h2> <p>Our website needs to support dark and light mode. I will refer them as <strong>color scheme</strong> from here onwards.</p> <p>Here are the requirements:-</p> <ol> <li>Display the page with the user's <strong>preferred color scheme</strong>. (e.g. if the user's system settings is dark mode, our site should show dark mode as default).</li> <li><strong>Option to change</strong> the color scheme - in our case, show a toggle on the top right corner.</li> <li>Once the user has changed the color scheme, we will use that setting for the <strong>entire session</strong> (until the user closes the tab). We will use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage">session storage</a> for that.</li> <li>Ensure <strong>smooth transition</strong> when changing the color scheme.</li> <li><strong>Dim the images' color</strong> when it's in dark mode.</li> <li>In case no color scheme is detected, always <strong>fall back to light mode</strong>.</li> </ol> <h2>First things first, how to test this?</h2> <p>Before we jump into coding, let's talk about how we can test color scheme changes. We will be spending a lot of time on testing this.</p> <p>Here is the guide on how to change the color scheme in your system settings:</p> <ul> <li><a href="https://lifehacker.com/how-to-enable-chromes-new-dark-mode-on-android-1834339091">Android and Apple iOS</a></li> <li><a href="https://www.howtogeek.com/360650/how-to-enable-dark-mode-for-google-chrome/">Windows and macOS</a></li> </ul> <p>However, changing our system settings multiple times is not fun. There's an easier way to do it: Chrome DevTools (CDT) is here to the rescue. We can emulate the color scheme with CDT.</p> <p>There are two ways to do this - the short version (if you remember the command) and a slightly longer version.</p> <p><strong>Short version</strong></p> <ol> <li>Open Chrome DevTools <ul> <li>macOS <kbd>⌘</kbd>+<kbd>Option</kbd>+<kbd>J</kbd></li> <li>Windows <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>J</kbd></li> <li>Right click on page &gt; Select &quot;Inspect&quot;</li> </ul> </li> <li>Run command <ul> <li>macOS <kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd></li> <li>Windows <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd></li> <li>Click on three dots (top right corner) &gt; Select &quot;Run command&quot;</li> </ul> </li> <li>Type in: &quot;dark&quot; (or &quot;light&quot;)</li> <li>Select <strong>Rendering: Emulate CSS prefers-color-scheme: dark</strong></li> </ol> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:71.125%"> <img src="https://jec.fish/assets/img/blog/supporting-dark-mode-04.gif" alt="How to emulate CSS prefers-color-scheme"> </picture> </div><figcaption>How to emulate CSS prefers-color-scheme</figcaption></figure> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:43.65234375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/supporting-dark-mode-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/supporting-dark-mode-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/supporting-dark-mode-01.jpg"> <img src="https://jec.fish/assets/img/blog/supporting-dark-mode-01.jpg" alt="Emulate CSS prefers-color-scheme: dark"> </picture> </div><figcaption>Emulate CSS prefers-color-scheme: dark</figcaption></figure> <p><strong>Slightly longer version</strong></p> <ol> <li>Open Chrome DevTools.</li> <li>Pull out the <strong>Rendering Panel</strong>: <ul> <li>Click on three dots &gt; More tools &gt; Select &quot;Rendering&quot;</li> <li>Click on three dots &gt; Run command &gt; Type &quot;render&quot; &gt; Select &quot;Show Rendering&quot;</li> </ul> </li> <li>Scroll down, look for the option <strong>Emulate CSS media feature prefers-color-scheme</strong>.</li> <li>Change the dropdown value to light, dark, or no emulation.</li> </ol> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.4453125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/supporting-dark-mode-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/supporting-dark-mode-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/supporting-dark-mode-02.jpg"> <img src="https://jec.fish/assets/img/blog/supporting-dark-mode-02.jpg" alt="Rendering panel"> </picture> </div><figcaption>Rendering panel</figcaption></figure> <h2>Detecting user's preferred color scheme</h2> <p>When the page first loads, we will use JavaScript to detect the user's current system settings.</p> <p>We will also detect if any color scheme is stored in session storage. We will use that if it exists, else we use system settings or fall back to light mode if not found.</p> <p>This logic is needed in every web page so we will place the code in the base layout. Here is the implementation.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_includes/base.layout.js</span><br><br><span class="token punctuation">{</span><br> <span class="token keyword">function</span> <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> <span class="token constant">DARK</span> <span class="token operator">=</span> <span class="token string">'dark'</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">LIGHT</span> <span class="token operator">=</span> <span class="token string">'light'</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> isSystemDarkMode <span class="token operator">=</span> matchMedia <span class="token operator">&amp;&amp;</span><br> <span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">'(prefers-color-scheme: dark)'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>matches<span class="token punctuation">;</span><br> <br> <span class="token keyword">let</span> mode <span class="token operator">=</span> sessionStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">'jec.color-scheme'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>mode <span class="token operator">&amp;&amp;</span> isSystemDarkMode<span class="token punctuation">)</span> <span class="token punctuation">{</span><br> mode <span class="token operator">=</span> <span class="token constant">DARK</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br> mode <span class="token operator">=</span> mode <span class="token operator">||</span> <span class="token constant">LIGHT</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>mode <span class="token operator">===</span> <span class="token constant">DARK</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// we will do something later</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br><br> <span class="token comment">// run the code </span><br> <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <p><a href="https://jec.fish/blog/%60https://caniuse.com/#feat=matchmedia%60">window.matchMedia</a> (or just <code>matchMedia</code>) is the web API for finding out whether a media query applies to the document or not. It is widely supported by modern browsers. We can use that to detect <code>prefers-color-scheme</code>, which is the user's system setting.</p> <p>When the color scheme is dark, we need to update our website appearance. We will look into that in a moment. No action is needed for the light color scheme, because it's the default mode.</p> <h2>Creating the color scheme toggle</h2> <p>We use an image as toggle. Place it in base layout too because we want it to show in every page.</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>color-scheme-toggle<span class="token punctuation">"</span></span><br> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>/assets/img/icons/dark.svg<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>toggle dark mode<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br></code></pre> <p>We gave the toggle an id <strong>#color-scheme-toggle</strong>. We will change the image <code>src</code> to &quot;light&quot; and <code>alt</code> to &quot;toggle light mode&quot; if the user clicks and toggles to light mode, and vice versa.</p> <p>Note that you can replace the image with your own dark and light mode images. It is okay to use text buttons too.</p> <p>We will handle the toggle click event in a moment.</p> <h2>How do we style for each color scheme?</h2> <p>We will style our website by using these two approaches:-</p> <ul> <li><strong>Split</strong> into light and dark CSS files, and load them conditionally. In our case, we will toggle between these 2 stylesheets for syntax highlighting - <code>prism-dark.css</code> and <code>prism-light.css</code>.</li> <li>Append a <strong>CSS class</strong> in the HTML <code>body</code> tag when the color scheme has changed. In our case, we will append the <code>dark-mode</code> CSS class in the <code>body</code> tag when it's in dark mode.</li> </ul> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/_includes/base.layout.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br> <span class="token comment">&lt;!-- Not every page need syntax highlight --></span><br> {% if prism %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>prism-css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><br> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>assets/css/prism-light.css<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> {% endif %}<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br> <span class="token comment">&lt;!-- we will toggle the dark-mode CSS class --></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br> ...<br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span><br></code></pre> <pre class="language-css"><code class="language-css"><span class="token comment">/* src/_includes/base.layout.css */</span><br><br><span class="token selector">body</span> <span class="token punctuation">{</span><br> <span class="token property">background-color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br> <span class="token property">color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">body.dark-mode</span> <span class="token punctuation">{</span><br> <span class="token property">background-color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span><br> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <h2>What happens when the toggle is clicked</h2> <p>When the user clicks on the color scheme toggle, we need to:</p> <ul> <li>toggle the body tag's <strong>dark-mode</strong> CSS class</li> <li>update the <strong>jec.color-scheme</strong> session storage key</li> <li>update the <strong>#color-scheme-toggle</strong>'s <code>src</code> and <code>alt</code></li> <li>update the <strong>#prism-css</strong> <code>href</code> if it exists</li> </ul> <p>We will also fire a custom event. Let's name it <strong>&quot;colorSchemeChanged&quot;</strong> because we want to <strong>decouple the page specific changes</strong>.</p> <p>For example, my profile photo in <a href="https://jec.fish/">home page</a> changes when the color scheme changes. It is specific to that page, and I don't need that logic in other pages.</p> <p>By firing a custom event, each page can listen to the event and implement page specific updates independently.</p> <p>Here is the code.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_includes/base.layout.css</span><br><br><span class="token punctuation">{</span><br> <span class="token keyword">const</span> bodyEl <span class="token operator">=</span> document<span class="token punctuation">.</span>body<span class="token punctuation">;</span><br> <span class="token keyword">const</span> toggleEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#color-scheme-toggle'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> prismEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#prism-css'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">DARK</span> <span class="token operator">=</span> <span class="token string">'dark'</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">LIGHT</span> <span class="token operator">=</span> <span class="token string">'light'</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">COLOR_SCHEME_CHANGED</span> <span class="token operator">=</span> <span class="token string">'colorSchemeChanged'</span><span class="token punctuation">;</span><br><br> toggleEl<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> isDark <span class="token operator">=</span> bodyEl<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">(</span><span class="token string">'dark-mode'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> mode <span class="token operator">=</span> isDark <span class="token operator">?</span> <span class="token constant">DARK</span> <span class="token operator">:</span> <span class="token constant">LIGHT</span><span class="token punctuation">;</span><br> sessionStorage<span class="token punctuation">.</span><span class="token function">setItem</span><span class="token punctuation">(</span><span class="token string">'jec.color-scheme'</span><span class="token punctuation">,</span> mode<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>isDark<span class="token punctuation">)</span> <span class="token punctuation">{</span><br> toggleEl<span class="token punctuation">.</span>src <span class="token operator">=</span> toggleEl<span class="token punctuation">.</span>src<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token constant">DARK</span><span class="token punctuation">,</span> <span class="token constant">LIGHT</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> toggleEl<span class="token punctuation">.</span>alt <span class="token operator">=</span> toggleEl<span class="token punctuation">.</span>alt<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token constant">DARK</span><span class="token punctuation">,</span> <span class="token constant">LIGHT</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>prismEl<span class="token punctuation">)</span> prismEl<span class="token punctuation">.</span>href <span class="token operator">=</span> prismEl<span class="token punctuation">.</span>href<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token constant">LIGHT</span><span class="token punctuation">,</span> <span class="token constant">DARK</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br> toggleEl<span class="token punctuation">.</span>src <span class="token operator">=</span> toggleEl<span class="token punctuation">.</span>src<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token constant">LIGHT</span><span class="token punctuation">,</span> <span class="token constant">DARK</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> toggleEl<span class="token punctuation">.</span>alt <span class="token operator">=</span> toggleEl<span class="token punctuation">.</span>alt<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token constant">LIGHT</span><span class="token punctuation">,</span> <span class="token constant">DARK</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>prismEl<span class="token punctuation">)</span> prismEl<span class="token punctuation">.</span>href <span class="token operator">=</span> prismEl<span class="token punctuation">.</span>href<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token constant">DARK</span><span class="token punctuation">,</span> <span class="token constant">LIGHT</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br><br> toggleEl<span class="token punctuation">.</span><span class="token function">dispatchEvent</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">CustomEvent</span><span class="token punctuation">(</span><br> <span class="token constant">COLOR_SCHEME_CHANGED</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> detail<span class="token operator">:</span> mode <span class="token punctuation">}</span><br> <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token punctuation">{</span><br> <span class="token comment">// init...</span><br><span class="token punctuation">}</span></code></pre> <p>If you want to learn more about custom event, here is the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent">documentation of Custom Event API</a> by MDN.</p> <p>Next, we can update our <code>init</code> function from just now. Trigger the color scheme toggle click event when it is in dark mode.</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/_includes/base.layout.css</span><br><span class="token operator">...</span><br><br><span class="token punctuation">{</span><br> <span class="token keyword">function</span> <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token operator">...</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>mode <span class="token operator">===</span> <span class="token constant">DARK</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// add this line</span><br> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#color-scheme-toggle'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><br> <span class="token punctuation">}</span><br> <span class="token operator">...</span><br><span class="token punctuation">}</span></code></pre> <h3>Handling page specific changes</h3> <p>Here is an example on how you could listen to the <code>colorSchemeChanged</code> custom event. We will update our profile photo's <code>src</code> on the home page when the color scheme changes.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:63.625%"> <img src="https://jec.fish/assets/img/blog/supporting-dark-mode-03.gif" alt="Toggle profile photo"> </picture> </div><figcaption>Toggle profile photo</figcaption></figure> <p>Prepare two profile photos, one for light and one for dark mode. Here is the HTML:</p> <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/root/index.njk --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>profilePhoto<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>assets/img/me-light.jpg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br></code></pre> <p>Here is the code:-</p> <pre class="language-js"><code class="language-js"><span class="token comment">// src/root/index.js</span><br><br><span class="token punctuation">{</span><br> <span class="token keyword">const</span> toggleEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#color-scheme-toggle'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">DARK</span> <span class="token operator">=</span> <span class="token string">'dark'</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">LIGHT</span> <span class="token operator">=</span> <span class="token string">'light'</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> <span class="token constant">COLOR_SCHEME_CHANGED</span> <span class="token operator">=</span> <span class="token string">'colorSchemeChanged'</span><span class="token punctuation">;</span><br><br> toggleEl<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token constant">COLOR_SCHEME_CHANGED</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> isDark <span class="token operator">=</span> e<span class="token punctuation">.</span>detail <span class="token operator">===</span> <span class="token constant">DARK</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> imgEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">#profilePhoto</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> mode <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token constant">DARK</span><span class="token punctuation">,</span> <span class="token constant">LIGHT</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>isDark<span class="token punctuation">)</span> mode<span class="token punctuation">.</span><span class="token function">reverse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> imgEl<span class="token punctuation">.</span>src <span class="token operator">=</span> imgEl<span class="token punctuation">.</span>src<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span>mode<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> mode<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <h2>Dim the image's colors for dark mode</h2> <p>Sometimes, the image's colors might be too bright when displayed on a dark background. We can adjust the image colors with CSS.</p> <p>This website applies the CSS below for all images:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.dark-mode img</span> <span class="token punctuation">{</span><br> <span class="token property">filter</span><span class="token punctuation">:</span> <span class="token function">grayscale</span><span class="token punctuation">(</span>30%<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <h2>Smooth transitions when changing color schemes</h2> <p>It is good to change the color schemes gradually, providing users with visual feedback and a pleasant experience. This site applies background color transitions when the color scheme changes.</p> <pre class="language-css"><code class="language-css"><span class="token comment">/* src/_includes/base.layout.css */</span><br><br><span class="token selector">body</span> <span class="token punctuation">{</span><br> <span class="token property">background-color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br> <span class="token property">color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span><br> <span class="token comment">/* add this line */</span><br> <span class="token property">transition</span><span class="token punctuation">:</span> background-color 300ms ease-in-out 0s<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">body.dark-mode</span> <span class="token punctuation">{</span><br> <span class="token property">background-color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span><br> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <h2>Bonus: Use the ready-made web component</h2> <p>The Chrome team has developed a custom element that allows you to easily insert the Dark Mode 🌒 Toggle on your site.</p> <p>Take a look at the <a href="https://github.com/GoogleChromeLabs/dark-mode-toggle">documentation</a>, NPM install and use it right away... <strong>without having to write the most of the custom code above</strong>.</p> <pre class="language-yml"><code class="language-yml"><span class="token comment"># command</span><br><br>npm install <span class="token punctuation">-</span><span class="token punctuation">-</span>save dark<span class="token punctuation">-</span>mode<span class="token punctuation">-</span>toggle</code></pre> <p>This web component is used in the <a href="https://jec.fish/blog/v8.dev">V8 blog</a>. (TLDR - V8 is the JavaScript engine, used in Chrome and Node.js.)</p> <p>You might be wondering why I did not use that? It is because I only found out after I have shipped my code. 😆</p> <p>Since my implementation is decent for my scenario, I'll just continue to use mine.</p> <p>[Updated: Dec 6, 2020] Oh, receiving several requests, I've updated my site to save theme preference in <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">localStorage</a> for longer persistence.</p> <h2>Alrighty, what's next?</h2> <p>Yay! Dark mode is proudly supported in our website. 😍 Here's the GitHub repo for the code above: <a href="https://github.com/jecfish/jec-11ty-starter">jec-11ty-starter</a>. I'll update the repo whenever I write a new post.</p> <p>If you need a more detailed guide on dark mode in general, including its history, I recommend you to read this article - <a href="https://web.dev/prefers-color-scheme/">Hello darkness, my old friend</a> by my colleague <a href="https://twitter.com/tomayac">Thomas Steiner</a>.</p> <p>In the coming posts, I plan to write about more on how I built my website with 11ty:</p> <ul> <li><a href="https://jec.fish/blog/building-my-static-site-with-11ty">Building Personal Static Site with Eleventy</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-github-actions-and-firebase-hosting">Setting up GitHub Actions and Firebase Hosting</a> ✅</li> <li><a href="https://jec.fish/blog/customizing-file-structure-urls-browsersync">Customizing File Structure, URLs and Browsersync</a> ✅</li> <li><a href="https://jec.fish/blog/automating-image-optimization-workflow">Automating Image Optimization Workflow</a> ✅</li> <li><a href="https://jec.fish/blog/setting-up-seo-and-google-analytics">Setting up SEO and Google Analytics</a> ✅</li> <li><a href="https://jec.fish/blog/minifying-html-js-css">Minifying HTML, JavaScript, CSS - Automate Inline</a> ✅</li> <li><a href="https://jec.fish/blog/favicons-manifest">How many favicons should you have in your site?</a> ✅</li> <li><a href="https://jec.fish/blog/creating-filters-shortcodes-plugins">Creating Filters, Shortcodes and Plugins</a> ✅</li> <li><a href="https://jec.fish/blog/supporting-dark-mode">Supporting Dark Mode in Your Website</a> ✅</li> <li>and probably more!</li> </ul> <p>Let me know if the above topics interest you.</p> <p>That's all. Happy coding!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1269897911233470465"> twitter.com/jecfish/status/1269897911233470465 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Fish 001</title> <link href="https://jec.fish/fish/20200620"/> <updated>2020-06-20T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200620</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200620.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200620.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200620.jpg"> <img src="https://jec.fish/assets/img/fish/20200620.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Helping dad sell fish at #pasar.</p> <p>Auntie: How much is this fish?<br> Me: (Weighting) RM 10.</p> <p>Auntie: Walao, why so expensive one. U knw anot one, ask ur dad weight n see.<br> Me: 🙄 ok lo…</p> <p>Dad: Oi, u calc properly la, it is RM 12.<br> Auntie: Ok, pack it.</p> <p>Me: … 😳🤷😂 (#seniority and #reputation matter ya, charge higher is fine)</p> <p>#活该你付多一点 #我是来倒米的</p> </content> </entry> <entry> <title>Fish 002</title> <link href="https://jec.fish/fish/20200627"/> <updated>2020-06-27T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200627</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:112.3046875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200627.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200627.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200627.jpg"> <img src="https://jec.fish/assets/img/fish/20200627.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Two aunties come n buy same kind of fish concurrently. Weigh and send to the fish processing(dad) queue in FIFO manner.</p> <p>Handing 1st fish to auntie A. Auntie B away for shopping.</p> <p>Auntie A: U sure this is mine anot? Or that one (fish 2) is mine.<br> Me: Sure ah.</p> <p>Auntie A: Ask ur dad.<br> Me: 🙄 (again…)</p> <p>Dad: Oh, u want this ah (fish 2)? Can la, but this one is lighter wor, u sure?<br> Me: (Actually he is js bluffing…)</p> <p>Auntie: Aiya, nvm la, ok la ok la. (Make payment like she's kind)</p> <p>Moral of the story: Top management js bluff their way up and it works. Life as a middle mgmt / operation sucks. 😂 (neh, kan Malaysian always likes to ask customer service: whr is ur manager? #ifeelu 😆)</p> <p>#不要整天叫我问爸爸</p> </content> </entry> <entry> <title>Fish 003</title> <link href="https://jec.fish/fish/20200704"/> <updated>2020-07-04T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200704</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200704.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200704.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200704.jpg"> <img src="https://jec.fish/assets/img/fish/20200704.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>I kept (hide) 1kg prawns and a fish for myself before biz start.</p> <p>Biz is good. All prawns and fishs sold out. I took out my 🐟🦐, wanna split and pack.</p> <p>Auntie A walks over.</p> <p>Auntie A: Yi, no prawn ady ah? (<em>Peeping</em> at my prawn)</p> <p>Me: (feeling alert)</p> <p>Dad: Oh, that's my daughter one. U want ah, can lah, split half for u.</p> <p>Me: … (I see that coming, but fine. Thinking still gt half kg ma)</p> <p>Auntie B passed by, overheard the conversation.</p> <p>Auntie B: Ei, I want oso leh! The other half give me la!</p> <p>Dad: Okok! (Look at me) U take next time la.</p> <p>Me: 😳😳😳</p> <p>Auntie A: How about that fish ah? (looking my 🐟)</p> <p>Me: Walao eh! (Stare at my dad with the expression - dare u say &quot;can la&quot; again, i flip table and resign frm this unpaid job permanently).</p> <p>Lesson: Speak up and know your worth when ppl step over your boundary. Avoid ending up with NO FISH NO PRAWN!</p> <p>The ending: Dad gt the signal. I end up with the fish, nt too bad la. 😁 Prawn high cholesterol ma, give to others la nvm. 😂</p> <p>#抢鱼抢虾记 #虾被抢光 #至少还有鱼</p> </content> </entry> <entry> <title>Fish 004</title> <link href="https://jec.fish/fish/20200711"/> <updated>2020-07-11T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200711</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200711.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200711.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200711.jpg"> <img src="https://jec.fish/assets/img/fish/20200711.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Today im feeling #senior coz my young bro came &amp; help tdy as well. #promoted</p> <p>The fact that I see him kelam-kabut, got critiqued by aunties this round (me none) make me feel im pro-er 😁. Finally my time saying &quot;ai yor, why u so slow one&quot; haha. 😆</p> <p>Obviously, this is nt something a good senior should do! 😂</p> <p>Should say nice thing, be more encouraging, make him motivated so im nt stuck in this role forever! #growingjunior</p> <p>Knowing doesn't mean doing. I really js want to laugh at him and enjoy my proud moment now lol. 🤣 #badsenior</p> <p>#但是问题是我们两个都想辞职啊</p> </content> </entry> <entry> <title>Fish 005</title> <link href="https://jec.fish/fish/20200718"/> <updated>2020-07-18T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200718</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200718.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200718.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200718.jpg"> <img src="https://jec.fish/assets/img/fish/20200718.jpg" alt="Underwater nemo"> </picture> </div><figcaption>Underwater nemo</figcaption></figure> <p>Today my dad turned into family matters #expert, consulting an auntie on her relationship with daughter-in-law (aka typical drama in-law war) #婆媳大战</p> <p>Both mom (the auntie M) and daughter (D) in-law are my dad regular customers, they stay in one home.</p> <p>No wonder so many drama la, also both sometimes a bit calculative on who should pay for the fish. 😆</p> <p>D uses Facebook to talk bad stuff about M. M uses pasar as a platform to gain her justice.</p> <p>Juicy story, too long to fit here. Too bad no popcorn sell in pasar. 😂</p> <p>Surprisingly, dad did nt add fire to the conversation instead advice properly.</p> <p>Then, dad said to me: Fish only sellable if family stick together.</p> <p>Woah, word of the day! What a wise business man, mitigate potential business risk. #visionary</p> <p>What I also think: If D move out (M repeated N times it's her home), splitting to two homes probably will increase fish sales? Happy family cook more lol. But that's risk.</p> <p>What I really think: Probably better to nt stay with in-law. Every LITTLE details got jot down!</p> <p>Anyway, next time I wanna hear story frm D! 😏🍿</p> </content> </entry> <entry> <title>Fish 006</title> <link href="https://jec.fish/fish/20200725"/> <updated>2020-07-25T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200725</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200725.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200725.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200725.jpg"> <img src="https://jec.fish/assets/img/fish/20200725.jpg" alt="Underwater nemo"> </picture> </div><figcaption>Underwater nemo</figcaption></figure> <p>No pasar fish story today. Go underwater survey real fish alive! 😆</p> <p>A family of friendly photogenic nemo. #tioman</p> <p>#本小姐offday</p> </content> </entry> <entry> <title>Fish 007</title> <link href="https://jec.fish/fish/20200801"/> <updated>2020-08-01T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200801</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:99.90234375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200801.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200801.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200801.jpg"> <img src="https://jec.fish/assets/img/fish/20200801.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Lately my dad also sell abalone cans in pasar. So there's this rich aunty A came by (heard later frm other aunties, her house big big one).</p> <p>A: Give me one carton! (24 cans)<br> Me: Wah really! Why u buy so many ah? #倒米王</p> <p>A: Oh, I buy for my grandchildren to eat ma, good nutrients. 😳<br> Me: (Don't knw how to continue the conversation…)</p> <p>Well u knw in life, some kids eat abalone since born, some hv to help selling fish in pasar since childhood. 🤷</p> <p>But that's ok right, while one has abalone, another gain priceless experience! 🙂</p> <p>Always appreciate wht u hv, be mostly positive and gt the most out of it. Blaming is no use, it doesn't turn one dad into rich papa!<br> .<br> .<br> .<br> .<br> .<br> Anyway… can I be your granddaughter too, rich mama? 😘😂</p> <p>#不介意当干女儿哦</p> </content> </entry> <entry> <title>Fish 008</title> <link href="https://jec.fish/fish/20200808"/> <updated>2020-08-08T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200808</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:113.37890625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200808.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200808.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200808.jpg"> <img src="https://jec.fish/assets/img/fish/20200808.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Today i prepared a success story of digital transformation, but turn out it's a failure because didn't met the business expectation.</p> <p>So the current 🐟 price calculation process is manual like this photo shown, it's slow.</p> <p>I bought a digital scale for my dad (actually to ease myself la 😆). I did proper user training, teach him how to use it. Also consult him to test it in production during non-busy day.</p> <p>So far so good, dad feedback the scale make the process faster. So I tot I can proudly goyang kaki tdy, took photo and share the #successstory.</p> <p>Manatau I arrived at the stall this morning and still see this calculator and normal scale. No sign of digital scale.</p> <p>Me: Why ah? I thought u say it's good and faster?<br> Dad: Faster is faster la, but too accurate ady, I lost money! Cannot la!</p> <p>U knw fish selling is a low margin business, every cent count. With manual scale one can round up easier! 😂😂😂 (Note: But our price is competitive, somemore with premium fish quality)</p> <p>Actually, it's a long consideration before I purchase the scale. Part of my heart wants him to use traditional calculation on normal day, use digital one only on busy day (when im thr 😁).</p> <p>The reason is old uncle needs to keep doing brain exercise to stay healthy! Also becoz I like how the old way can simulate more conversation with human, can fight price calculation with aunties!</p> <p>Js that young one like me lazy lo sometimes, brain karat ady. Haiz, finding shortcut but gt banned by old folk, oppss.</p> <p>Happy for him, sad for me. Need to find a way to convince him to let me use it when im helping! 😏</p> <p>#暂时转型失败 #不过也是好的 #老的脑要多运动 #不自动化比较有人情味</p> </content> </entry> <entry> <title>Fish 009</title> <link href="https://jec.fish/fish/20200815"/> <updated>2020-08-15T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200815</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200815.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200815.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200815.jpg"> <img src="https://jec.fish/assets/img/fish/20200815.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Today im demotivated as an employee.</p> <p>Basically, the boss (my dad) doubled up the stock tdy and we sold out.</p> <p>Good right? Of course! Good for the boss lo, more works for me. 🙄</p> <p>See the fish queue. Calculate and pack until I blur. Also no fish to steal home tdy because no time to hide it. 😌</p> <p>A good boss should incentivize their workers accordingly to overload them!</p> <p>I was hoping one day my dad will reward me by saying &quot;nah, i kept a fish for u tdy&quot;. Even js a cheap fish i think i will be very happy. Haiz, never happen. 😆</p> <p>Better jump ship faster if gt boss like this okkkk!</p> <p>#无良老细 #typicalCinaManCompany</p> </content> </entry> <entry> <title>Fish 010</title> <link href="https://jec.fish/fish/20200822"/> <updated>2020-08-22T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200822</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200822.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200822.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200822.jpg"> <img src="https://jec.fish/assets/img/fish/20200822.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>An unknown 70 yrs-old-ish auntie dragged a trolley to our stall and started talking to me. Dad was away.</p> <p>A: Pretty, shopping pasar very tired. Are you going home soon?</p> <p>Me: (Alert, why she asked that. I stay calm, ignore question) U can put ur trolley here for a while, sit and rest.</p> <p>A: This trolley is very heavy. I'm thinking if u r going back soon can u help to send to my house?</p> <p>Me: (Alert, what!? I don't even knw u!? 跟你很熟咩?) Errrr…</p> <p>Luckily, dad came back in time. Auntie repeated the same request.</p> <p>Dad: Ok ah, no problem.<br> Me: 😮</p> <p>Apparently this is nt the first time.</p> <p>Me: Wah, nvr know u r so kind-hearted one.<br> Dad: Aiya, her home is just on the way back. Pasar might be her only social place nw. Helping ppl whatever possible lo. U old ady then u knw. 😲</p> <p>Wow! I guess the older one grows, the more empathy one will be? Coz u might be the next one tht need help.</p> <p>Probably I shouldn't be too alert js nw. This is the kind of service u dun get in hypermarket. #人情味</p> <p>Ok, good model to show me how he wants to be treated when he is older. 😆</p> <p>#言教不如身教 #丢他去老人院吗</p> </content> </entry> <entry> <title>Fish 011</title> <link href="https://jec.fish/fish/20200829"/> <updated>2020-08-29T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200829</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:119.43359375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200829.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200829.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200829.jpg"> <img src="https://jec.fish/assets/img/fish/20200829.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>My dad has a loyal premium customer that came all the way frm Klang bi-weekly.</p> <p>A young mom tht dun eat fish herself... Buy a lot and nt very calculative. Friendly, nt supervising my weighting, just pay whatever I ask. 😍</p> <p>Honestly, I was quite surprised but then I understand why later.</p> <p>Me: Why u come so far to buy fish here?<br> Y: Ur dad fish fresh ma.</p> <p>Me: But Klang pasar got so many fresh fishes...<br> Y: Used to buy frm ur dad ady, good service.</p> <p>Me: (Actually I still wanna ask further one, but better dun la, nt that i wanna convince her to nt come kan. 😂) #倒米王</p> <p>Dad: Hey, I reserve these fishballs, free for you and your family.<br> Y: Wah, so good ah. Tq uncle.<br> Me: (Well, the fact is - it's just so happen we cant finish selling the fishballs 🙄 #明明就是卖不完怕坏掉)</p> <p>Aha! Rephrase your offering to double up the impact and customer satisfaction. 😏 Make them feel valued and happy, they'll always come back!</p> <p>#行走江湖识做人很重要</p> </content> </entry> <entry> <title>Fish 012</title> <link href="https://jec.fish/fish/20200905"/> <updated>2020-09-05T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200905</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200905.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200905.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200905.jpg"> <img src="https://jec.fish/assets/img/fish/20200905.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>No pasar 🐟 story today. My boss off to #kualakurau pasar for market survey lol!</p> <p>#微服出巡 #格格被逼随行</p> </content> </entry> <entry> <title>Fish 013</title> <link href="https://jec.fish/fish/20200912"/> <updated>2020-09-12T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200912</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200912.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200912.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200912.jpg"> <img src="https://jec.fish/assets/img/fish/20200912.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Last weekend was off, dad forecasted business tdy would be great, so he gets more fishes. However, it's RAINY DAY, sh**! 🌧️</p> <p>Manage to almost sold out luckily, after dad pushed the sales harder and make calls to some regular customers (still sleeping 😴 or lazy to come out).</p> <hr> <p>8.30 am, left a Chinese Pomfret #斗底鲳 and a Grouper #石斑, about RM 42 and RM 35 each (mid-upper range fish).</p> <p>Me: Ok la, u take one, i take one, we go home eat la.<br> Dad: Cannot, expensive. Let's wait.<br> Me: 😑 (fine)</p> <hr> <p>9 am, left only the Pomfret.</p> <p>Me: Give me la.<br> Dad: (Think for a few seconds) Ok. Took and chop.</p> <p>Me: 😁<br> Dad: (Split the fish into 2) U take half, I take half. Cheaper.</p> <p>Me: 😑😑😑 Huh!? #酱都可以 #cheapskate!</p> <hr> <p>Story aside, getting wet at rain and worrying about fish sales aren't fun.</p> <p>Always be prepared at normal day (building good relationship with customers) make it easier to navigate business through bad weather. #survivalskill</p> <hr> <p>I really think we should just reward each of us one fish today instead of half lol, celebrate little win ma. Well, clearly boss only think about profits!</p> <p>#不需要一人一半 #一人一条感情也不会散的嘛</p> </content> </entry> <entry> <title>Fish 014</title> <link href="https://jec.fish/fish/20200919"/> <updated>2020-09-19T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200919</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:119.3359375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200919.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200919.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200919.jpg"> <img src="https://jec.fish/assets/img/fish/20200919.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Today suddenly gt scolded by an hot-tempered uncle. He bought RM 11 fish.</p> <p>Man: How much?<br> Me: RM 11.</p> <p>Man: (Give me RM 50)<br> Man: I give u RM 5 also.</p> <p>Me: Ok. (Pass him RM 44 - 2xRM20, 4xRM1)<br> Man: (Shout at me) What r u doing? Why u give me so many RM 1 notes.</p> <p>Me: (Blur) Huh?<br> Man: How much is the fish? (In high pitch, annoying face)</p> <p>Me: RM 11. (Doing math in my head, wondering are thr anyway i can give less RM1)<br> Man: Oh, say la RM 11, then I give u RM 1 instead of RM 5. (Still raising voice)</p> <p>Me: ...</p> <p>Man grabbed the fish and the 💵, show me an angry eyesight, then left the stall.</p> <p>WTH, Im still wondering why he gt so triggered by RM1. Seriously I didn't do anything. 😳</p> <p>Potato also need to be respected one ok. Customer is not king sorry.</p> <p>Luckily my mood isn't bad today else u see la, throw u the fish. 😂</p> </content> </entry> <entry> <title>Fish 015</title> <link href="https://jec.fish/fish/20200926"/> <updated>2020-09-26T00:00:00-00:00</updated> <id>https://jec.fish/fish/20200926</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:64.74609375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20200926.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20200926.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20200926.jpg"> <img src="https://jec.fish/assets/img/fish/20200926.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Manager should help staff to prioritize tasks and share the workloads. Nt just giving instructions.</p> <p>Dad: Pack this fish for auntie A.</p> <p>Dad: Peel these prawn shells for auntie B.</p> <p>Dad: Weight these fishes (a lot) for auntie C.</p> <p>Auntie D: Amoi, come help me pick a few nice prawns.</p> <p>Auntie E: Girl, how much is this fish ah?</p> <p>Auntie F: Ei, my fish processed ady anot, can faster ah.</p> <p>Walao, im a single core, at most duo core processor jer, not thousand-hands guanyin ok!!! #千手观音</p> <p>Ideally, I will handle pipeline first C-E-D, then closing sales F-A, then after sales service B.</p> <p>The problem is, when tasks r all coming at once, all eyes r on you; some said rushing, some are premium customers, the u gg. The queue doesn't work that way.</p> <p>Me: 🙄 (Asking while peeling prawns for the premium and rushing customer). Wait! Boss, which one should I handle first, no hands ady!</p> <p>Successfully diverted attention. All aunties eye on dad now, don't care. 😬</p> <p>Dad stopped cutting fish and come helping #frontend. 😁</p> <p>Frontend is nt easy ok, dun keep passing all the tasks to frontend, and hiding in the backend ok!!!</p> <p>#managingupwards</p> </content> </entry> <entry> <title>Fish 016</title> <link href="https://jec.fish/fish/20201003"/> <updated>2020-10-03T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201003</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:69.23828125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201003.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201003.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201003.jpg"> <img src="https://jec.fish/assets/img/fish/20201003.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Pasar way of calculation is not conventional for me.</p> <p>When aunties asking &quot;how much is the fish?&quot;, they are asking for per kati (600g) price, NOT per kilogram (kg).</p> <p>However, when i weight the fishes, I calculate with kg price. 😌</p> <p>Hard to remember the long list of price with 2 measurement systems, so the first thing i do is always writing them (like the picture).</p> <p>My dad just memorize these numbers in his head. 👏 Sorry la, my brain doesn't work that way, similar to coding, just Google or StackOverflow when needed. 🙈</p> <p>Me: Why pasar ppl dun ask for per kg price?<br> Dad: Because it sounds more expensive? Per kati sounds cheaper.</p> <p>Me: But it's eventually the same price and it's easier for calculate.<br> Dad: People just wanna feel better at the moment of buying. It's like this one, just follow la.</p> <p>Me: (not convinced) <em>Testing saying KG price to a few customers</em></p> <p>Customer ABCDE: 😱😱 Why ur fishes suddenly become so expensive!</p> <p>Dad: (Stare at me in killing mode then explain to aunties) No la, per kati is RM XX la!</p> <p>Me: (Ok, experiment failed. It's proven hard to change the existing system or I hv nt try hard enough! 😆)</p> <p>#倒米王</p> </content> </entry> <entry> <title>Fish 017</title> <link href="https://jec.fish/fish/20201010"/> <updated>2020-10-10T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201010</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201010.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201010.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201010.jpg"> <img src="https://jec.fish/assets/img/fish/20201010.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Sometimes i really beh tahan pasar aunties bargaining skill.</p> <p>Auntie A: How much?<br> Me: 19.2. count u RM 19 la.</p> <p>Auntie B: 24.5 ah, RM 24 enough la~</p> <p>Auntie C: 33.9 ah, RM 33 cannot meh!</p> <p>Auntie D: 47 ah, RM 45 can ady la~</p> <p>Walao eh, 90 cent also want to round down? All math fail is it. Rounding doesn't work like that leh! 😂</p> <p>Small number i will round down automatically, big number don't la, we r js small business ok. 😌</p> <p>That's y my dad said, sometimes it's good to say higher then charge lower.</p> <p>For example, when the price is 11.4, just say &quot;11.7, aiya cincai la, charge u 11.5 enough ady, special discount for u&quot;.</p> <p>Usually aunties wont bargain again, unless those really thick face one, can ignore.</p> <p>Problem reduced, 💰in 👩 happy. Win-win. #streetsmart</p> <p>I dun do it all the time, because i always forget that this is not programming, no need to be so accurate. 😆 (except JavaScript...)</p> <p>#我们绝对不是奸商哦 #情势所逼啊</p> </content> </entry> <entry> <title>Fish 018</title> <link href="https://jec.fish/fish/20201017"/> <updated>2020-10-17T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201017</id> <summary></summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:124.21875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201017.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201017.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201017.jpg"> <img src="https://jec.fish/assets/img/fish/20201017.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Woo, today got premium customer ordered a whole cod fish, about 1.4k, each slice bigger than my palm.</p> <hr> <p>Doing business is about extending conversation.</p> <p>A: Fish fresh anot today?<br> Me: Fresh! We no fresh no sell one. (Abor i said nt fresh meh, extend further, bluffing + demo) Ytd catch one. Nah, u press and see this fish, bla bla bla</p> <p>B: Is this fish tasty? Meat elastic anot?<br> Me: Of course tasty la, everyone eat will sure come back buy one (extend, empty promise), if nt tasty u come back i replace one for u.</p> <p>C: Which fish nicer?<br> Me: All fish r equally nice wor (can stop here or extend suggest), but if u like steam fish, then this is better (extend even further) u can steam with (recipe sharing, bla bla).</p> <h2>Next level</h2> <p>D: (Just passing by the stall, nt stopping)<br> Dad: Ei, ur last Monday fish taste good anot? (knw details, extend upsell) tdy I gt this prawn, i rmb ur son like it, wanna get some?</p> <h2>Learnings</h2> <p>The conversation can be meaningless, telling the obvious or asking them boring questions.</p> <p>Prepare a few &quot;pickup lines&quot;. Just keep talking. The key is avoiding awkward silence, easier to close sales.</p> <p>It's even better if u rmb some details if possible, establish more human touch and connection.</p> <p>This skill is useful everywhere. #吹水讲废话</p> <p>My dad used to call me &quot;clam mouth&quot;, coz my face is serious by default #他生的 and i dun talk further when customer asks for price (my mouth = clam shell = close hard 😂).</p> <p>Many customers walked away without buying if I served last time. Cost him some lost before, haha.</p> <p>But! I'm now his favorite helper lol... compare to my brother. 🤣</p> <p>Well, programmer better, by default can be &quot;clam mouth&quot;, that's the common expectation. 😆</p> </content> </entry> <entry> <title>Fish 019</title> <link href="https://jec.fish/fish/20201024"/> <updated>2020-10-24T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201024</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:58.984375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201024.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201024.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201024.jpg"> <img src="https://jec.fish/assets/img/fish/20201024.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Sold out all before 9. Maybe becoz ppl scare of the current political situation?</p> <p>Today special - 7 stars grouper, hot selling.</p> <hr> <p>There's always market for good stuff, for the right one.</p> <p>So at one point, we left about 20-ish small prawns. Kinda little to sell.</p> <p>Me: Dad, i pack these for myself.</p> <p>Auntie A: (overheard) Eh, u weight n see, I want!<br> Me: 🙄 (fine, don't hv strong craving for prawn)</p> <p>Auntie A: Count me cheaper la, these r leftover. Too expensive I don't want.<br> Me: 🙄 (hello, no one asked u to buy ok)</p> <p>Me: (big smile on face) RM 4.2. (no discount)<br> Auntie A: So expensive one, no discount meh. Dun wan la, u bring home eat.</p> <p>Me: 🙄 ok. (That's the original plan ok)</p> <p>Didn't notice Auntie B on the side.</p> <p>Auntie B: Give me that la, since she doesn't want, I want. Give me 1kg of the big prawns too.</p> <p>Auntie A twisted and looked at Auntie B.</p> <p>Me: (Oops, too late liao, A. Good job, B! Discount 20 cent for u. 😁)</p> <hr> <p>There're time u might feel undervalued at work and life, feel like u need to give in more for something.</p> <p>Examine the situation, maybe u r really lousy 🤭, or maybe not. Nt always necessary to &quot;give discount&quot; or improve anything.</p> <p>Go find / Wait for the right one that values u. Don't eagerly try to fit in nor under sell yourself k. No rush.</p> <p>If u'd like to, can treat this as an love advice lol 😂 (which i think im nt qualified to give 🙈)</p> <p>But really, just anything.</p> </content> </entry> <entry> <title>Fish 020</title> <link href="https://jec.fish/fish/20201031"/> <updated>2020-10-31T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201031</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201031.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201031.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201031.jpg"> <img src="https://jec.fish/assets/img/fish/20201031.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Heck, raining whole morning. Envy dad bald head, no need to cover hair. 😂</p> <hr> <p>Packing cod fish for a rich auntie. She's chit chatting with dad.</p> <p>Auntie: Wah, ur daughter really good ah, every weekend come help u.</p> <p>(Me intercept)</p> <p>Me: Ya, daughter is good, but father isn't. U knw he charged me for the cod fish last last week, and he ate it. (Yes, he said MUST PAY, even he was eating oso 😤)</p> <p>Auntie: Aiyor, did he paid u to come help out?<br> Me: Never.</p> <p>Dad: Wah, she worked ady, time to pay back, IT good earning u knw.<br> Auntie: (Look at me) What u do ah? How much u earn ah?</p> <p>Me: (🙄, kepochi, my dad doesn't even knw my 💰 ok)<br> Me: (divert topic) Eh, auntie, how much your son earn ah? I heard he earn a lot leh, is he single?<br> Auntie: (Proud mom speaking) ...</p> <p>Today learning:<br> Learning the skill to nt answer questions directly, but still keep the conversation going, nt pissing ppl off, is a really important comm skill.</p> <p>Divert and switch focus is a good method. I am getting better at this. Really a lot of kepochi ppl in pasar, asking direct privacy questions. Haha.</p> <p>ps: Maybe her son really single... U nvr knw. The fact is, I don't even knw if she gt son anot, coz im nt kepochi k. 😂</p> <p>#八卦是日常 #可能有意外收獲</p> </content> </entry> <entry> <title>Fish 021</title> <link href="https://jec.fish/fish/20201107"/> <updated>2020-11-07T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201107</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:67.67578125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201107.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201107.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201107.jpg"> <img src="https://jec.fish/assets/img/fish/20201107.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Woo, today sold out at 8.40am. Also first week of accepting e-payment!</p> <p>Business needs to keep up with the trend, else probably bye bye.</p> <hr> <p>Dad, as any old ppl, does nt feel secure of nt accepting cash on hand, and paying extra transaction fees (free for now). He rejected TnG installation once. However, he knw can't fight with the trend, because customers requested.</p> <p>Conducted a training for him, taught him how to verify in the app if customers made payment.</p> <p>Well, production is different frm testing.</p> <p>Honestly, the app payment notifications kinda suck, need to check in inbox (few clicks away). Imagine your hands are full with dirty water and fish scales, how to check... #UXfail</p> <p>So what we did is verify by trust. Ppl pay and show us receipt on their phone.</p> <p>So technically, if u just show a fake screenshot, u can get fish for free! But, we hv faith in human race and pasar aunties! (Let's see if anything happens) 😆</p> <p>During the training, dad keeps asking me where's the money goes, coz it doesn't show up in the e-wallet. Honestly, the TnG sales didn't do a good job in training non-tech savvy user lol. 😌</p> <hr> <p>U knw what my dad did to me now? Usually I took fish and pay later, with the hope that he forgot to collect 💵 frm me.</p> <p>Today he said to me: TnG me the payment immediately NOW! 🤬🙄</p> <p>Probably he should nt keep up with the trend, also he didn't pay me training / technical support fee. 😒</p> </content> </entry> <entry> <title>Fish 022</title> <link href="https://jec.fish/fish/20201114"/> <updated>2020-11-14T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201114</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:70.01953125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201114.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201114.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201114.jpg"> <img src="https://jec.fish/assets/img/fish/20201114.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Well, I thought I'm my dad biggest customer tdy (helping friends buy). I were proud 😎 to be the top sales for a moment.</p> <p>Then my dad pulled out this cod fish. Someone ordered this whole. Dad 1 - Daughter 0.😂</p> <hr> <p>Company culture strongly influence how its employees behave, whether u aware or not, same to family culture.</p> <p>An auntie bought a golden pomfret tdy.</p> <p>Auntie: Golden pomfret is a forgiving fish. The meat still taste smooth even oversteam. (Some fish meat become horrible if overcook)</p> <p>Dad: Ya la, the fish meat still way smoother than your face even oversteam! Haha.</p> <p>Me: Walao eh, no manner, dad u so mean! Auntie, don't buy frm this kind of ppl, go buy next stall better.</p> <p>Auntie: Haha, my face always smooth, u see how many ppl has good skin like me at this age.</p> <p>Me: (Actually... well... Ok) Yayaya, auntie looks young always. 😆</p> <p>(This auntie is the can-joke kind and is regular, so it's okay i guess!)</p> <p>I hv friendsss told me im mean sometimes when talking. Now u knw whr I learned frm.</p> <p>It's nt something I aware coz that's how my dad talked to us since young, the culture.</p> <p>Over the years, I improved. The meanness still in my DNA, but I learned to better control it and who to mean to.</p> <p>Now I'll mostly mean to the ppl I'm closer with. So... if I'm mean, lucky u! 😂</p> <p>Or... it might be I really dislike u or unconscious act, true self hard to change I guess? 🤭</p> <p>#鱼肉就算蒸过熟粗粗还是比你的脸滑 #这个人真的够力够胆讲 #我也是受害者 #monkeyseemonkeydo</p> </content> </entry> <entry> <title>Chrome DevTools的最新功能 + Puppeteer的最新进展</title> <link href="https://jec.fish/deck/gdd-2020"/> <updated>2020-11-18T00:00:00-00:00</updated> <id>https://jec.fish/deck/gdd-2020</id> <summary>Google Dev Summit 2020 - Chrome DevTools的最新功能 + Puppeteer的最新进展</summary> <category term="deck"/> <content type="html"><p>在线上与大家分享了Chrome DevTools的最新功能以及Puppeteer的最新进展。</p> <p><a href="https://developersummit.googlecnapps.cn/">Google 开发者大会</a> (Google Developer Summit) 是谷歌面向开发者展示最新产品和平台的年度盛会。这是谷歌首次以全线上大会的形式与中国开发者相聚。</p> <h2>Chrome 开发中工具 DevTools 的最新功能</h2> <p>本场演说为大家介绍七个Chrome 开发者工具 (DevTools) 的新功能。</p> <div class="video-wrapper"> <iframe frameborder="0" src="https://v.qq.com/txp/iframe/player.html?vid=a3203fqiili" allowFullScreen="true"></iframe> </div> <h2>Puppeteer的最新进展</h2> <p>Puppeteer 是一个 Nodejs 工具库,它提供了高级的 JavaScript API 来控制 Chromium 与其他的浏览器。本次演说将为大家简单介绍 Puppeteer 的基本操作以及最新功能。</p> <div class="video-wrapper"> <iframe frameborder="0" src="https://v.qq.com/txp/iframe/player.html?vid=o3203vn66r2" allowFullScreen="true"></iframe> </div> . <h2>场外制作</h2> <p>It was fun making the videos, although many NGs (the subject stuttered a lot. 😂). Thanks to my friends - Lydia and See Yin who help to set up the gear, proof read my Chinese script and did the recording.</p> <p>Wouldn't have made these videos without you girls. ❤️</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/deck/gdd-2020.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/deck/gdd-2020.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/deck/gdd-2020.jpg"> <img src="https://jec.fish/assets/img/deck/gdd-2020.jpg" alt="The making of the videos"> </picture> </div><figcaption>The making of the videos</figcaption></figure> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1329007769236606976"> twitter.com/jecfish/status/1329007769236606976 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Fish 023</title> <link href="https://jec.fish/fish/20201121"/> <updated>2020-11-21T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201121</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:65.72265625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201121.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201121.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201121.jpg"> <img src="https://jec.fish/assets/img/fish/20201121.jpg" alt="Uncle is holding a big fish"> </picture> </div><figcaption>Uncle is holding a big fish</figcaption></figure> <p>No, the fish is belong to a Datin, uncle didn't buy anything. 😂</p> <p>He passed by our stall and say this fish is beautiful, want to take picture with it. Dad approved, asked me to help him.</p> <p>Walao, uncle got high photo requirements eh! Need to face sunlight la, try different poses and check end result.</p> <p>After that, he told me: Your photoshooting skill nt bad ah, I can add u as fb friend. Many ppl dun knw how to take good photo.</p> <p>Woah uncle! Should I feel honored that I qualified to be your fb friend? 😂</p> <p>This could probably be a good pick up line if he is not my dad age. Guys, can learn that!</p> <p>Aiya, forgot to ask uncle if he got son lol.</p> <p>This fish is called Threadfin / Senangi / 马友.</p> <p>No business learning this week. Super busy selling, tired. Keep hustling all!</p> <p>*No, I didn't add him in fb.</p> </content> </entry> <entry> <title>Fish 024</title> <link href="https://jec.fish/fish/20201128"/> <updated>2020-11-28T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201128</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:71.77734375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201128.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201128.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201128.jpg"> <img src="https://jec.fish/assets/img/fish/20201128.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Woohoo! Today got crab. Slightly late tdy coz gt a tech talk immediately after pasar.</p> <p>Me and my dad rarely call or text each other, except every Friday - he will text me a reminder: Remember to come earlier tomorrow.</p> <p>Note that he always use the word &quot;earlier&quot;.</p> <p>It irked me and make me wonder: Am I not coming early enough every week, so u ask me to come earlier (than last week)? Could you rephrase your reminder?</p> <p>But then I realized I don't care about the answer so much. 😆 Coz I won't go earlier anyway even if requested. Reaching at 6.30am is the best I can do ady.</p> <p>And to debate about that sentence with my dad seems like a dumb thing to do. There's a lot of things in life or at work u don't need to clarify, rectify or react.</p> <p>Don't take ppl word so seriously. Close one eye and just let it go. Don't be so detailed and bitchy k.</p> <p>Choose your battle wisely.</p> <p>Well, write is easier than done. 😂</p> </content> </entry> <entry> <title>Fish 025</title> <link href="https://jec.fish/fish/20201205"/> <updated>2020-12-05T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201205</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:58.49609375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201205.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201205.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201205.jpg"> <img src="https://jec.fish/assets/img/fish/20201205.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Polices come pasar spot check today.</p> <p>Aunties: Aiyer... faster ciao, later kena fine 1k.<br> Stall sellers: Haiz, later they take photos, want to rasuah is it?</p> <p>Me: Uncle, your fish RM 10. No worries la, just follow the rules, police won't simply fine u.</p> <p>Uncle: Confused by the rules now. Don't exactly knw what to follow, later fish jump from RM 10 to RM 1010, no money pay la. 😂</p> <hr> <p>Everyone worry about the money, not the virus.</p> <p>Is it a problem of our system or citizen mindset? Confident to the enforcement system is so low.</p> <p>Anyway, in a company, if unlawful thing happens, blame the system, not the employees.</p> <p>While monetary reward/punishment could solve problems temporarily, it might not be the most effective one.</p> <p>Wah, serious post today ah.</p> <p>ps: I woke up late tdy because my dad didn't call me ytd to tell me &quot;come earlier tmr&quot; (refer to last week post). He scolded me, I said it's his fault. 😂</p> </content> </entry> <entry> <title>What's G.R.E.A.T in Chrome DevTools</title> <link href="https://jec.fish/deck/cds-2020"/> <updated>2020-12-10T00:00:00-00:00</updated> <id>https://jec.fish/deck/cds-2020</id> <summary>Chrome Dev Summit 2020 - Great new features of Chrome DevTools</summary> <category term="deck"/> <content type="html"><p>Combined my 2 favourites together - cooking and Chrome DevTools.</p> <p><a href="https://goo.gle/cds2020">Chrome Dev Summit</a> is an annual conference where developers can learn about the latest tools and updates coming to the Google Chrome browser.</p> <p>My session gives you an overview of the latest and greatest features in Chrome DevTools.</p> <p>Here are all the demos: <a href="https://jec.fish/demo/cds-2020">/demo/cds-2020</a>.</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/QsOF9SJJdAA" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/QsOF9SJJdAA?autoplay=1><img src=https://img.youtube.com/vi/QsOF9SJJdAA/maxresdefault.jpg alt='What's new in DevTools'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="What's new in DevTools"> </iframe> </div> <h1>Behind the scene</h1> <p>I set the fried rice theme and the G.R.E.A.T acronym, but I have no idea on how to deliver it. My creative friend, Aaron helped me brainstorm the idea together. Thanks Aaron! 🙇🏽♀️</p> <p>Here is the prototype, haha.</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/Q2F5LIDvZTA" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/Q2F5LIDvZTA?autoplay=1><img src=https://img.youtube.com/vi/Q2F5LIDvZTA/maxresdefault.jpg alt='Prototype'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Prototype"> </iframe> </div> <p>.</p> <p>I have a sleepless week (or more) thinking about the making of this video. The timeline is tight. Nevertheless, it was fun! There goes my last talk in year 2020.</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1337429741352075265"> twitter.com/jecfish/status/1337429741352075265 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div></content> </entry> <entry> <title>Fish 026</title> <link href="https://jec.fish/fish/20201212"/> <updated>2020-12-12T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201212</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:60.25390625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201212.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201212.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201212.jpg"> <img src="https://jec.fish/assets/img/fish/20201212.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Sea cucumber #海参 selling season starts, winter solstice and Chinese New Year are coming!</p> <hr> <p>Many pasar sellers like to gamble. Morning cash in, afternoon go Genting.</p> <p>It becomes their daily life, hobby and social activities.</p> <p>Stall A: Walao, Genting now has many COVID cases. Scary leh.</p> <p>Stall B: I miss going up, hand itchy (want to bet)!</p> <p>Dad: I think Genting road gt many potholes now, coz all &quot;donors&quot; cannot go. 😂</p> <p>Me: Stall C, your fish RM 50.</p> <p>Stall C: So expensive!</p> <p>Stall D: Stall C, how much do u want to buy Toto later, XXXX?</p> <p>Stall C: RM 100 big, RM 100 small.</p> <p>Me: 🙄 (Fish is expensive, but Toto is not)</p> <p>Some stalls hv good business, but still in great debts. Some cannot afford kids for better education, some cannot afford operation freeze like MCO, some old sellers cannot retire. There are many more worse examples.</p> <p>Why? Because they hv no saving, no proper financial planning. Money in, money goes.</p> <p>If you are youngsters reading this, this isn't apply js to gambling - gaming spending, impulsive online shopping, etc as well.</p> <p>It's okay to hv hobby, but pls be a responsible adult. Properly plan your spending, saving and investment.</p> <p>Or get married and hv kids early, hope they will be grateful and support u financially when u are old lol.</p> <p>Or... buy saving/investment plan, force yourself to do it. But I am nt an agent and this is nt an ads. Agents can leave your contact in comment. 😂</p> <p>#今宵有酒今宵醉 #风吹鸡蛋壳 #财散人安乐</p> </content> </entry> <entry> <title>Fish 027</title> <link href="https://jec.fish/fish/20201219"/> <updated>2020-12-26T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201219</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201219.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201219.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201219.jpg"> <img src="https://jec.fish/assets/img/fish/20201219.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Let's talk about personal branding.</p> <p>Dad: As long as you understand your customers, with good reputation and decent selling skill, u can sell anything.</p> <p>My dad is a brand of himself. People buy stuff frm him because they trust him (the quality of his pick).</p> <hr> <p>My best friend started a new family business recently - the homemade pork belly braised yam in the photo. #芋头扣肉</p> <p>I asked my dad to help selling that. Unknown and not-too-cheap product like this really put his skill in test - #blowwater and #reputation.</p> <p>A: (Take the pack, talk to dad) Wah, u made this ah?<br> Me: No la, homemade by my friend.</p> <p>A: Oh. (put it back)<br> Dad: Not I made, but I ate the other day, quite tasty oh, can buy n try.</p> <p>A: But expensive leh<br> Dad: If too cheap you don't buy lo, u tot quality meat cheap nowadays meh? This one her friend uses grade A chinese wine somemore leh.</p> <p>Me: Yaya.<br> A: Ok la, give me a pack to test.</p> <p>Well, I estimated slow selling, so I only brought 5 boxes to the pasar for trial. But wow, end up my dad sold 25 packs in a day. 😳</p> <p>Even when the 5 physical packs are gone, he still keep selling with his talking skill and it works, customers pre-order that.</p> <p>Well... that was unexpected. I'm impressed. No wonder my mom married him last time. 😂</p> <p>(This story happened last week, this week Im off)</p> <p>#自媒体 #个人品牌</p> </content> </entry> <entry> <title>Fish 028</title> <link href="https://jec.fish/fish/20201226"/> <updated>2020-12-26T00:00:00-00:00</updated> <id>https://jec.fish/fish/20201226</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:68.5546875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20201226.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20201226.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20201226.jpg"> <img src="https://jec.fish/assets/img/fish/20201226.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>GG, today business nt good. All eating turkey for Christmas is it? See my boss promote so hard today. 😂</p> <hr> <p>To senior, on-the-job training is about you (trainer) or the newbie? Today dad and bro argue at the stall. 🙈</p> <p>My bro is jobless recently, dad requested him to help at stall.</p> <p>I reached at pasar slightly late tdy, stall was busy, and bro is thr helping ady.</p> <hr> <p>Dad: (talk to me) U faster come, weight this and calculate that.</p> <p>Me: (I saw bro is doing that already)</p> <p>Dad: (talk to bro) No need u already, damn slow like 🐢. Faster go to the side, look after the other products.</p> <p>Me: (brutal, but dad is always like that when he's busy)</p> <p>Bro: (angry) Walao, u ask me to help, then u scold me like that.</p> <p>Dad: (annoyed) Wah, is this consider scolding? U so slow, just stand on the side, learn a bit la.</p> <p>Bro: I was looking after other good js nw, u asked me to come weight the fish! Blame me again and I will never help you.</p> <p>Dad: (annoyed) Don't want to come then don't come la. Youngster nowadays so not teach-able.</p> <p>(Customers watching, eating popcorn 🍿)</p> <p>Me: Wah, u both so free fighting is it? (Stand btw them, start weighting the fish)</p> <p>All get back to work silently.</p> <hr> <p>When bro is away, I talked to dad.</p> <p>Me: So do u want him to help or not?<br> Dad: Yes, but he is so slow.</p> <p>Me: If you need help, can u talk politely?<br> Dad: You see, he came a few days already, still blur blur. He doesn't learn with heart.</p> <p>Me: Boss, u teased him like that, then u expect him to learn passionately? Dream la.<br> Dad: (Silent for a while)</p> <p>Me: U should appreciate he doesn't js walk away js nw. Adjust your way talking to him lo.<br> Dad: Erm... Aiya, u don't understand one la...</p> <p>I think he listened, but whether he will improve anot we shall see.</p> <hr> <p>Junior might be slow in learning and making newbie mistakes.</p> <p>Try your best to assist, teach the way they absorb. Throwing cruel words might discourage them.</p> <p>After all, u want the junior to perform and split the workload, not bringing u more trouble. Give chance a bit la.</p> <hr> <p>I kesian my bro today, although he is really blur blur but i still guide him. My patient level is much better than dad, haha. Coz i want to escape my pasar duty. 😏</p> </content> </entry> <entry> <title>Fish 029</title> <link href="https://jec.fish/fish/20210102"/> <updated>2021-01-02T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210102</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:66.89453125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210102.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210102.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210102.jpg"> <img src="https://jec.fish/assets/img/fish/20210102.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>2021, a story about customer feedback and continuous business improvement. In the photo, there're packs of raw ikan bilis and fried ones.</p> <p>Few weeks ago, dad started to pack and sell raw ikan bilis.</p> <h2>1st week</h2> <p>A: Ei, the ikan bilis nice anot<br> Dad: Nice la, u buy one home and fry.</p> <p>A: How to fry oh, teach me la.<br> Dad: Blah blah blah...</p> <p>A: Talk so much, u fried some for us to test la.<br> Dad: Ok</p> <p>Anyway, the ikan bilis finished selling.</p> <h2>2nd week</h2> <p>Dad: Come buy the ikan bilis, come try some sample!<br> B: Oh, this sample is nice. U fried one?</p> <p>Dad: Ya, very easy only, here is how: blah blah blah...<br> B: I lazy la, how about u just fry and sell it?</p> <p>B didn't buy ikan bilis, but ikan bilis finished selling anyway.</p> <h2>3rd week</h2> <p>Dad: Wanna buy fried ikan bilis anot?<br> C: Oh, u fried one?</p> <p>Dad: Yaya, homemade la. Come test and see.<br> C: Oh, not bad oh. Give me 2 jars.</p> <p>Dad: Wanna buy some raw one anot? Very easy jek, i teach u.<br> C: Ok la, give me a raw pack to try.</p> <p>This week we sell more ikan bilis - both raw and fried. B bought the fried one too.</p> <hr> <p>What happen if dad dun listen to those feedback? His business would be still fine, but no extra gain as a business.</p> <p>These feedback are vague. It happens informally during conversation. It's easily ignored if you don't pay attention to details.</p> <p>Dad is nt a detailed person, but when come to business somehow he is more sensitive.</p> <p>Value customer input will earn u more money. Wish everyone has a huat year ahead! 💵💵💵</p> </content> </entry> <entry> <title>Fish 030</title> <link href="https://jec.fish/fish/20210109"/> <updated>2021-01-09T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210109</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210109.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210109.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210109.jpg"> <img src="https://jec.fish/assets/img/fish/20210109.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Today I wish I am octopus, hv many hands. Ppl panic buying like no tomorrow as rumors said might mco soon. Crazy ppl.</p> <hr> <p>I like to observe ppl life through their purchasing behavior.</p> <p>There's one Malay auntie we called her &quot;Miss Malaysia&quot;.</p> <p>She will buy ~3 pieces of salmon everytime her son came home.</p> <p>Miss Malaysia sounds extra happy everytime when buy salmon. She told us her son like to barbeque salmon for the family.</p> <p>It has been about a month she didn't buy salmon. Today she bought crabs, only 2 small one.</p> <p>Guess her son is nt coming back yet? Too busy today. Didn't get time to chit chat.</p> <p>Hopefully they don't change their salmon favorite to barbeque chicken else lose business. 😂</p> <p>Faster go home, son! Our salmon sales depends on u.</p> </content> </entry> <entry> <title>Fish 031</title> <link href="https://jec.fish/fish/20210116"/> <updated>2021-01-16T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210116</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:65.625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210116.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210116.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210116.jpg"> <img src="https://jec.fish/assets/img/fish/20210116.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>This is what's left at 7.52am. Ppl keep storming in after 8 but no more fish.</p> <p>Heard frm aunties, all the surrounding pasars either closed down due to COVID cases or gt rumors about tht.</p> <p>We r the last pasar standing at the moment, but it's js a matter of time. *Touch wood*</p> <hr> <p>Me and dad conversation ytd.</p> <p>Me: Life is more important or money?<br> Dad: As long as I got life, money is important. Don't ask stupid question.<br> Me: (😂 Ok, u win)</p> <p>Dad: Help me write a letter to apply operation permit frm MITI and movement approval from police station.</p> <p>Me: Need meh? Ask ur son to help.<br> Dad: Ur bro English nt good.</p> <p>Me: Ur daughter Malay nt good.<br> Dad: Shut up. Just do what ur dad said.</p> <p>Me: I don't want to go pasar tmr. My life is precious.<br> Dad: If me and ur bro COVID positive, u think u can escape?<br> Me: (🙄 ... u win)</p> <p>Dad: Now is the best time to earn money, before the rules change again.</p> <p>Dad: We piled up stock for Chinese New Year. Hv to sell those off, else no happy new year. gg.<br> Me: If we all tested positive, then it's really happy new year. All gg.</p> <p>Dad: Shut up.<br> Me: 🤐</p> <p>So here I am, in pasar today. I nvr agree but I think what he said make sense, we r on the same boat. Wish my family luck.</p> <p>Hope you learn some persuasive skills frm this post lol. 😂 To get what u want, u dun need the other parties to agree. Js need to make sure they do what u want.</p> </content> </entry> <entry> <title>Fish 032</title> <link href="https://jec.fish/fish/20210123"/> <updated>2021-01-23T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210123</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:59.375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210123.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210123.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210123.jpg"> <img src="https://jec.fish/assets/img/fish/20210123.jpg" alt="COVID-19 test result"> </picture> </div><figcaption>COVID-19 test result</figcaption></figure> <p>Many msg me about the covid cluster at the fish wholesale market.</p> <p>Thanks for concerns. Yea, my dad went thr. He tested negative. Biz as usual tdy.</p> <p>Dad even frames and shows the result at stall. Good marketing, but tht test was few days ago, so who knows... 🙈 <em>touchwood</em> 🙊</p> <p>I think, if ppl scared enough, they won't even walk closer to see the tiny &quot;negative&quot; word on the result.</p> <p>Fish sold out rather early tdy.</p> <hr> <p>I was angry at my dad few days ago.</p> <p>Dad: (Send me a photo of his friend's test result) Can u help me to create a test result like that.</p> <p>Me: What do u mean by &quot;create&quot;?<br> Dad: U make a fake test result paper for me.</p> <p>Me: WTF r u talking about? 🤬<br> Dad: I need the test result to enter the wholesale market.</p> <p>Me: Then u go do it. U r high risk anyway.<br> Dad: A lot of people queue for the RM70+ test leh, I scared I will be infected at the clinic.</p> <p>Me: Then u go do the test at hospital, no waiting.<br> Dad: More expensive. U pay for me is it?</p> <p>Me: Hello, it's your biz. In fact I asked u to off for the week for safety purpose.<br> Dad: It's js a piece of paper, police won't really verify properly one. So u js help me to fake one. I knw u can.</p> <p>Me: I can doesn't mean I should. I can also go report u now u know?</p> <hr> <p>So yeah, that test result is REAL, not photoshop. The QR code to the verification website is real as well, I DID NOT FAKE it for him.</p> <p>We IT ppl can fake these shit, but let's be ethical.</p> <p>Be a responsible person, dun try to be smart coz that's stupid.</p> <p>Educate / Scold not js the youngster, but old stubborn ppl as well, even they r ur parents.</p> </content> </entry> <entry> <title>Fish 033</title> <link href="https://jec.fish/fish/20210130"/> <updated>2021-01-30T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210130</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:72.265625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210130.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210130.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210130.jpg"> <img src="https://jec.fish/assets/img/fish/20210130.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Photo shown premium prawn peeling service by me for a friend. Price x 125%. 😆</p> <p>Serve an annoying mumbling auntie today.</p> <p>A: (Pass me a basket of 2 mackerels) Help me reserve these.<br> Me: Ok. (Kept the basket. Busy packing others fishes)</p> <p>5 seconds later</p> <p>A: Help me keep them in the reserve box below.<br> Me: (Still busy) Yes I will do later. It's safe here. I rmb you. No worries.</p> <p>ConversationAbove.loop(3 times, every 5 sec) #annoying 🙄😡😒</p> <p>A: Do it now. Later other ppl steal my fish.<br> Me: 🙄 (No one will steal but ok. Pause operation, kept her 2 mackerels in a bag n put in reserve box)</p> <p>A few moments later...</p> <p>Dad: (Added a few more fishes in A's bag in the reserve box, WITHOUT me noticing)</p> <p>1 hour later</p> <p>A and B come at the similar time, asked for their fishes.</p> <p>B: Where is my fish?<br> Dad: Here. Take out that bag.</p> <p>Me: (🔍 in box) Eh, where is my bag of 2 mackerels?<br> Dad: Don't know.</p> <p>A: See I told you, now my fishes missing. U lah!<br> Me: (Look at B's bag, look at dad) U steal my mackerels!?<br> Dad: Oh... that's yours ah...</p> <p>B: Nevermind, can give her that. (B is a premium &amp; easy-going customer)<br> A: I don't want! Doesn't look like mine. The 2 mackerels I picked js nw so chubby, these r too slim, eyes look small, bla bla bla. (Minus 3000 mumbling of her I-told-you-so 🙄)</p> <p>Dad: It's yours la.<br> Me: (Losing patience) So u want anot?<br> A: Don't want. (Bla bla bla again.)</p> <p>B: Help me to pack it then.<br> Me: Okay. (No fish for u, bye A)</p> <p>A: (Keep mumbling. My god, can u stop.)<br> Dad: Enough. (Offer her a RM6 fishcake in discounted price RM5)</p> <hr> <p>So whose fault is it? Auntie, dad or me.</p> <p>It could be dad who steal, or me coz im nt labeling the fish bag with her name. (Most of the time we dont, js by memory as quantity r small)</p> <p>Or it could be tht auntie. If she didn't insist to put in tht reserve box, the fish could be still safe under my watch. 😆</p> <p>Blame the system n boss lah. There should be protocol to prevent internal conflicts like this. 🙈</p> <hr> <p>Anyway im happy that annoying auntie doesn't get any fish. Cannot imagine if I'm her children, keep mumbling by her. #behtahan 🙉</p> </content> </entry> <entry> <title>Fish 034</title> <link href="https://jec.fish/fish/20210206"/> <updated>2021-02-06T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210206</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210206.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210206.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210206.jpg"> <img src="https://jec.fish/assets/img/fish/20210206.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Last weekend before CNY. #最后冲刺 So many fishes, more than what u see in the pic.</p> <p>Fish and prawn prices hike as well, but sold out anyway. Crazy ppl.</p> <hr> <p>Let's talk about selling fish to some friends u knw, which I try to avoid most of the time, if i dun knw them well.</p> <p>Bro gt a friend's mom bought quite some fish last week, buy again this week.</p> <p>A: Wah, why so expensive this week?<br> Bro: Because CNY is coming, price hike.</p> <p>A: I want last week market price.<br> Bro: There is no such thing as last week market price, market price is market price.</p> <p>A: Wah, I buy a lot last week leh. U and my son so close still want to earn my money like that meh.<br> Bro: 🙄 The supply price goes up, auntie. We earn a little bit only. Already calculate u cheaper.</p> <p>A: We all r Chinese leh, u still want to chop us like that during CNY. COVID leh, living is hard.<br> Bro: Ya, we r hard oso ah. Risking life to sell u fish.</p> <p>A: ...</p> <hr> <p>Anyway, js like doing any sales, friends might nt be the easiest crowd to please.</p> <p>Some think &quot;since i knw u, u shud nt earn frm me&quot;. Hello, so who pay my bills then? Too bad my family cannot survive by eating sand or grass. 😂</p> <hr> <p>Oh, one side story today. Salmon price hike from RM 70 to 80 per kg. I... forgot about that, still calculate as 70, oops. 🙈</p> <p>Making losses for that. Dad scolded, but what's done is done, free labor (me) ain't gonna cover the losses. 😬 #果然是倒米王</p> </content> </entry> <entry> <title>Fish 035</title> <link href="https://jec.fish/fish/20210213"/> <updated>2021-02-13T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210213</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:70.8984375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210213.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210213.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210213.jpg"> <img src="https://jec.fish/assets/img/fish/20210213.jpg" alt="steam fish"> </picture> </div><figcaption>steam fish</figcaption></figure> <p>Obviously no pasar tdy but gt story.</p> <p>As I observed, most reunion dinners hv fish on table. Tq for the businesses. ☺️</p> <p>That's the tradition symbolizes &quot;every year gt surplus&quot;, js how money-minded Chinese ppl is. 😂 #年年有余🐟 #扎年</p> <hr> <p>Dad &amp; bro fight few days ago before CNY. Tension has been quite some time since they pasar together.</p> <p>Sigh, I received multiple complaint calls frm them per day. 🙉</p> <p>**<br> [Calls]</p> <p>Bro called: Can't stand your dad anymore, it's spring cleaning, he wanna keep all those useless shit. Bla bla bla~<br> Me: Oh.</p> <p>Dad called: Ur bro wanna throw away everything. Useful stuff also wanna throw. Why? Bla bla bla~<br> Me: Don't know.</p> <p>Bro called: I come stay with u. I don't wan to stay with him anymore. I come now.<br> Me: Har...</p> <p>Dad called: Ask ur bro no need to come home anymore. I can stay myself. Ask him forever don't need to come back.<br> Me: Har...</p> <p>**<br> Dad called: U come help me sell fish tmr. Don't need ur useless bro anymore.<br> Me: Cannot. I am busy working.</p> <p>Dad: Shut up. U r my daughter, come help me, it's an order.<br> Me: He is also your son. U go settle with him. Why u two so funny one...</p> <p>**<br> Afternoon. Bro showed up at my place.</p> <p>Me: It's almost CNY. What happen to u both oh...<br> Bro: Ur dad really xyzzzzz bla bla bla~</p> <p>Me: So reunion dinner how?<br> Bro: Don't care. Ask the old man eat himself.</p> <p>Me: Har...<br> Bro: I wanna stay with u till end of month.</p> <p>Me: Cannot. Max can stay 1 day only.<br> Bro: Walao, u only hv one bro. Why u treat me like that!</p> <p>Me: U only hv one dad oso. U hv to go pasar tmr. Why both of u so childish!</p> <p>**<br> Night. After bro talked a lot and ate some ice cream, fruits, CNY biscuits, etc (ate a lot) at my place, he became happier.</p> <p>Bro: I can go home now.<br> Me: Oh, not going to overnight ya?</p> <p>Bro: Nvm la, I forgive him.<br> Me: Thanks God. Everytime u come I need to restock my food. Pls go away.</p> <p>**<br> Family, is family. No overnight hatred we said. Support and forgive.</p> <p>I am nt a good conflict resolver but I think learning nt to throw harsh words during argument is essential (They r bad examples).</p> <hr> <p>Anyway, reunion dinner did happened, in the messy home with lots of useless shit. 😂</p> <p>That's the fish I cooked for our reunion dinner. Wish I got surplus 🐟 frm both of them on mahjong 🀄 table.</p> </content> </entry> <entry> <title>Fish 036</title> <link href="https://jec.fish/fish/20210220"/> <updated>2021-02-20T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210220</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:65.234375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210220.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210220.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210220.jpg"> <img src="https://jec.fish/assets/img/fish/20210220.jpg" alt="abalone"> </picture> </div><figcaption>abalone</figcaption></figure> <p>Pasar still off tdy. That's the sea cucumber and fish maws my dad sold. Yum yum.</p> <p>There're abalone in it too. But that's a low quality one. Why?</p> <p>Dad: Wah, ppl come back and find the abalone we sold.<br> Me: Oh?</p> <p>Dad: Ppl said it's good. Gt an auntie even came back and buy 6 more.<br> Me: Doesn't matter to us. U didn't even left one for us. #再好吃都不关我们事</p> <p>Dad: This one we r eating r given by supplier one.<br> Me: No wonder. U cheapskate so ppl give u cheap abalone oso la. 😂</p> <p>Dad: Cheapskate what. Got abalone is good enough ady. Still want to talk 3 talk 4 tokok. Children at Africa gt nothing to eat u knw.<br> Me: (Since when u care about Africa, u don't even care about us.) 🙄</p> <p>I hope parents these days don't keep talking about Africa children like that. It's nt exactly like that lol.</p> <p>Anyway, Gong Xi Fatt Chai all!</p> </content> </entry> <entry> <title>Crosshair Highlight in Google Sheets with Apps Script</title> <link href="https://jec.fish/blog/highlight-apps-script"/> <updated>2021-02-24T00:00:00-00:00</updated> <id>https://jec.fish/blog/highlight-apps-script</id> <summary>Crosshair Highlight in Google Sheets with Google Apps Script</summary> <category term="blog"/> <content type="html"><p>Let's create a new Highlight menu in Google Sheets with Google Apps Script to perform row, column and crosshair highlight.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.4453125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/highlight-apps-script-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/highlight-apps-script-01.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/highlight-apps-script-01.jpg"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-01.jpg" alt="The Highlight menu and crosshair highlight"> </picture> </div><figcaption>The Highlight menu and crosshair highlight</figcaption></figure> <p>If you don't care about the steps to do it, feel free to scroll to the end and copy paste the code! 😆</p> <h2>Background</h2> <p><a href="https://www.google.com/sheets/about/">Google Sheets</a> has <a href="https://stackoverflow.com/a/25815667">keyboard shortcuts</a> to perform row and column highlight but not both:</p> <ul> <li><code>CTRL</code> + <code>SPACE</code> highlights the column</li> <li><code>SHIFT</code> + <code>SPACE</code> highlights the row</li> </ul> <p>However, I've configured my MacBook <code>CTRL</code> + <code>SPACE</code> shortcut to something else and it overridden the Sheets shortcut. 🤷🏽♀️ It's annoying and Sheets doesn't support crosshair highlight yet which is extremely useful for viewing big data sheet.</p> <p>So... let's roll our own function with <a href="https://developers.google.com/apps-script">Google Apps Script</a> to do so.</p> <h2>Creating the Highlight menu</h2> <p>In a spreadsheet, Open <strong>Tools</strong> &gt; <strong>Script editor</strong>.</p> <p>Create a new <strong>File</strong>, name it <strong>Code.gs</strong> or whatever name you like.</p> <p>We will be creating a new <strong>Highlight</strong> menu with 3 sub menus:</p> <ul> <li>Highlight row</li> <li>Highlight column</li> <li>Highlight crosshair</li> </ul> <p>Here is the code:</p> <pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">createHighlightMenu</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> SpreadsheetApp<br> <span class="token punctuation">.</span><span class="token function">getActiveSpreadsheet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">addMenu</span><span class="token punctuation">(</span><span class="token string">'Highlight'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><br> <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'Highlight row'</span><span class="token punctuation">,</span> functionName<span class="token operator">:</span> <span class="token string">'highlightRow'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'Highlight column'</span><span class="token punctuation">,</span> functionName<span class="token operator">:</span> <span class="token string">'highlightColumn'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'Highlight crosshair'</span><span class="token punctuation">,</span> functionName<span class="token operator">:</span> <span class="token string">'highlightCrosshair'</span> <span class="token punctuation">}</span><br> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightRow</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// code later</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightColumn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// code later</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightCrosshair</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// code later</span><br><span class="token punctuation">}</span></code></pre> <p>In the <code>createHighlightMenu</code> function, the <code>name</code> under the <a href="https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#addMenu(String,Object)"><code>.addMenu</code></a> function refers to the menu display name on the Sheets UI and <code>functionName</code> is the name of the function. We will code these functions in a bit.</p> <h2>Add Highlight menu automatically</h2> <p>Now we have the function to create <strong>Highlight</strong> menu, we need a way to add the menu automatically everytime when we open the Sheets.</p> <p>We can use <a href="https://developers.google.com/apps-script/guides/triggers">Triggers</a> to do so.</p> <p>On the left menu, select <strong>Triggers</strong>, then <strong>create a new Trigger</strong>.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:53.515625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/highlight-apps-script-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/highlight-apps-script-02.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/highlight-apps-script-02.jpg"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-02.jpg" alt="Steps to open Triggers"> </picture> </div><figcaption>Steps to open Triggers</figcaption></figure> <p>Follow my configuration below and save it:</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:96.58203125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/highlight-apps-script-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/highlight-apps-script-03.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/highlight-apps-script-03.jpg"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-03.jpg" alt="Trigger configuration"> </picture> </div><figcaption>Trigger configuration</figcaption></figure> <p>Basically we are setting our <code>createHighlightMenu</code> function earlier (the Highlight menu) to run everytime when the spreadsheet opens.</p> <p>Next, go back the spreadsheet and hit refresh. Wait for a moment, the Highlight menu should appear at the end of the menu.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:26.66015625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/highlight-apps-script-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/highlight-apps-script-04.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/highlight-apps-script-04.jpg"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-04.jpg" alt="The Highlight menu"> </picture> </div><figcaption>The Highlight menu</figcaption></figure> <h2>Creating the Highlight functions</h2> <p>Go back to our <code>Code.gs</code> file. Let's start coding the Highlight functions. The basis of all 3 highlight functions (row, column, crosshair) are the same. Therefore, in order to save some coding effort, we will add one more function <code>highlight(type)</code> with a parameter to handle the actual operation.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token parameter">type</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// code later</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightRow</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'row'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightColumn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'column'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightCrosshair</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'crosshair'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre> <p>Great, now let's look at the <code>highlight</code> function.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token parameter">type</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> ss <span class="token operator">=</span> SpreadsheetApp<span class="token punctuation">.</span><span class="token function">getActive</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> s <span class="token operator">=</span> ss<span class="token punctuation">.</span><span class="token function">getActiveSheet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> a <span class="token operator">=</span> ss<span class="token punctuation">.</span><span class="token function">getActiveCell</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">const</span> col <span class="token operator">=</span> a<span class="token punctuation">.</span><span class="token function">getColumn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> row <span class="token operator">=</span> a<span class="token punctuation">.</span><span class="token function">getRow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> maxColumns <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getMaxColumns</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> maxRows <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getMaxRows</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">const</span> c <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getRange</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> col<span class="token punctuation">,</span> maxRows<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// the whole column</span><br> <span class="token keyword">const</span> r <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getRange</span><span class="token punctuation">(</span>row<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> maxColumns<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// the whole row</span><br> <span class="token keyword">const</span> ar <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getRange</span><span class="token punctuation">(</span>row<span class="token punctuation">,</span> col<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// the current active cell</span><br> <br> <span class="token comment">// Highlight properly based on type</span><br> <span class="token keyword">let</span> ranges <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">!=</span> <span class="token string">'column'</span><span class="token punctuation">)</span> ranges<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span><span class="token function">getA1Notation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">!=</span> <span class="token string">'row'</span><span class="token punctuation">)</span> ranges<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>c<span class="token punctuation">.</span><span class="token function">getA1Notation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> ranges<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>ar<span class="token punctuation">.</span><span class="token function">getA1Notation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> ss<span class="token punctuation">.</span><span class="token function">getRangeList</span><span class="token punctuation">(</span>ranges<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">activate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <p>Let's walkthrough the code together:</p> <ol> <li>First we need to get the current selected cell <a href="https://developers.google.com/apps-script/reference/spreadsheet/sheet#getactivecell"><code>ss.getActiveCell()</code></a>.</li> <li>Then from the current selected cell, we can get the column, row information. We also need to get the max row and column number because we need to highlight the whole row and column later.</li> <li>Next, we use the <a href="https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangerow,-column"><code>getRange(row, column, numRows?, numColumns?)</code></a> function to set the appropriate highlight range - column, row and the current cell <code>ar</code>.</li> <li>Depending on the type, we will highlight the appropriate ranges with this line <code>ss.getRangeList(ranges).activate();</code>. For example, if it's <code>row</code> type (<code>if (type != 'column')</code>), then we won't highlight column.</li> </ol> <p>Save your code and viola. Run it yourself and see it in action! 😃</p> <p>Here are the demos:</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-05.gif" alt="Row highlight"> </picture> </div><figcaption>Row highlight</figcaption></figure> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-06.gif" alt="Column highlight"> </picture> </div><figcaption>Column highlight</figcaption></figure> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-07.gif" alt="Crosshair highlight"> </picture> </div><figcaption>Crosshair highlight</figcaption></figure> <h2>Bonus tip: Highlight on select</h2> <p>Says, if you want the crosshair highlight happens automatically when you select a cell, you can add this function in the code.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">onSelectionChange</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'crosshair'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre> <p><a href="https://developers.google.com/apps-script/guides/triggers#onselectionchangee"><code>onSelectionChange(e)</code></a> is basically a trigger provided by the Sheets.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:51.125%"> <img src="https://jec.fish/assets/img/blog/highlight-apps-script-08.gif" alt="Crosshair highlight on selection change"> </picture> </div><figcaption>Crosshair highlight on selection change</figcaption></figure> <p>Save your code and test it on your page. Take note that the performance is quite slow. It takes a few seconds to crosshair highlight after you select a cell.</p> <h2>Final code</h2> <p>Finally, the complete code:</p> <pre class="language-js"><code class="language-js"><span class="token comment">/* Code.gs */</span><br><br><span class="token keyword">function</span> <span class="token function">createHighlightMenu</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> SpreadsheetApp<br> <span class="token punctuation">.</span><span class="token function">getActiveSpreadsheet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br> <span class="token punctuation">.</span><span class="token function">addMenu</span><span class="token punctuation">(</span><span class="token string">'Highlight'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><br> <span class="token punctuation">{</span>name<span class="token operator">:</span> <span class="token string">'Highlight row'</span><span class="token punctuation">,</span> functionName<span class="token operator">:</span> <span class="token string">'highlightRow'</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">{</span>name<span class="token operator">:</span> <span class="token string">'Highlight column'</span><span class="token punctuation">,</span> functionName<span class="token operator">:</span> <span class="token string">'highlightColumn'</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">{</span>name<span class="token operator">:</span> <span class="token string">'Highlight crosshair'</span><span class="token punctuation">,</span> functionName<span class="token operator">:</span> <span class="token string">'highlightCrosshair'</span><span class="token punctuation">}</span><br> <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token parameter">type</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token keyword">const</span> ss <span class="token operator">=</span> SpreadsheetApp<span class="token punctuation">.</span><span class="token function">getActive</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> s <span class="token operator">=</span> ss<span class="token punctuation">.</span><span class="token function">getActiveSheet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> a <span class="token operator">=</span> ss<span class="token punctuation">.</span><span class="token function">getActiveCell</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">const</span> col <span class="token operator">=</span> a<span class="token punctuation">.</span><span class="token function">getColumn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> row <span class="token operator">=</span> a<span class="token punctuation">.</span><span class="token function">getRow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> maxColumns <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getMaxColumns</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> maxRows <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getMaxRows</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br> <span class="token keyword">const</span> c <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getRange</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> col<span class="token punctuation">,</span> maxRows<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> r <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getRange</span><span class="token punctuation">(</span>row<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> maxColumns<span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> ar <span class="token operator">=</span> s<span class="token punctuation">.</span><span class="token function">getRange</span><span class="token punctuation">(</span>row<span class="token punctuation">,</span> col<span class="token punctuation">)</span><span class="token punctuation">;</span> <br> <br> <span class="token keyword">let</span> ranges <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">!=</span> <span class="token string">'column'</span><span class="token punctuation">)</span> ranges<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span><span class="token function">getA1Notation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">!=</span> <span class="token string">'row'</span><span class="token punctuation">)</span> ranges<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>c<span class="token punctuation">.</span><span class="token function">getA1Notation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> ranges<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>ar<span class="token punctuation">.</span><span class="token function">getA1Notation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br> ss<span class="token punctuation">.</span><span class="token function">getRangeList</span><span class="token punctuation">(</span>ranges<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">activate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightRow</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'row'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightColumn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'column'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">highlightCrosshair</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token function">highlight</span><span class="token punctuation">(</span><span class="token string">'crosshair'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token comment">// Uncomment this function if you need</span><br><span class="token comment">// function onSelectionChange(e) {</span><br><span class="token comment">// highlight('crosshair');</span><br><span class="token comment">// }</span></code></pre> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1364499699386884096"> twitter.com/jecfish/status/1364499699386884096 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> <entry> <title>Fish 037</title> <link href="https://jec.fish/fish/20210227"/> <updated>2021-02-27T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210227</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210227.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210227.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210227.jpg"> <img src="https://jec.fish/assets/img/fish/20210227.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Officially back at fish selling today. CNY js over, but I still greet every auntie Happy New Year tdy. 😁</p> <p>We sell less stock tdy, but customer flow is smooth. I like it this way without crazy crowd coz conversations can happen.</p> <p>My dad might think otherwise. 😂</p> <hr> <p>A: I don't want the mouth and tail, help me chop it off.<br> Me: Dad, u roger that?<br> Dad: Okay.</p> <p>(A few moments later)</p> <p>Dad: Done, pack it.<br> A: Aiyo, why still gt head and tail one, i said dun wan ady. Why dun listen to me?</p> <p>Me: Haiya, he is old ady, memory faded easily nowadays, haha. Will cut it for u now ya.</p> <p>Dad: CNY leh, no head no tail nt good. Symbolize u do work nt complete and nt responsible leh. (做事无头无尾)</p> <p>A: (Worried) Har, like that one meh, but CNY is over ady, js cut la. Should be fine right?</p> <p>Me: Dad, forgot to cut say forgot la, stop bluffing k. 😂</p> <p>Auntie: Ceh, don't learn frm ur dad, always talk 3 talk 4.</p> <p>Dad, Auntie, Me: Laughing.</p> <p>Keeping customers entertained while we making/fixing mistakes is an important skill. I think we did it well.</p> <p>If a joke cannot fix the problem, then giving fish discount probably will. 😆</p> <p>Have a lovely weekend all!</p> </content> </entry> <entry> <title>Fish 038</title> <link href="https://jec.fish/fish/20210306"/> <updated>2021-03-06T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210306</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:78.515625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210306.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210306.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210306.jpg"> <img src="https://jec.fish/assets/img/fish/20210306.jpg" alt="fish packs"> </picture> </div><figcaption>fish packs</figcaption></figure> <p>This is how we mark customers' purchases. Name on plastics.</p> <p>Let's talk about how Chinese name relates to society progress.Then I share about a problem with the above marking method.</p> <hr> <p>The two words on plastics are common names for old old generation (50s) - 金(gold), 财(prosper). Other common ones are 银(silver), 富(rich), 发(be rich).</p> <p>From the names, u knw how much Chinese like money 💰- that reflect the hardship and poverty the old one went thru, so they wish the next generation $$$.</p> <p>As life getting better over time, less pressure on $$$, then naming changed for older generation (70s) - 扬(famous), 胜(win), 建(build), 丽(beautiful), 爱(love), 诗(poem).</p> <p>See it moved frm $$$ to wishing:</p> <ul> <li>men: hv a career / society status, winning, build something, be famous #扬名立万 #名扬四海</li> <li>women: js to be love, poetic, beautiful</li> </ul> <p>Then for the younger generation, the names become more artsy, creative and spontaneous - 雨(rain), 宇 (universe), 峰 (mountain), 紫 (purple).</p> <p>Even better, I start seeing some parent try to make the names sound like Korean stars or just direct translated from English (For example Henry Lim - 林亨力). 😂</p> <p>--<br> Names are gifts from parents to us. Hopefully parents choose their gifts wisely.</p> <p>Make sure it at least sound acceptable in a few commonly used languages, so ur kids dun get laughed by ppl, else it become nightmare. 😆</p> <p>--<br> There are a few 金(gold) and 财(prosper) in pasar, so I did mix up their fishes sometimes. 🙈</p> </content> </entry> <entry> <title>Fish 039</title> <link href="https://jec.fish/fish/20210313"/> <updated>2021-03-13T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210313</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210313.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210313.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210313.jpg"> <img src="https://jec.fish/assets/img/fish/20210313.jpg" alt="long queue"> </picture> </div><figcaption>long queue</figcaption></figure> <p>Jealous! The opposite stall is super crowded tdy. Guess what they sell? (Reveal at the end)</p> <p>See our stall gt only 1 customer and im so free to take photo, u knw how good is our business tdy la... 🙈</p> <hr> <p>Today I got challenged again. This time is my fish picking skill.</p> <p>Auntie: Ei, help me choose 3 pomfrets.<br> Me: Okay.</p> <p>Auntie: Har, you ah? U knw hw to choose anot oh?<br> Me: Of course I knw, depends on whether u trust me or not jer.</p> <p>Auntie: Err... Ok la, u choose ba. (Sound nt convincing)<br> Me: (Picking)</p> <p>(Dad freed up frm fish processing)</p> <p>Auntie: Eh, ur daughter really knw hw to pick one anot...</p> <p>Me: 🙄🙄 (Finished pick, but I put back all the fishes) Aiya, let's my dad pick for u la, the fishes are fresher tht way.<br> Auntie: Haha, he is expert ma, nt like me and u.</p> <p>Me: Haha, ya la.<br> Dad: (Picking the same fishes I put back)</p> <hr> <p>Did I sound sarcastic? Partially.</p> <p>Am I fed up? Nah, I put back the fishes nt becoz i 😠, js because I can read her mind. She is happier if it's dad's pick.</p> <p>It's my dad's stall, let him be the face of the business, I don't mind.</p> <p>It's nt perception, it's the fact tht my dad IS MORE experienced than me, but all fishes r fresh and frm the same pool, no special picking skill needed.</p> <p>U don't always need the most experienced to do a simple job.</p> <p>Js saying, give some chance to junior la. 😆</p> <hr> <p>That's a vegetarian food stall, only operate on 1st and 15th (lunar calendar). #初一十五</p> <p>Usually my dad's stall is the busiest. This is an exception, and cant believe it's a vegetarian stall. 😆</p> <p>Taste good. I knw coz an auntie customer helped me to queue and buy js nw. Good auntie.</p> </content> </entry> <entry> <title>Fish 040</title> <link href="https://jec.fish/fish/20210320"/> <updated>2021-03-20T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210320</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:60.3515625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210320.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210320.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210320.jpg"> <img src="https://jec.fish/assets/img/fish/20210320.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Super busy day. Many fishes, many customers, partly coz my dad was off ytd due to a rat visiting our house. 🙈</p> <hr> <p>Today I became a superstar in the pasar, because... my dad bought the #feminine magazine few days ago, put at the stall let ppl read. 🙄</p> <p>He also shared my <a href="https://youtu.be/kEieXe-3DGU">videos</a> and <a href="https://feminine.com.my/news/choosetochallenge-%e8%b0%b7%e6%ad%8c%e6%8a%80%e6%9c%af%e5%bc%80%e5%8f%91%e5%a5%b3%e4%b8%93%e5%ae%b6-%e9%98%ae%e8%b4%9d%e7%90%aa%e4%b8%8d%e5%ae%b9%e5%bf%8d%e6%80%a7%e5%88%ab%e6%ad%a7/">interview URLs</a> with some customers and pasar sellers. 🙄</p> <p>Dad: Did the fruit stall auntie asks u anything?<br> Me: Ya, she asked about the interview.</p> <p>Dad: She always like to show off her children ma, so I purposely shared with her.<br> Me: So what's the difference btw u and her since u show off oso?</p> <p>Dad: I won't if she doesn't.<br> Me: But in the interview I did point out tht u treat son better wor, even educational wise. #重男轻女</p> <p>Dad: Doesn't matter, that's common thinking in old generation. I'm js one of them. The point is that train u well.</p> <p>Me: 🙄 (Should I say thank you)</p> <p>Well, I don't enjoy limelight in pasar setting. So many kepochi questions I need to entertain, and no aunties offer to introduce me their sons. 😂</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="https://feminine.com.my/news/choosetochallenge-%e8%b0%b7%e6%ad%8c%e6%8a%80%e6%9c%af%e5%bc%80%e5%8f%91%e5%a5%b3%e4%b8%93%e5%ae%b6-%e9%98%ae%e8%b4%9d%e7%90%aa%e4%b8%8d%e5%ae%b9%e5%bf%8d%e6%80%a7%e5%88%ab%e6%ad%a7/">Feminine Magazine</a> - A popular long running local Chinese magazine. I did an interview with them.</li> </ul> </content> </entry> <entry> <title>Fish 041</title> <link href="https://jec.fish/fish/20210327"/> <updated>2021-03-27T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210327</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.25%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210327.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210327.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210327.jpg"> <img src="https://jec.fish/assets/img/fish/20210327.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Today specialty - 9-section shrimps. #九节虾</p> <p>Sold out, nt manage to hide some beforehand, ish. 🙄</p> <hr> <p>Today is questions time! (A pasar auntie spotted me eating with a guy closely &amp; reported to dad few days ago)</p> <p>Dad: So u don't need to go to Germany ady is it?<br> Me: Don't knw yet. Why?</p> <p>Dad: Then why u paktor?<br> Me: Har, cannot meh? U want me to go Germany and find an angmo is it?</p> <p>Dad: No la, but what's your plan then?<br> Me: What plan? No plan.</p> <p>Dad: Later marry, baby all these how?<br> Me: Walao, eh! Boss, chill! Don't think so long first k.</p> <p>Dad: Har? Then what for u paktor!?<br> Me: Because I feel like it, cannot meh!</p> <p>Dad: If I say cannot u listen meh?<br> Me: Since u knw I dun listen, then y u still mumbling like those pasar aunties!!!</p> <p>Dad: ... 🤦(Continue cut fish, thinking what kind of daughter is this)</p> <hr> <p>In fact I got many similar &quot;caring&quot; queries and congrats messages everywhere.</p> <p>As Chinese saying - &quot;boat will be straight when reach to jetty&quot; lah. (All issues will hv solution when the time comes) #船到桥头自然直</p> <p>Whatever u ask me, pls go and ask God. Coz only She knows.</p> <p>I don't/can't think too far ahead, js go with the flow. So pls stop &quot;caring&quot; / kepochi me k!</p> </content> </entry> <entry> <title>Fish 042</title> <link href="https://jec.fish/fish/20210403"/> <updated>2021-04-03T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210403</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:73.92578125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210403.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210403.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210403.jpg"> <img src="https://jec.fish/assets/img/fish/20210403.jpg" alt="fish packs"> </picture> </div><figcaption>fish packs</figcaption></figure> <p>Ishh, today morning drizzling, make me work OT...</p> <p>These 2 bags got premium packing (with ice), premium marking (with emoji) and premium delivery service by me. 😎</p> <hr> <p>9.30 am still left 5 white pomfrets #白昌</p> <p>Me: Dad, do u want to see top sales in action?<br> Dad: I am the top of the top already.</p> <p>Me: 🙄 Walao, so u want me to help sell all these fishes anot?<br> Dad: Want lah. Can ah?</p> <hr> <p>Call 1</p> <p>Me: (Calling Kenneth) Jie ah, wanna buy pomfret? Good for babies.<br> Ken: How much?</p> <p>Me: Buy 2 la, RM 39.<br> Ken: RM 39 each or total?</p> <p>Me: (The fact he asked that question mean he doesn't usually buy fish 😂) It's total lah.<br> Ken: Ok.</p> <hr> <p>Call 2</p> <p>Me: (Calling Hugo) Kwan ah, wanna buy pomfrets for ur family?<br> Hugo: Oh, ok.</p> <p>Me: How many u want? 2?<br> Hugo: Ok.</p> <p>Me: (Didn't even ask price. 😂)</p> <hr> <p>Sell 4 fishes in 5 min. Not bad ah. 😎</p> <p>Just when I wanna make another call to sell the last one, 1 auntie passed by.</p> <p>Me: Wah, auntie, lucky u, left the last pomfret waiting for u.<br> Auntie: Really ah? Fresh anot?</p> <p>Me: (Straight weighting) As fresh as your look today. Last one sell u cheap cheap, RM 18. (Actually I didn't discount)<br> Auntie: Haha, okay, help me pack it.</p> <hr> <p>Woohoo, sold out. Off work!</p> <p>Me: How's your daughter performance?<br> Dad: U can inherit my fish business ady.</p> <p>Me: 😂</p> <hr> <p>Learning today: It's okay to utilize your friends sometimes, especially it's for their own good, fresh fishes! 😂</p> <p>Thanks my friends Hugo and Kenneth for giving me a chance to be the top of the top sales today!</p> </content> </entry> <entry> <title>Fish 043</title> <link href="https://jec.fish/fish/20210410"/> <updated>2021-04-10T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210410</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:86.1328125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210410.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210410.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210410.jpg"> <img src="https://jec.fish/assets/img/fish/20210410.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>That's what left at 8.40 am. I leave the stall, off work.</p> <p>Energy was running low for both dad and me for the week. We hv our own issues to deal with and family one.</p> <p>Dad has nt been operating biz for the whole week. Pasar aunties were quite concern.</p> <p>A: Wah, my grandchild has no fishes to eat for a week ady.<br> B: Where were u go holiday?<br> C: Fuiyor, rich ady hor, no need work.<br> D: Wow, u still alive, thought u r dead. (In fact a few pasar operators js passed away)</p> <p>In fact, my dad feels boring and lonely, also... lazy. I support he took time off, coz he really doesn't need to work hard.</p> <p>His loneliness isn't something me as a children can help with. He did hang out with friends everyday, but I guess that's different too.</p> <p>What he is looking is a companion. The feeling that someone is thr when he's home.</p> <p>The fact tht he's alone (even with kids) facing 4 side of walls in home/room scares him.</p> <p>I think many of us face similar feeling sometimes. It's ok, js need to deal with it.</p> <p>If anyone has aunties wanna intro him, pm lol! 😂</p> <p>#CMB for dad.</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="https://coffeemeetsbagel.com/">CMB</a> - Coffee meets Bagel, a dating app.</li> </ul> </content> </entry> <entry> <title>Fish 044</title> <link href="https://jec.fish/fish/20210417"/> <updated>2021-04-17T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210417</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:86.1328125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210417.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210417.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210417.jpg"> <img src="https://jec.fish/assets/img/fish/20210417.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Oops, 9.30am, still left a flounder, golden pomfret, salmon, and a few black pomfrets. Nt good, nt good.</p> <hr> <p>Me: So do u hv anything to say to me?<br> Dad: So fast one. Why ah? Is it because of reason x.</p> <p>Me: No, because he is a jerk.<br> Dad: Men don't deserve good treatment frm girls unless they prove otherwise.</p> <p>Me: Wise. Including you.<br> Dad: Ya, men are bad. Don't waste your kindness on them. #犯贱</p> <p>Me: I'll take ur advise and this cod fish.<br> Dad: (Weight) Ok, RM 32.</p> <p>Me: U don't deserve my kindness too. U took advantage of me, free labor n charge me for this tiny piece of fish.<br> Dad: ...</p> <hr> <p>Well, my friend asked: What would you say looking back.</p> <p>Me: I completed a full cycle of dating app experience. Achievement unlocked.✌️</p> <p>Friend: Is it worth all the heartache though.<br> Me: Worth, I learned my lesson ma. Warm up engine. 😂</p> <hr> <p>Life lesson: There are many greater things to focus in life, there're ppl who worth it.</p> <p>Thanks buddies who send regards, dessert, free meals, wine, beer, companionship my way. Overload with love, u all knw who u r. 🥰</p> <p>Make me realized, I am such a great person to be with. 😎</p> <p>I'm fine, for real, else I don't deserve all your kindness too.</p> </content> </entry> <entry> <title>Fish 045</title> <link href="https://jec.fish/fish/20210424"/> <updated>2021-04-24T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210424</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:63.76953125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210424.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210424.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210424.jpg"> <img src="https://jec.fish/assets/img/fish/20210424.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>A gg day, both me and the pasar business. Raining, customers all hv good sleep didn't come pasar. Me still woke up at 6 despite sleeping at 3am, tired. 🥺😴</p> <hr> <p>&quot;You should treat all customers equally, u r being unfair,&quot; an auntie told dad today.<br> The conversation started coz my dad has a whole bag of hidden glutinous fishes #糯米糍 (nt putting on the table coz all pre-booked, such hot fish).<br> Auntie A noticed it when my dad took it out for a pre-booked customer.</p> <hr> <p>A: Eh, why got glutinous fish. I want.<br> Dad: All booked ady, limited.</p> <p>A: Aiyo, I told u long time ago to reserve for me if got stock.<br> Dad: Is it, I forgot.</p> <p>(Dad packing glutinous fishes for a premium customer 😎)<br> A: (Triggered) Am I not a big customer so u didn't reserve for me?<br> Dad: No la, limited stock really. Next time sure reserve for u.</p> <p>A: But u could hv give the limited stock to me. I buy fish frm u every week leh. Always spend quite a bit, am I not a big customer?<br> A: You should treat all customers equally, u r being unfair. I am big customer also.<br> Dad: (Squeeze out a glutinous fish somewhere) Ok la, give u one la, took frm another customer one.</p> <p>A: Haiz, u knw I hv big family, I want two.<br> Dad: Really don't hv ady. Do u want it?</p> <p>A: Don't want, I want two.<br> Dad: Really dun hv. Next time la.</p> <p>B: Give me that as well.<br> A: (Paused for a min) I think I js take the one.<br> B: No, js now u said dun want already.<br> A: But now I want it.</p> <p>Me: (gg la, customers fight)<br> Dad: (Talk to 😎 Js give her the one la.</p> <p>B: Cannot, I lazy to come out buy fish tmr and she didn't want it js nw.<br> A: Why are you all being so unfair to me, it supposed to be mine, bla bla bla~~~</p> <p>Me: 🥱 (It's js a fish!)</p> <hr> <p>What is equality? Standard service are available to all customers (clean, pack, pretty smiles by me, etc).<br> As a biz, of course there're service offered based on tier lol.</p> <p>Anyone can reserve fish, but the fish availability is subject to market and... priority.<br> I mean, seriously. Premium customers who generate have higher priority.</p> <p>Is it unfair? That's js the life fact one needs to accept. Upgrade your membership if u want it, stop act and think you are one coz...</p> <p>YOU. ARE. NOT. AS. PREMIUM. AS. YOU. THOUGHT.</p> <p>#不要自以为是个人物</p> </content> </entry> <entry> <title>Fish 046</title> <link href="https://jec.fish/fish/20210501"/> <updated>2021-05-01T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210501</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:78.125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210501.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210501.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210501.jpg"> <img src="https://jec.fish/assets/img/fish/20210501.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>No pasar fish story this week because im off! 👋</p> </content> </entry> <entry> <title>Fish 047</title> <link href="https://jec.fish/fish/20210508"/> <updated>2021-05-08T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210508</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:77.1484375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210508.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210508.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210508.jpg"> <img src="https://jec.fish/assets/img/fish/20210508.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Fuiyoh, today sold out at 7.55 am! No fish next week due to Raya celebration and COVID cluster in the wholesale market.</p> <hr> <p>My dad doesn't hv high education so his English sucks. A regular customer came today.</p> <p>Dad: Take out Angie's fishes frm the secret box.<br> Me: Ok. What fishes are those ah?</p> <p>Dad: You find la, I wrote her name on it already.<br> Me: (Looking for ANGIE but cannot find.)</p> <p>Me: Don't hv leh, u sure ah?<br> Also me: (Spot the plasctic with N.G on it.) N G !?!?</p> <p>Dad: Ya lah, this one. So stupid u, so obvious also need to find so long.</p> <p>Ok, you win. Me burst into laugher. The customer as well. 🤣</p> <p>In fact this is nt the first time. Another customer name &quot;Irene&quot; but my dad always called her &quot;Ee Ren&quot;. 😂<br> Well, whatever it is, the level of English proficiency doesn't matter to his business. However broken his English is, customer can still understand somehow.</p> <p>Language is about communication. Successfully deliver the right message is more important than grammar correctness.<br> No need to shy about or look down on yourself for nt good enough at it. The more u unconfident, the worse u speak.</p> <hr> <p>That being said, I will continue to laugh at my dad, and ppl will continue to bash my Google videos on my Asian English accent.</p> <p>Both of us will continue ignore those, and do what we r doing. 😆</p> </content> </entry> <entry> <title>Fish 048</title> <link href="https://jec.fish/fish/20210515"/> <updated>2021-05-15T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210515</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:71.6796875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210515.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210515.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210515.jpg"> <img src="https://jec.fish/assets/img/fish/20210515.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>No fish selling today coz it's Hari Raya, but got story.</p> <p>My dad recently keep saying something like these.</p> <p>Dad: My friend A's son so good. Give him X amount of monthly allowance.<br> Me: So?</p> <p>Dad: Friend's B's daughter also good. Give family Y amount of $.<br> Me: (Knowing where is this going but act stupid) So?</p> <p>Dad: Don't u think u should contribute more?<br> Me: Why should I? Don't u hv enough?</p> <p>Dad: Because I'm your dad. Their children don't earn as much but give family almost half their salary as allowance. Look at u...<br> Me: Appreciate what u already hv. Don't keep comparing with others, else you won't be happy ya.</p> <p>Me: This is how u taught us during child time. No extra $ without valid and strong reasons. U taught me well ya, so no points asking. 😂<br> Dad: ...</p> <hr> <p>Supporting parents financially is our responsibility as children, but nt blindly.</p> <p>Well, dad doesn't increase my angpao money even I complain every Chinese New Year. So same concept apply lol. 😆</p> </content> </entry> <entry> <title>Chrome DevTools for designers</title> <link href="https://jec.fish/deck/io-2021"/> <updated>2021-05-19T00:00:00-00:00</updated> <id>https://jec.fish/deck/io-2021</id> <summary>Google IO 2021 - Chrome DevTools for designers</summary> <category term="deck"/> <content type="html"><p>Chrome DevTools is your DesTools (Designer Tools) too! Adam and I share about how to use Chrome DevTools in solving day-to-day design challenges.</p> <p>Have been attending <a href="https://g.co/io">Google IO</a> for a few years. Google IO 2021 was supposed to be the first IO (https://g.co/io) that I would be speaking on stage.</p> <p>Well, end up I speak virtually at home with Adam, haha.</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/yNwwEu3Kcbs" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/yNwwEu3Kcbs?autoplay=1><img src=https://img.youtube.com/vi/yNwwEu3Kcbs/maxresdefault.jpg alt='Chrome DevTools for designers'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Chrome DevTools for designers"> </iframe> </div> <p>Miss the pre-covid time where we get together at Mountain View, under hot sun, shouting, get excited when there are new features announced.</p> <p>Till we meet again, stay safe, miss you all!</p> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1409350391427264523"> twitter.com/jecfish/status/1409350391427264523 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div></content> </entry> <entry> <title>Fish 049</title> <link href="https://jec.fish/fish/20210522"/> <updated>2021-05-22T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210522</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:60.15625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210522.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210522.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210522.jpg"> <img src="https://jec.fish/assets/img/fish/20210522.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Sold out at 8am, this is a big red snapper pre-ordered by a customer.</p> <p>Hot pasar topic tdy: Will the govt announce &quot;MCO 3.0 (Full)&quot; version later today? 🤨</p> <hr> <p>Customers expect us to remember their and their family purchase history.</p> <p>Uncle A: Did my wife bought pomfret ytd?<br> Me: Huh? Who is your wife? (Look at dad)<br> Dad: Ya, she bought only a small one ytd, did you all ate ytd? Tdy can buy this grouper.</p> <p>Auntie B: Did I bought prawn ytd?<br> Me: (Walao auntie, can call n ask ur hubby instead of me)<br> Dad: U didn't ytd, but you bought Thurs. It's ok la, MCO might come, buy and keep la.</p> <p>Auntie C: Give me these 3 fishes, split it, one is for my mom.<br> Dad: Har, ur mom js came and bought one js nw.<br> Me: 😳</p> <p>Auntie C: Is it? My mom already come js nw?<br> Dad: Yaya, so 2 enough or 3?</p> <hr> <p>Omg, remember customer family tree ady hard. Somemore need to track individual preference and purchase history, that's really require extra RAM and database (brain).</p> <p>Not sure how my dad did that. Impressive and wondering at the same time because... he doesn't even remember my birthday nor my food preference lol. 🙄😂</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="hhttps://en.wikipedia.org/wiki/Malaysian_movement_control_order">MCO</a> - Movement Control Order (FMCO)</li> </ul> </content> </entry> <entry> <title>Fish 050</title> <link href="https://jec.fish/fish/20210529"/> <updated>2021-05-29T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210529</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:72.75390625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210529.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210529.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210529.jpg"> <img src="https://jec.fish/assets/img/fish/20210529.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>6.15am, sky still dark. Stall filled with crazy crowd. All fishes sold out at 7.30, then receive many why-no-reserve-fish-for-me complains.<br> I thought SOP mentioned operation should start at 8am!? Fine (Saman) all these ppl la! 🙈</p> <hr> <p>8am in the morning.</p> <p>Auntie A: Har, no fish already?<br> Me: No more. Even I don't get time to steal some. Crazy crowd.</p> <p>A: Can reserve fish for me tmr?<br> Dad: Call me tmr 5am.</p> <p>A: I woke up 5am already today, but I follow SOP. Scare got fine. No police come patrolling?<br> Dad: Maybe police has nt wake up yt.</p> <p>A: What fish u hv tmr, can I js preorder?<br> Dad: Not sure what fishes we'll hv tmr. Call me 5am la.</p> <p>A: (Walk away and come back in 1 hr) Eh, how about u just reserve me whatever fishes u hv tmr ok? Any fishes will do.<br> Me: Har? Any fishes?</p> <p>A: Ya, just any. As long as fishes.</p> <hr> <p>Wow, people really so desperate about fishes! So many preorders today.<br> Crisis time, all stuck at home. One of the joy/activity one can have is eating/cooking something good.<br> As a fish seller, the least we can do is pick fresh stock n provide to customers. Not saving-the-world kind of mission, but does make tiny impact to lifes.<br> Eat healthy, stay safe and happy all!<br> Tmr is a fish selling day for me as well in view of tdy crowd, what a poor free labor. 😌</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="https://www.mkn.gov.my/web/ms/sop-pkp/">SOP</a> - Standard operating procedure, here refer to Malaysia COVID-19 lockdown SOP</li> </ul> </content> </entry> <entry> <title>Fish 051</title> <link href="https://jec.fish/fish/20210530"/> <updated>2021-05-30T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210530</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:72.265625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210530.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210530.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210530.jpg"> <img src="https://jec.fish/assets/img/fish/20210530.jpg" alt="7 star groupers"> </picture> </div><figcaption>7 star groupers</figcaption></figure> <p>7 star groupers kind of day. 🐟 I draw that ⭐! #七星斑</p> <p>5am reach at pasar, 7am sold out. Question of the day - can morning roadside pasar operates during FMCO?</p> <p>Not sure yt, but hopefully not. The &quot;F&quot; in FMCO could means Full or Failed.</p> <p>Dear govt, pls granted my wish 🙏, need proper weekend break! 😬</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="https://www.iproperty.com.my/lifestyle/fmco-sops/">FMCO</a> - Full Movement Control Order (FMCO)</li> </ul> </content> </entry> <entry> <title>Fish 052</title> <link href="https://jec.fish/fish/20210605"/> <updated>2021-06-05T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210605</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:72.75390625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210605.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210605.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210605.jpg"> <img src="https://jec.fish/assets/img/fish/20210605.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Woohoo, no fish selling this week. God granted my wish lol, long weekend break!</p> <p>This is a photo frm last Sunday, spot the yellow eels, tofu fish and seven stars groupers - if u know which is which.</p> <p>My dear diver friends, can't do Fish ID (identify fish types &amp; names) in the sea now, come do in pasar la! 😆</p> </content> </entry> <entry> <title>Debugging with Chrome DevTools</title> <link href="https://jec.fish/deck/jsnation-2021"/> <updated>2021-06-11T00:00:00-00:00</updated> <id>https://jec.fish/deck/jsnation-2021</id> <summary>JSNation 2021 - Debugging with Chrome DevTools</summary> <category term="deck"/> <content type="html"><p>Sharing some tips and tricks to help you debug your web app effectively with Chrome DevTools.</p> <p><a href="https://live.jsnation.com/">JSNation Live 2021</a> is a 3-day conference on all things JS.</p> <p>In case the video below isn't working, you can watch it in <a href="https://portal.gitnation.org/contents/debugging-with-chrome-devtools">portal.gitnation.org/contents/debugging-with-chrome-devtools</a>.</p> <div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/559950111?title=0&byline=0&portrait=0" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen=""></iframe></div><script src="https://player.vimeo.com/api/player.js"></script> <!-- <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1409349445381083136"> twitter.com/jecfish/status/1409349445381083136 </a> </blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> --></content> </entry> <entry> <title>Fish 053</title> <link href="https://jec.fish/fish/20210612"/> <updated>2021-06-12T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210612</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:71.09375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210612.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210612.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210612.jpg"> <img src="https://jec.fish/assets/img/fish/20210612.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Pasar still closed. 😊</p> <p>Hv been sick for the week, never usually sick that long. I went n did my first PCR covid test, coz the kiasu thinking of &quot;although I think it's nt Covid, but what if it is?&quot;. Anyway, test result is negative. 😃</p> <p>Dad purposely sent me the famous Chinese medicine which rumors said it might cure COVID (莲花清瘟胶囊).</p> <p>Initially dad asked my bro to send me, but my bro refused.</p> <p>Bro: Ur daughter doesn't eat this kind of medicine. N it's rumors.<br> Dad: It's effective, trust me. U js send her la.</p> <p>Bro: (Call me) If I send u tht, will u eat?<br> Me: U knw ur sis well ya. 😂</p> <p>Bro: (Tell dad) No need la, your daughter doesn't even take medicine usually.<br> Dad: Shut up. If u don't want to send, then I send myself.</p> <p>Then the 莲花清瘟胶囊 appeared at my doorstep. Touching, aw. 🥺</p> <p>If it's you, will you eat it or lied to dad that you've eaten (even if u don't)? 🤔</p> <p>Also guess what I did at the end? 😏</p> <p>#世纪难题 #要辜负老人家一番心意 #还是乖乖吃掉可能是假药</p> </content> </entry> <entry> <title>Fish 054</title> <link href="https://jec.fish/fish/20210619"/> <updated>2021-06-19T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210619</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:62.5%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210619.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210619.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210619.jpg"> <img src="https://jec.fish/assets/img/fish/20210619.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Pasar is going to stop for... 2 more months or longer? According to the National Recovery Plan.</p> <p>How will the rakyat survive then? Some pasar sellers with stable customer base start taking online (phone line) orders, process and deliver to customers.</p> <p>Some are taking time to rest. My dad is doing hybrid. Taking small orders only when there are big orders, so work only once or twice a week.</p> <p>The reason being is it's energy draining and time consuming to do the additional cleaning at home afterwards and the delivery.</p> <p>Are there way to improve? Certainly. Worth the effort? Probably not. Just rest la~</p> <p>For those sellers who don't hv stable customer base, that's suck, especially those who hv kids to feed. 😌</p> <p>God bless all get through this covid economy crisis.</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="https://www.theedgemarkets.com/article/highlights-national-recovery-plan">National Recovery Plan</a> - A four-phase exit strategy from the COVID-19 crisis based on 3 indicators - no. of daily infections, bed utilisation rate in ICU wards, vaccination rates.</li> <li><a href="https://translate.google.com.my/?sl=ms&amp;tl=en&amp;text=rakyat&amp;op=translate">Rakyat</a> - the people, usually refer to the people of a country</li> </ul> </content> </entry> <entry> <title>Browser Automation with Puppeteer</title> <link href="https://jec.fish/deck/workerconf-2021"/> <updated>2021-06-25T00:00:00-00:00</updated> <id>https://jec.fish/deck/workerconf-2021</id> <summary>WorkerConf 2021 - Browser automation with Puppeteer</summary> <category term="deck"/> <content type="html"><p>Sharing a bit of my hike and bike experience and Browser automation with Puppeteer.</p> <p><a href="https://www.worker.sh/">WorkerConf 2021</a> is a tech conference with bike and hike action for open source.</p> <p>My session gives you an overview of what is Puppeteer.</p> <div class="video-wrapper"> <iframe src="https://www.youtube.com/embed/mqu3h84os-Y" srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/mqu3h84os-Y?autoplay=1><img src=https://img.youtube.com/vi/mqu3h84os-Y/maxresdefault.jpg alt='Browser automation with Puppeteer'><span>▶</span></a>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" title="Browser automation with Puppeteer"> </iframe> </div> <p>Stay safe all during this COVID-19 time!</p> <p>Here's the slides:</p> <script async="" class="speakerdeck-embed" data-id="932c979363e94deba3b04814cab38cb2" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script> <noscript> Download the slides here: <a href="https://speakerdeck.com/jecfish/workerconf-2021-browser-automation-with-puppeteer">https://speakerdeck.com/jecfish/workerconf-2021-browser-automation-with-puppeteer</a> </noscript> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1409349445381083136"> twitter.com/jecfish/status/1409349445381083136 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div></content> </entry> <entry> <title>Fish 055</title> <link href="https://jec.fish/fish/20210626"/> <updated>2021-06-26T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210626</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:75.390625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210626.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210626.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210626.jpg"> <img src="https://jec.fish/assets/img/fish/20210626.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>The 3rd closing week. I did miss pasar aunties a little.</p> <p>FMCO does impact my mental health a little. The feeling of stuck strikes more often, sometimes lonely. Anyone feel the same?</p> <p>Unsurprisingly, I hv many friends (mostly IT ppl) doing all right during this period. Make me wonder, why couldn't I? Am I nt IT!?</p> <p>If u r thinking &quot;Aiya, u r very lucky ady, u dun hv to worry about food and money, js stay at home also complain so much, so difficult meh, js hang in thr.&quot;, u r nt wrong, but blaming isn't helping.</p> <p>Social interaction is a need. One can pause for a while (coz unlike food, u won't die immediately without that) but the longer the time, it's getting harder. It impacts mental health, and might leads to greater problems.</p> <p>Stay negative (covid) all. Gambateh! No advice for u, but let's nt dismiss your feeling, get vaccinated, stay strong and get through this together. Fighting! 💪</p> <p><strong>Explain</strong>:</p> <ul> <li><a href="https://www.iproperty.com.my/lifestyle/fmco-sops/">FMCO</a> - Full Movement Control Order (FMCO)</li> </ul> </content> </entry> <entry> <title>Fish 056</title> <link href="https://jec.fish/fish/20210703"/> <updated>2021-07-03T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210703</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:79.296875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210703.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210703.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210703.jpg"> <img src="https://jec.fish/assets/img/fish/20210703.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Pasar still close. A random conversation with dad.</p> <p>Dad: You thought being a dad easy?<br> Me: Difficult meh, I see ur life now pretty chill.</p> <p>Dad: U don't knw the hardship I went through being the sole bread winner when u all r kids.</p> <p>Dad: So many &quot;mouths&quot; waiting for food, need to make sure I earn enough to provide.</p> <p>Dad: The fact that u r still here, talking so much, nt die due to hunger is already a good achievement of me.<br> Me: Oh...</p> <hr> <p>With sis out of job, bro forced to take long unpaid leaves, dad can't operate fish biz, I feel the need of nt losing my income.</p> <p>The pressure of bread winner must be 9999x than that, especially those out of job, with kiddo, old parents waiting for food, plus this unknown pandemic end date, really holly shit, feeling future-less.</p> <p>Some who did well before COVID, and struggle since then find it hard to ask for help - &quot;feeling no face&quot; (shameful).<br> Suicidal rate increase significantly since pandemic. Between ending life and raising white flag (asking for help, nt give up), choose the latter.</p> <hr> <p>Kerajaan failed us, nt the first time, wont be the last. Biar #KitaJagaKita, donate and help if you can.<br> Hang in there all! 💪</p> </content> </entry> <entry> <title>Fish 057</title> <link href="https://jec.fish/fish/20210710"/> <updated>2021-07-10T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210710</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:79.296875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210710.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210710.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210710.jpg"> <img src="https://jec.fish/assets/img/fish/20210710.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Pasar still close, but shopping mall reopened today!?</p> <p>Dad fell down and hurt his arm few days ago when delivering fish to a customer.</p> <p>Me: Why you fell down ah? Never see road properly?<br> Dad: The floor is slippery. My slippers has no pattern already (worn out).</p> <p>Dad: You know what. Luckily I always drink milk.<br> Me: Huh?</p> <p>Dad: See my bone is so strong now. Even I fell, it js hurt a little bit.<br> Me: 😂 Keep your positivity on!</p> <p>Me: (Secretly think: But if it's that strong, u dun even need to go clinic ah... 🙊)</p> <hr> <p>Case rises, but let's keep your positivity on.</p> <p>At least I am happy that my dad can go get a pair of slippers coz shopping mall is open! 🙈</p> <p><strong>Explain</strong></p> <ul> <li><a href="https://www.freemalaysiatoday.com/category/nation/2021/07/09/netizens-puzzled-over-reopening-of-department-stores-in-kl/">The reopening of several department stores in Kuala Lumpur has left netizens puzzled since the federal territory is still under Phase 1 of the national recovery plan.</a></li> </ul> </content> </entry> <entry> <title>Fish 058</title> <link href="https://jec.fish/fish/20210717"/> <updated>2021-07-17T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210717</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:79.4921875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210717.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210717.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210717.jpg"> <img src="https://jec.fish/assets/img/fish/20210717.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Woot, COVID cases are rising but EMCO ended mostly. Stationary and phone shops can resume biz but roadside pasar still can't operate! 😳</p> <hr> <p>There are some roadside pasar stalls start operating illegally. 🙊</p> <p>Me: How ah? Nt scare police come saman meh?<br> Dad: No choice lo, need money ma. Open super early and close before police wake up.</p> <p>Me: But in case police wake up early come raid, and the stall got fine, it's nt worth it!<br> Dad: Sometimes u js need to risk it. Maybe can nego ma~</p> <p>Me: Can nego with virus ah?<br> Dad: ...</p> <hr> <p>&quot;Sometimes u js need to risk it&quot; is a good life quote, but doesn't apply to everything I guess!?</p> <p>Well, we all broke rules sometime. Who am I to judge whether it's worth it or not right? That's the risk taker's call.<br> Caught by police or not, stay safe and healthy all!</p> </content> </entry> <entry> <title>Fish 059</title> <link href="https://jec.fish/fish/20210724"/> <updated>2021-07-24T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210724</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:123.73046875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210724.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210724.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210724.jpg"> <img src="https://jec.fish/assets/img/fish/20210724.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Helping dad to process 🐟 booking this morning. The list is so long!<br> That's my dad's handwriting. Pretty nice huh?</p> <hr> <p>He missed some orders. There are orders he forgot to capture. He doesn't sum the quantity correctly as well for some fishes coz he count manually.</p> <p>To do small improvements, what he needs is a printer, Google Sheets and probably a Google form for data entry.<br> Only if his daughter is patient enough to set that up and teach him that.</p> <p>But I kinda like to see his handwriting, and I think writing is good to prevent Alzheimer (or similar). So should I?</p> <p>As an unpaid non business-minded daughter, I think missing orders is fine, coz less work ma, can't hear aunties screaming in front of me anyway. 😂</p> </content> </entry> <entry> <title>Fish 060</title> <link href="https://jec.fish/fish/20210731"/> <updated>2021-07-31T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210731</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:72.16796875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210731.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210731.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210731.jpg"> <img src="https://jec.fish/assets/img/fish/20210731.jpg" alt="Fish stall"> </picture> </div><figcaption>Fish stall</figcaption></figure> <p>Another Saturday. Late pasar story today. Dad is admitted to hospital since Wednesday.</p> <p>Still nt yt out, but in good recovery progress. Hospital bill is really nt cheap, still accumulating. 😌</p> <hr> <p>What happens? Overstep on fish head last Saturday. Big toe sting by fish bone.</p> <p>Sting by fish bone is... quite normal as a fishmonger but it was kinda deep cut this time and wound infected.</p> <p>Went to clinic nearby initially, doctor did some jabs n monitor for a few days. The skin turns dark n infected area got bigger. Kinda scary if u think of amputation.</p> <p>Went to hospital. COVID situation make the admission process longer even private hospital, but eventually things sorted out.</p> <p>Ok la, so he is now enjoying his long holiday in hospital, without having to see us (no visitors allowed) and bill on us. 😒</p> <hr> <p>Me: My thumb also sting by fish bone same day as u n swollen for a few days.<br> Dad: Look like we r really father n daughter.<br> Bro: Remember the curry fish head u two cooked last week? See, its sibling now revenge lah! 😂</p> <p>Me: (Bro, u eat oso wor...🙊)</p> <hr> <p>Good la, still can joke. Stay safe n healthy all!</p> </content> </entry> <entry> <title>Fish 061</title> <link href="https://jec.fish/fish/20210807"/> <updated>2021-08-07T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210807</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:74.31640625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210807.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210807.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210807.jpg"> <img src="https://jec.fish/assets/img/fish/20210807.jpg" alt="Fish packages"> </picture> </div><figcaption>Fish packages</figcaption></figure> <p>Wee! Dad is out of hospital. Bill is 9k. Cheap? Expensive? Compare to saving his foot, the price definitely worth it.</p> <p>Bill is still rolling, coz need to return to hospital twice a week for cleaning and monitoring. 💰</p> <p>Dad dislikes visiting hospital coz it's pain and coz higher chance for COVID. 😷</p> <p>Me: See la, ask u dun work during EMCO, dun listen.<br> Me: Now the bill is more expensive than your fish earnings ady.</p> <p>Dad: Wei, u cannot say like that la. This is accident ma.<br> Bro: Different la. Fish earnings goes to him ma, but hospital bill goes to us! 😂</p> <p>Dad: I give u money and let u experience all these u want?<br> Me, Bro: Give us first and we'll consider. 😂😂😛😛</p> <p>COVID cases went up to 20k. No eyes see but life goes on. As long as we're alive, stay healthy, happy and safe, buddies! 💪</p> </content> </entry> <entry> <title>Fish 062</title> <link href="https://jec.fish/fish/20210814"/> <updated>2021-08-14T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210814</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210814.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210814.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210814.jpg"> <img src="https://jec.fish/assets/img/fish/20210814.jpg" alt="Tiger prawns"> </picture> </div><figcaption>Tiger prawns</figcaption></figure> <p>I wanna to eat prawn! But dad nt back in business yet. 🥺</p> <p>Had dinner with dad last night. His phone kept ringing - text, calls, voice messages non-stop.</p> <p>A,B,C...Z: Are you open tmr? Can I order fish? I want fish xxx tmr ...<br> Dad: Aiya, I'm still in recovering. Hopefully back in biz next week!</p> <p>Me: Wah, so busy hor. U really popular aren't you?<br> Dad: Haiz, I feel sad.</p> <p>Me: Why sad? Seeing all the money (potential earnings) fly away? Or feel sorry to the customers?<br> Dad: (Didn't answer, but show sad face) Haiz. 😟</p> <p>Me: No worries about customers la, they will go get fish somewhere else temporarily.<br> Me: Maybe temporary become permanently then u gg. 😆</p> <p>Me: Anyway, u guai guai recover first, don't rush it.<br> Me: Else later u operate, and infection again, earn a bit but another few k gone. 💰 Cannot la!</p> <hr> <p>It's a risk when u, as a business, don't operate consistently, coz customers will buy from somewhere else, and might switch forever if find it better.</p> <p>However, without a healthy body, you can do nothing.</p> <p>Can only follow up with them on phone lo, beat competitors with the relationship u build over years. Hopefully they'll be back when u r back again. 💪</p> <p>Again, stay healthy all! Probably watch more meme and funny videos to stay sane! 😆</p> </content> </entry> <entry> <title>Fish 063</title> <link href="https://jec.fish/fish/20210821"/> <updated>2021-08-21T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210821</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:67.48046875%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210821.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210821.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210821.jpg"> <img src="https://jec.fish/assets/img/fish/20210821.jpg" alt="Salmon"> </picture> </div><figcaption>Salmon</figcaption></figure> <p>Back in business today. Celebrate Malaysia got a new prime minister wee! 🤦♀️<br> Today an auntie said swear words to my bro. Half is auntie rudeness, half is our fault. 🤬</p> <hr> <p>The auntie is a regular customer which i labelled her as #ddly - request a lot, buy little.</p> <p>She likes to pick a few slices of fish, ask to weight, then complain about the price, usually buy none.</p> <p>Once in a blue moon, she might buy one, after some very heavy discount 🙄.</p> <p>Many times, she shows #behsong face and slap-throw my fish slices back to the base after checking the price as if my fish doesn't has mother. 😡</p> <p>Today she came again, and my bro handles her. Hoho~ 🍿🍿 kan we shud provide intensive training for newbie? 😈</p> <p>X: Weight these A, B, C fish slices for me.<br> Bro: Ok, price A B C.</p> <p>X: Expensive, how about E F?<br> Bro: Price E F.</p> <p>X: I take B, 17.8? RM 17 will do.<br> Bro: Cannot la.</p> <p>X: U blood sucker stall, always expensive.<br> Dad: Ok la, RM 17.</p> <p>(Bro distracted by other customer)<br> (I pack and put aside, collect money as well)</p> <p>X: Chang my mind, wanna swap probably. How about slices G H I?<br> Bro: 🙄 These r price G H I.</p> <p>X: Ok la, I want this H - RM 21.9 eh, 20 cannot?<br> Dad: Cannot la. Cincai la, RM 21.<br> X: Ok la.</p> <p>(Bro took out the RM 17 slice coz he thought it's a swap and she didn't pay yet, then pack and collect RM 21.)<br> (30 min later)</p> <p>X: (Shouting) Missing one fish, why missing? I paid for 2!<br> Bro: Huh, i tot u wanna swap? U only pay me once!</p> <p>X: What the f*** r u talking? I pay u twice js nw.<br> Bro: Huh?</p> <p>Me: Yaya, she paid me once, then pay u another one.<br> Bro: Oh sorry, I didn't notice, sorry.<br> X: Damn, now I don't hv grouper to eat, n****.</p> <p>Me: Sorry, let me refund to u, RM 17.<br> X: Refund me 21. The expensive slice is missing.</p> <p>Me: 🙄 Give me the fish, I weight for u. (Weight) It's the 21 la. So i refund 17.<br> X: No, u shud refund me 17.8.</p> <p>Bro: Har? We discounted js now right? It's 17.<br> X: No, it's 17.8. N****, why do u want to tipu my money.<br> Me, dad: Ok ok, give 17.8.</p> <p>(X kept mumbling and go away.)</p> <hr> <p>She was on wheelchair quite sometimes already, also staying alone, so me n dad is more tolerate. However, tbh, that doesn't give her the right to be rude.</p> <p>The fact is she can pick cheaper fish, but she js like to target expensive fish and hope the price is cheap! How possible lah~~</p> </content> </entry> <entry> <title>Fish 064</title> <link href="https://jec.fish/fish/20210828"/> <updated>2021-08-28T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210828</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:95.41015625%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210828.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210828.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210828.jpg"> <img src="https://jec.fish/assets/img/fish/20210828.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Drizzling morning, but good start! See the big tail of grouper on the left? Supposed to cut and sell by pieces. An auntie just come and said: wow, nice tail, give me all. Then, we sold out at 8am. 🤩</p> <hr> <p>An auntie asked my dad.<br> A: Do u knw where can order fish in PJ?</p> <p>Dad: Why?<br> A: My sister at PJ doesn't dare to go out buy fish. Do u hv any fish seller friend in PJ?</p> <p>Dad: Don't hv. Buy online la.<br> A: She doesn't knw how to buy online. Do u deliver?</p> <p>Dad: So far away, no.<br> Me: We can help u Lalamove thr. The delivery fee would be ~RM25 I think.</p> <p>A: Har, so expensive. No other options ah?<br> Me: (Got, u buy and deliver yourself lo) No wor...</p> <p>A: She missed eating fish a lot. Why u dun hv fish seller friend in PJ?</p> <hr> <p>&quot;Does your dad knw anyone selling fish x at area y?&quot; is a very common question I got frm friends.</p> <p>I wonder why ppl think fish seller got fish seller friends. In fact, my dad doesn't hv any fish seller friends. Only some fish supplier contacts.</p> <p>Oh, and he feel threatened by other rivals (fish sellers) at the same pasar. So no fish friends even same area. 😂</p> <p>#同行如敌国</p> </content> </entry> <entry> <title>Fish 065</title> <link href="https://jec.fish/fish/20210904"/> <updated>2021-09-04T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210904</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:79.58984375%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210904.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210904.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210904.jpg"> <img src="https://jec.fish/assets/img/fish/20210904.jpg" alt="fish in basket"> </picture> </div><figcaption>fish in basket</figcaption></figure> <p>This week no selling, because we &quot;heard&quot; police and DBKL are coming!<br> Don't ask me how we &quot;heard&quot;. You know there are ways. 😏</p> <p>Me: Why no selling since we are legal business?<br> Dad: To avoid whatever potential troubles.</p> <p>Me: What sorta trouble we can get in oh?<br> Dad: U know... there are this new SOP requires pasar to hire RELA to take care of the crowd queue and registration.</p> <p>Me: Good SOP, hire la.<br> Dad: One RELA costs about RM 80, and we need a few.<br> Me: Then? Price ok ma, and u all sellers split the cost anyway.</p> <p>Dad: Expensive la, it's only less than 4 hours. My hourly wage is less than that.<br> Dad: Pasar is not crowded. Many still don't dare to come out.</p> <p>Dad: Also many sellers nt willing to pay. Why throw money into the sea?<br> Me: Yayaya... Many not willing to pay, including YOU.</p> <p>Dad: You get a free rest day now, not happy?</p> <hr> <p>Errr... Generally I think the SOP is a right thing to do. But I also understand the profit vs cost concerns of the pasar sellers.</p> <p>Let's say 10 sellers splitting the cost of 6 RELA, each seller needs to fork out RM 48. Not a lot from my perspective, but...<br> RM 48 could be a seller's whole morning income or a big portion of it.</p> <p>Need to work on driving down the RELA headcount or nego a better price then. That's none of my business~</p> </content> </entry> <entry> <title>Fish 066</title> <link href="https://jec.fish/fish/20210911"/> <updated>2021-09-11T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210911</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210911.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210911.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210911.jpg"> <img src="https://jec.fish/assets/img/fish/20210911.jpg" alt="fish stall"> </picture> </div><figcaption>fish stall</figcaption></figure> <p>Operate today! Successfully nego with the local authority. Instead of RELA, we hire old uncles frm the neighborhood to take care of the registration. Half the price and very responsible~ win win win. 👍</p> <hr> <p>Erm... recently me and my bro realized my dad is more forgetful after the operation. Is he becoming like goldfish??? (It's false btw, scientists proved that goldfish has &gt;3s memory 😆)</p> <p>Bro called dad: Help me tapao later.<br> Dad: Okay.</p> <p>(Dad went home without food.)</p> <p>Dad: Eh, why are you home?<br> Dad: Why u didn't ask me to tapao for u since u r home?</p> <p>Bro: What? I just called u one hour ago eh!<br> Dad: Is it? No la, u didn't.</p> <p>Bro: I did.<br> Dad: No, u didn't.<br> Bro: Check your phone log.</p> <p>Dad: ... How u insert fake record to my phone log?</p> <p>(Okay, u win 😂😂😂)</p> <p>This is js one of the few examples. Is this defined as &quot;forgotten&quot; or &quot;lost of memory&quot;? He couldn't recall that at all.</p> <p>These only happened recently. Not sure if it's aging or short term (due to the strong doses of antibiotics). Let's monitor. 😧</p> <p>Good thing is he still rmb how to win mahjong and calculate winning amount in light speed. 😆</p> <hr> <p>If one day, he is going to lost his memory, I hope he at least rmb us children. Else, send him to old folks home! 😂</p> </content> </entry> <entry> <title>Fish 067</title> <link href="https://jec.fish/fish/20210918"/> <updated>2021-09-18T00:00:00-00:00</updated> <id>https://jec.fish/fish/20210918</id> <summary>Pasar fish stories</summary> <category term="fish"/> <content type="html"><figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:100%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/fish/20210918.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/fish/20210918.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/fish/20210918.jpg"> <img src="https://jec.fish/assets/img/fish/20210918.jpg" alt="otak-otak banner"> </picture> </div><figcaption>otak-otak banner</figcaption></figure> <p>Fuiyoo, overhead ppl talking about going Klang for seafood, family outing and more. I'm both happy and worried. 😬</p> <hr> <p>We sell much less otak-otak than usual today. Here is why.</p> <p>Usually dad placed the otak-otak right next to the its banner (in photo). However, he is lazy today. 🙄</p> <p>Me: (taking out otak-otak and wanted to put next to it)<br> Dad: Aiya, no need la. Later need to put it back to freezer if nt finish selling.<br> Dad: We sell quite a while ady. Ppl knw that. See the banner so big, obvious enough liao.</p> <p>Me: Huh, u sure?<br> Dad: Ya.</p> <hr> <p>End of morning, only 2 packs of otak-otak sold.</p> <p>Dad: Eh, why do we sell so little today?<br> Me: 😤😤😤</p> <hr> <p>Actually I expected a drop of sale by no display. But didn't expect that much. 2 vs 20. 🙈</p> <p>Seeing the real things trigger ppl intention to buy, the banner is only helping, nt the main attraction.</p> <p>Not everyone notice the banner. Some might also thought the otak-otak are sold out without asking (coz we usually display if we hv).</p> <p>Good learning by earning less. Production testing failed. Padan muka! 😂</p> </content> </entry> <entry> <title>Conferences 2023</title> <link href="https://jec.fish/blog/conf-2023"/> <updated>2022-12-27T00:00:00-00:00</updated> <id>https://jec.fish/blog/conf-2023</id> <summary>Jecelyn Yeen will be speaking at these conferences 2023!</summary> <category term="blog"/> <content type="html"><p>In-person events are back! These are the conferences I will be speaking and attending at in 2023. Looking forward to meet you at these amazing events.</p> <figure><div class="pic-wrapper"> <picture class="pic" style="padding-bottom:56.4453125%"> <source media="(min-width: 501px)" srcset="https://jec.fish/assets/webp/blog/conf-2023.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/webp-500/blog/conf-2023.webp" type="image/webp"> <source media="(max-width: 500px)" srcset="https://jec.fish/assets/img-500/blog/conf-2023.jpg"> <img src="https://jec.fish/assets/img/blog/conf-2023.jpg" alt="The conferences I will be speaking at"> </picture> </div><figcaption>The conferences I will be speaking at</figcaption></figure> <p><em>[Last updated: July-2023]</em></p> <p>🙊 - Attending<br> 🎤 - Speaking</p> <ol> <li>✅ Jan - 🙊 <a href="https://www.meetup.com/munich-frontend-developers/events/290551896/">Frontend Munich New Year Meetup</a> @ Munich, Germany</li> <li>✅ Jan - 🎤 <a href="https://gdg.community.dev/events/details/google-gdg-berlin-presents-web-in-2023-meet-the-chrome-team/">GDG Berlin Meetup: Web in 2023 - Meet the Chrome Team</a> @ Berlin, Germany</li> <li>✅ Feb - 🎤 <a href="https://jsworldconference.com/">JSWorld</a> @ Amsterdam, Netherlands - <a href="https://youtu.be/6vwWBKfwTWk">watch 📺</a></li> <li>✅ Feb - 🎤 <a href="https://www.alpine-conferences.com/agent-conf-2023/">AgentConf</a> @ Dornbirn, Austria - <a href="https://youtu.be/pul2qnJFZnw">watch 📺</a></li> <li>✅ Mar - 🎤 <a href="https://london.cityjsconf.org/">CityJS</a> @ London, UK - <a href="https://youtu.be/RdiXuoMPtUA?t=4450">watch 📺</a></li> <li>✅ Apr - 🎤 <a href="http://jsday.it/">JSDay</a> @ Verona, Italy - <a href="https://vimeo.com/showcase/10333657/video/831903182">watch 📺</a></li> <li>✅ Apr - 🙊 <a href="https://www.meetup.com/munich-frontend-developers/events/292126956/">Frontend Munich April meetup</a> @ Munich, Germany</li> <li>✅ May - 🎤 <a href="https://github.com/GitNation/JavaScript-frameworks-devs-summit">JavaScript Frameworks Developers Summit</a> @ Amsterdam, Netherlands</li> <li>✅ Jun - 🎤 <a href="https://jsnation.com/">JSNation</a> @ Amsterdam, Netherlands - <a href="https://portal.gitnation.org/contents/modern-web-debugging">watch 📺</a></li> <li>✅ Jun - 🎤 <a href="https://romaniatesting.ro/speakers/jecelyn-yeen/">Romanian Testing Conference (RTC)</a> @ Cluj, Romania</li> <li>✅ Jun - 🎤 <a href="https://me.linkedin.com/posts/jecfish_saucelabs-webinar-chromedevtools-activity-7067381073101381633-jwFs">Sauce Labs meetup</a> @ Berlin, Germany - <a href="https://saucelabs.com/resources/webinar/test-easier-with-google-chrome-devtools-and-sauce-labs">watch 📺</a></li> <li>✅ Jun - 🎤 <a href="https://conference.eurostarsoftwaretesting.com/">EuroSTAR Software Testing Conference</a> @ Antwerp, Belgium</li> <li>✅ Jun - 🎤 <a href="https://rsvp.withgoogle.com/events/ioconnect-amsterdam">I/O Connect Amsterdam</a> @ Amsterdam, Netherlands</li> <li>✅ Jun - 🎤 <a href="https://rsvp.withgoogle.com/events/ioconnect-bengaluru">I/O Connect Bengaluru</a> @ Bengaluru, India</li> <li>✅ Sep - 🎤 <a href="https://smashingconf.com/freiburg-2023">SmashingConf</a> @ Freiburg, Germany - <a href="https://youtu.be/ssrKTDmDo4c?si=kTQHyA50Wv2gLvj2">watch 📺</a></li> <li>✅ Sep - 🙊 <a href="https://www.meetup.com/munichjs-user-group/events/295733864/">MunichJS September meetup</a> @ Munich, Germany</li> <li>✅ Oct - 🎤 <a href="https://dev.events/conferences/dev-fest-nantes-nantes-9-2023">DevFest Nantes</a> @ Nantes, France - <a href="https://youtu.be/CNrTlXOtcnE?si=FWMCYLDfdixi0MKv">watch 📺</a></li> <li>✅ Oct - 🎤 <a href="https://biznagafest.com/">BiznagaFest Malaga</a> @ Malaga, Spain</li> <li>✅ Nov - 🎤 <a href="https://fequan.com/2023/">FEDAY (前端日) Conference</a> @ Hangzhou, China - <a href="https://www.bilibili.com/video/BV1mG411i7f2">watch 📺</a></li> <li>✅ Nov - 🙊 <a href="https://www.meetup.com/munichjs-user-group/events/296532615/">MunichJS November meetup</a> @ Munich, Germany</li> <li>✅ Nov - 🎤 <a href="https://epichey.dev/">EpicHey! Conference</a> @ Lisbon, Portugal</li> <li>✅ Dec - 🎤 <a href="https://www.facebook.com/gdggeorgetown/">DevFest Georgetown</a> @ Penang, Malaysia</li> </ol> <div class="comments"> <p> <em>Have something to say? Leave me comments on Twitter 👇🏼</em> </p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr"> <a style="word-break: break-all;" href="https://twitter.com/jecfish/status/1607750323598413825"> twitter.com/jecfish/status/1607750323598413825 </a> </p></blockquote> <p> <em>Follow my writing: <a class="twitter-follow-button" style="word-break: break-all;" href="https://twitter.com/jecfish" data-size="large">@jecfish</a></em> </p> </div> </content> </entry> </feed>