<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: platformOS</title>
    <description>The latest articles on Forem by platformOS (@platformos).</description>
    <link>https://forem.com/platformos</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png</url>
      <title>Forem: platformOS</title>
      <link>https://forem.com/platformos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/platformos"/>
    <language>en</language>
    <item>
      <title>2023 — A Year of Innovation and Growth for DocsKit</title>
      <dc:creator>Tamas Simon</dc:creator>
      <pubDate>Fri, 09 Feb 2024 18:43:04 +0000</pubDate>
      <link>https://forem.com/platformos/2023-a-year-of-innovation-and-growth-for-docskit-1fea</link>
      <guid>https://forem.com/platformos/2023-a-year-of-innovation-and-growth-for-docskit-1fea</guid>
      <description>&lt;p&gt;Reflecting on the incredible journey and growth we've experienced together in 2023, we can celebrate many significant milestones. The launch of our documentation platform, DocsKit is just the beginning, here's a summary of all the exciting highlights.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DocsKit is open-source, licensed under the terms of the Creative Commons Attribution 4.0 International License. Separately, platformOS offers a range of services, including setup, hosting, support, accessibility, sustainability, SEO, and performance packages.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  User research
&lt;/h2&gt;

&lt;p&gt;When we developed the award-winning &lt;a href="https://documentation.platformos.com/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;platformOS Developer Portal&lt;/a&gt;, we conducted extensive research over several years into all aspects of documentation. This included its target audience, editorial workflow, features and functionality, accessibility, sustainability, and much more. We also delved into existing research and developed an &lt;strong&gt;empathetic understanding of how end-users, technical writers, editors, and contributors interact with documentation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Using all this experience and research, we set out to build our own documentation solution to share with the platformOS partner community. Our goal was to ensure that a &lt;strong&gt;customizable solution&lt;/strong&gt; covers all their documentation needs in a manner consistent with &lt;strong&gt;industry standards and best practices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Of course, as always, we started with research again, conducting a competitive analysis and held discussions with documentation experts to shape our understanding of the ideal documentation solution for their needs. We continued user research at different development phases and haven't stopped. We hope that through this approach, we can consistently provide a documentation solution that exceeds our partners’ and clients' evolving needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development and launch
&lt;/h2&gt;

&lt;p&gt;Based on previous and new research, we began developing the &lt;strong&gt;architecture and features&lt;/strong&gt; of DocsKit. Integrating Gatsby, GitHub, and platformOS, we built a solution that can function both as a standalone documentation site hosted on platformOS or as part of the platformOS modules ecosystem. This integration means it works seamlessly with other platformOS modules and kits. For example, it utilizes the platformOS DesignKit as its design system. Additionally, thanks to its Gatsby component, &lt;strong&gt;DocsKit can integrate with any of the 3000+ Gatsby plugins&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We &lt;strong&gt;launched&lt;/strong&gt; DocsKit with a strong selection of features, enabling the creation of a complete documentation site, including an editorial workflow and automated CI/CD. During our soft launch, we reached out to partners for testing and feedback, and we have since incorporated all the feedback we received.&lt;/p&gt;

&lt;p&gt;For instance, we made the &lt;a href="https://github.com/Platform-OS/docskit-landing?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;GitHub repository for our DocsKit site public&lt;/a&gt;. Built entirely on DocsKit, this site's code and automation are now open for exploration.&lt;/p&gt;

&lt;p&gt;In preparation for the launch of our &lt;a href="https://docskit.platformos.com/docs-as-code-course/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;Docs as Code educational series&lt;/a&gt;, we developed the &lt;strong&gt;course module&lt;/strong&gt; as an extension to DocsKit. This module allows you to enhance your documentation site with video training and courses, along with downloadable content, and as a platformOS module, it can be used for any platformOS site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;In parallel with development, we've also crafted the theme included in the DocsKit package to ensure it offers a &lt;strong&gt;fully customizable design, featuring an accessible and responsive theme&lt;/strong&gt;. Key features encompass SEO readiness, responsiveness, optimized images, automatically generated navigation, table of contents, and breadcrumbs. The theme leverages Gatsby's Shadowing API, empowering users to implement a custom color scheme. Everything in the DocsKit theme adheres to the naming and format conventions of platformOS's public design system, the platformOS DesignKit. Furthermore, DocsKit provides the flexibility to use the Shadowing API for extending or overriding built-in components, enabling a high level of customization.&lt;/p&gt;

&lt;p&gt;Out of the box, DocsKit offers a diverse selection of &lt;strong&gt;pre-designed custom components&lt;/strong&gt; for use in MDX pages, enhancing the ease and efficiency of your documentation creation. Users also have the freedom to create unique versions of components, like the Button, by copying the original component file and adding customizations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gxw7yt9pacvl5c53iqu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gxw7yt9pacvl5c53iqu.gif" alt="DocsKit custom components" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To assist DocsKit users in conceptualizing the content for their documentation site's landing page, we have introduced the &lt;a href="https://docskit.platformos.com/articles/landing-page-content-collector/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;Landing Page Content Collector&lt;/a&gt; — a FigJam brainstorming board tailored for non-designers. This board simplifies the process of promoting content from the comprehensive documentation to the landing page in a structured format. As of the time of writing this article, the user base has grown to a commendable 630 users, and we are delighted to learn that our Landing Page Content Collector is proving valuable to its intended audience and likely beyond. While specifically crafted to meet the needs of DocsKit users, this versatile board can be widely utilized for brainstorming any landing page content or for designing page layouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge sharing
&lt;/h2&gt;

&lt;p&gt;We believe that sharing knowledge is crucial for growth and innovation. Having gained a thorough understanding of documentation, we are happy to share our insights with DocsKit users and the broader documentation community.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DocsKit Developer Portal:&lt;/strong&gt; We began developing the &lt;a href="https://docskit.platformos.com/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;DocsKit Developer Portal&lt;/a&gt; simultaneously with the launch of DocsKit. Incorporating insights from research, feedback, and experiences with clients and partners, we are organically expanding our developer portal to become the most reliable resource for all DocsKit users. This year, our plan is to incorporate additional useful, practical information and real-world examples, along with offering straightforward onboarding through a DocsKit sandbox.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Articles:&lt;/strong&gt; Our &lt;a href="https://docskit.platformos.com/articles/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;articles section&lt;/a&gt; covers a range of topics, from Docs as Code and documentation standards to best practices in technical writing. It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Educational resources related to the core principles of technical writing.&lt;/li&gt;
&lt;li&gt;DocsKit news, updates, and announcements.&lt;/li&gt;
&lt;li&gt;Invitations to courses and webinars.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Case studies:&lt;/strong&gt; Our DocsKit site now boasts a dedicated &lt;a href="https://docskit.platformos.com/case-studies/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;case studies section&lt;/a&gt;, offering valuable insights into the practical applications of DocsKit. We present two distinct types of case studies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature case studies: Illustrating how specific DocsKit features address common challenges, complete with practical examples and code snippets.&lt;/li&gt;
&lt;li&gt;Site case studies: Showcasing fully implemented DocsKit sites.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;University course:&lt;/strong&gt; We are currently working on a course curriculum for the University of Szeged, which will cover the entire product development life cycle for a PaaS application. platformOS serves as the main example, with DocsKit illustrating documentation, accessibility, and sustainability aspects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docs as Code Fundamentals course:&lt;/strong&gt; Our &lt;a href="https://docskit.platformos.com/docs-as-code-course/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;Docs as Code Fundamentals course&lt;/a&gt;, starting in January 2024, provides a comprehensive understanding of Docs as Code. Ideal for beginners and those refining their skills, the course covers the basics to the editorial workflow. &lt;a href="https://docskit.platformos.com/user/register?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;Sign up for the course&lt;/a&gt; to access videos for each lesson and participate in interactive webinars for deeper engagement with the instructor and our team.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Partnerships
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DocsKit is now accessible for any platformOS Partner.&lt;/strong&gt; Furthermore, we are actively collaborating to establish a &lt;strong&gt;network of specialized partners&lt;/strong&gt; focused on DocsKit development for documentation sites. This initiative aims to provide clients with the option to choose a partner with expertise in DocsKit when they decide to create a DocsKit site.&lt;/p&gt;

&lt;p&gt;For clients interested in directly working with the DocsKit team, we've initiated &lt;a href="https://calendar.thetrackapp.com/colin-frost/5939f8d5-c49e-4626-9e70-3f51aa599dcd"&gt;discovery calls&lt;/a&gt; where we introduce key features and benefits, compare DocsKit with open-source and SaaS solutions, discuss integrations, showcase example DocsKit powered sites, and outline pricing.&lt;/p&gt;

&lt;p&gt;These initiatives aim to empower individuals and organizations with the knowledge they need for effective and sustainable documentation practices. Stay connected with us for the latest updates and opportunities to enhance your documentation skills!&lt;/p&gt;

&lt;p&gt;&lt;a rel="canonical" href="https://docskit.platformos.com/articles/2023-summary/?utm_source=blog&amp;amp;utm_medium=devto&amp;amp;utm_campaign=dkawareness"&gt;Originally published at docskit.platformos.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>technicalwriting</category>
      <category>docsascode</category>
      <category>developerplatform</category>
    </item>
    <item>
      <title>Background images in TailwindCSS - the clean and easy way</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Thu, 14 Oct 2021 12:12:26 +0000</pubDate>
      <link>https://forem.com/platformos/background-images-in-tailwindcss-the-clean-and-easy-way-gho</link>
      <guid>https://forem.com/platformos/background-images-in-tailwindcss-the-clean-and-easy-way-gho</guid>
      <description>&lt;p&gt;My biggest issue with TailwindCSS was the ugly way of using background images with it. I had to inline &lt;code&gt;background-url&lt;/code&gt; property and it was not clean. And I know I was not alone, because other frontend developers said the same thing. It looked similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-accent-dark bg-cover"&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"background-image: url({{ 'images/home/hero.jpg' | asset_url }})"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note: We use liquid to generate url to an image placed on CDN. &lt;/p&gt;

&lt;p&gt;Couple days ago, when I was doing six different themes in TailwindCSS using six different configs (I will report my findings in another article) I discovered that it can be simplified. Because CSS file is also on CDN, and the background-image can be customized in TailwindCSS config (&lt;a href="https://tailwindcss.com/docs/background-image#background-images" rel="noopener noreferrer"&gt;read more&lt;/a&gt;), it can be replaced with simple &lt;code&gt;bg-hero&lt;/code&gt; after adding its definition into theme extend section:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;backgroundImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url('../images/home/hero.jpg')&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, our hero element HTML looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-accent-dark bg-cover bg-hero"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And I think its much cleaner! Reading documentation paid off :)&lt;/p&gt;
&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/using-webp-in-your-existing-webpage-809" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Using WebP in Your Existing Webpage&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 17 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#design&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;




&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/optimizing-images-for-the-web-18gc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Optimizing Images For The Web&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Apr 24 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#node&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-tips-on-preserving-website-speed-5g0c" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Tips on Preserving Website Speed&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 24 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>beginners</category>
    </item>
    <item>
      <title>We Are Switching From TestCafe to CodeceptJS – Here’s Why</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Tue, 28 Sep 2021 08:02:24 +0000</pubDate>
      <link>https://forem.com/platformos/we-are-switching-from-testcafe-to-codeceptjs-here-s-why-39ml</link>
      <guid>https://forem.com/platformos/we-are-switching-from-testcafe-to-codeceptjs-here-s-why-39ml</guid>
      <description>&lt;p&gt;We have been using and promoting TestCafe at platformOS for the past couple of years with great success. Because a lot of people will write tests and maintain them over a long time, an End-to-End framework has to come with some specific requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Easy to remember and type out API&lt;/li&gt;
&lt;li&gt;Good waiting mechanisms (for XHR requests, animations)&lt;/li&gt;
&lt;li&gt;Extendibility, page object support, helpers support&lt;/li&gt;
&lt;li&gt;Good search in documentation to quickly reference less used APIs&lt;/li&gt;
&lt;li&gt;Run properly in Docker and/or GitHub Actions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TestCafe is scoring high on the above areas, I would say averaging around 7.5/10, which means there is still room for improvement.&lt;/p&gt;

&lt;p&gt;Even though we have been happy with TestCafe, last year when I stumbled upon a new contender, CodeceptJS, I decided to give it a shot on our &lt;a href="https://documentation.platformos.com/"&gt;documentation&lt;/a&gt; and &lt;a href="https://www.platformos.com/"&gt;marketing&lt;/a&gt; sites. It delivered excellent developer performance. It was enough to dive deeper into its documentation and expand our test suites to include some more test cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Test API
&lt;/h2&gt;

&lt;p&gt;Very often when writing TestCafe tests, we had to resort to vanilla JS and DOM operations. One of the most frustrating examples was to get some text from an element and then compare it to another. It was too much work and I never could see a reason why TestCafe had no API for that. CodeceptJS has a lot more API helpers to avoid these complications and diverging into vanilla JS. Below, I give you some examples of TestCafe scenarios converted to CodeceptJS ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking if correct breadcrumbs links are present on a page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TestCafe&lt;/span&gt;
&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Breadcrumbs are showing up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigateTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api-reference/liquid/introduction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.breadcrumbs a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Documentation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.breadcrumbs a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API Reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.breadcrumbs a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Introduction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// CodeceptJS&lt;/span&gt;
&lt;span class="nx"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Are showing up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;I&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amOnPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api-reference/liquid/introduction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;see&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Documentation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.breadcrumbs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;see&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API Reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.breadcrumbs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;see&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Introduction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.breadcrumbs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;h4&gt;
  
  
  If this is interesting to you &lt;a href="https://www.platformos.com/blog/post/we-are-switching-from-testcafe-to-codeceptjs-here-s-why"&gt;read rest of this article on our blog&lt;/a&gt;.
&lt;/h4&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/platformos-documentation-site-webpack-setup-93l" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;platformOS Documentation Site Webpack Setup&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Dec 4 '20 ・ 6 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#platformos&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;




&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-performance-tips-for-your-next-project-2bdm" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Performance Tips for Your Next Project&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 29 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/should-you-always-care-about-your-website-size-2jcc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Should You Always Care about Your Website Size?&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 12 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>How We sped Up Our Webpack (TailwindCSS) 7 times!</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Wed, 07 Apr 2021 17:20:31 +0000</pubDate>
      <link>https://forem.com/platformos/how-we-sped-up-our-webpack-tailwindcss-7-times-1c05</link>
      <guid>https://forem.com/platformos/how-we-sped-up-our-webpack-tailwindcss-7-times-1c05</guid>
      <description>&lt;p&gt;In the last article about build speed optimization, we described how we went from 64 seconds to 17 seconds on our Webpack build (measured on GithubActions, a pretty slow environment CPU-wise). Just as we did it and managed to write an article about it to share the knowledge, something amazing happened: TailwindCSS/JIT.&lt;/p&gt;

&lt;p&gt;JIT (short from Just In Time) for TailwindCSS is a much more performant way of generating the TailwindCSS output file. Instead of generating a big (sometimes 10MB+) CSS file and then using PurgeCSS to remove unnecessary classes, it only generates what is needed in the first place. This makes PurgeCSS and many other speed optimization techniques in TailwindCSS unnecessary. It is very fast no matter what config you use, and the output file size is still optimal.&lt;/p&gt;

&lt;p&gt;We jumped into experimenting with JIT as soon as it got a beta release, so there were some bugs, but now we consider it good enough for production, hence this article.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Result: Webpack build took &lt;strong&gt;8.9&lt;/strong&gt; seconds (down from 17)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a nice side-effect of using JIT, our TailwindCSS config became much smaller because we don't need to disable modules, override theme colors, spacing, etc. Now everything is taken care of by JIT during runtime. It is so fast that development became a breeze.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailwindcss.com/docs/just-in-time-mode#enabling-jit-mode"&gt;Read more on JIT in the official TailwindCSS documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/platformos-documentation-site-webpack-setup-93l" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;platformOS Documentation Site Webpack Setup&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Dec 4 '20 ・ 6 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#platformos&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-performance-tips-for-your-next-project-2bdm" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Performance Tips for Your Next Project&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 29 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/should-you-always-care-about-your-website-size-2jcc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Should You Always Care about Your Website Size?&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 12 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>performance</category>
      <category>webpack</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How We Sped Up Our Webpack (TailwindCSS) Build By 74%</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Mon, 22 Mar 2021 10:23:55 +0000</pubDate>
      <link>https://forem.com/platformos/how-we-sped-up-our-webpack-tailwindcss-build-by-57-1hci</link>
      <guid>https://forem.com/platformos/how-we-sped-up-our-webpack-tailwindcss-build-by-57-1hci</guid>
      <description>&lt;p&gt;Since Github Actions became a thing, I became interested in saving precious seconds in our plan by optimizing assets build time. As long as it didn't exceed 60 seconds, there was little motivation, but once it did, I said enough is enough.&lt;/p&gt;

&lt;p&gt;Our build step runs two commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm ci&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use babel and TailwindCSS. Using simple methods, I discovered that CSS and JS take about the same time to build. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Benchmark: Webpack build took &lt;strong&gt;64&lt;/strong&gt; seconds, whole build took &lt;strong&gt;91&lt;/strong&gt; seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Replacing babel-loader + terser with &lt;a href="https://github.com/privatenumber/esbuild-loader" rel="noopener noreferrer"&gt;esbuild loader&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The first thing I did was replace &lt;code&gt;babel-loader&lt;/code&gt; and Terser (minification tool) with &lt;a href="https://github.com/privatenumber/esbuild-loader" rel="noopener noreferrer"&gt;esbuild-loader&lt;/a&gt;. This made our JS compile around 12 times faster, it went down to 1.4 seconds. It was a good start.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Result: Webpack build took &lt;strong&gt;40&lt;/strong&gt; seconds (down from 64), whole build took &lt;strong&gt;65&lt;/strong&gt; seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Configuring Tailwind
&lt;/h2&gt;

&lt;p&gt;The second optimization had to do something with CSS because that's where most of the build time now lay. I visited the TailwindCSS documentation to find out how to decrease build size.&lt;/p&gt;

&lt;p&gt;The template we base our projects on uses a style guide with predefined colors, sizes, etc., so we don't need some of the configuration from TailwindCSS. Knowing this, here's what I did:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Moved &lt;code&gt;colors&lt;/code&gt; configuration outside &lt;code&gt;extend&lt;/code&gt;, which disabled all the built-in colors from the build. This made a huge difference in development file size (10.5MB → 4MB).&lt;/li&gt;
&lt;li&gt;Moved &lt;code&gt;spacing&lt;/code&gt; configuration outside &lt;code&gt;extend&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Disabled corePlugins&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I had to bring some of these back because we used them in a couple of places. After these changes, I considered TailwindCSS optimization done. &lt;/p&gt;

&lt;p&gt;The file size of development CSS went down from 10.5MB to 3.9MB, which is a big deal (depending on the developer's connection speed) if you are sending your CSS on every CSS change. It does not happen often when working with TailwindCSS, but it was still a welcome improvement for everyone using our &lt;code&gt;pos-cli sync&lt;/code&gt; command.  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Result: Webpack build took &lt;strong&gt;26&lt;/strong&gt; seconds (down from 40), whole build took &lt;strong&gt;49&lt;/strong&gt; seconds.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Read about TailwindCSS optimization in their documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/docs/optimizing-for-production" rel="noopener noreferrer"&gt;https://tailwindcss.com/docs/optimizing-for-production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/docs/configuration#theme" rel="noopener noreferrer"&gt;https://tailwindcss.com/docs/configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Disabling PostCSS processing
&lt;/h2&gt;

&lt;p&gt;The last step was to disable PostCSS processing of CSS pulled in from &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before, it was pretty naive — everything went through all the CSS loaders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/(\.css)$/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;MiniCssExtractPlugin.loader&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;loader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;css-loader'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;},&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postcss-loader'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="pi"&gt;],&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I split it into two so that &lt;code&gt;node_modules&lt;/code&gt; are processed only by &lt;code&gt;css-loader&lt;/code&gt;, and application CSS is processed by PostCSS first.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/(\.css)$/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/node_modules/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;MiniCssExtractPlugin.loader&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;loader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;css-loader'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;],&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/(\.css)$/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/node_modules/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
    &lt;span class="nv"&gt;MiniCssExtractPlugin.loader&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;loader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;css-loader'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;},&lt;/span&gt;
    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postcss-loader'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="pi"&gt;],&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;Result: Webpack build took &lt;strong&gt;17&lt;/strong&gt; seconds (down from 26), whole build took &lt;strong&gt;39&lt;/strong&gt; seconds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  That's all :)
&lt;/h3&gt;

&lt;p&gt;This journey was one of those creative ones where you fiddle around, find gains in various places, and they add up. In this case, they added up to 52 seconds of savings for every build. And we build a lot, so this improves our developer experience and lowers the cost of CI.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Webpack build time (seconds)&lt;/th&gt;
&lt;th&gt;Whole build time (seconds)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;At the start&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Using esbuild loader&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuring Tailwind&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disabling PostCSS processing&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the following article, I will report how development speedup is going because the TailwindCSS team just released the experimental &lt;a href="https://github.com/tailwindlabs/tailwindcss-jit" rel="noopener noreferrer"&gt;TailwindCSS JIT&lt;/a&gt;, which might improve live development even further.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/platformos-documentation-site-webpack-setup-93l" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;platformOS Documentation Site Webpack Setup&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Dec 4 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#platformos&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;




&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-performance-tips-for-your-next-project-2bdm" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Performance Tips for Your Next Project&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 29 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/should-you-always-care-about-your-website-size-2jcc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Should You Always Care about Your Website Size?&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 12 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>performance</category>
      <category>webpack</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Build and Host Anything with platformOS 
</title>
      <dc:creator>Diana Lakatos</dc:creator>
      <pubDate>Tue, 16 Feb 2021 14:07:02 +0000</pubDate>
      <link>https://forem.com/platformos/build-and-host-anything-with-platformos-1kgo</link>
      <guid>https://forem.com/platformos/build-and-host-anything-with-platformos-1kgo</guid>
      <description>&lt;p&gt;&lt;em&gt;A getting started guide using an open source ecommerce Marketplace Template to learn from.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What is platformOS? 
&lt;/h1&gt;

&lt;p&gt;platformOS (pOS) is a model-based application development platform.&lt;/p&gt;

&lt;p&gt;It's like Firebase and Heroku had a baby, but with a lot more flexibility and power.  It's infrastructure agnostic, allowing your solutions to be deployed to &lt;a href="https://aws.amazon.com/"&gt;AWS&lt;/a&gt;, &lt;a href="https://cloud.google.com"&gt;GCP&lt;/a&gt; and (soon) &lt;a href="https://azure.microsoft.com"&gt;Azure&lt;/a&gt; with all your DevOps taken care of.&lt;/p&gt;

&lt;p&gt;Aimed at front-end developers and site builders, it supports any front-end development framework such as &lt;a href="https://reactjs.org/"&gt;React&lt;/a&gt;, &lt;a href="https://vuejs.org/"&gt;Vue.js&lt;/a&gt;, &lt;a href="https://angular.io/"&gt;Angular&lt;/a&gt;, &lt;a href="https://getbootstrap.com/"&gt;Bootstrap&lt;/a&gt;, and others. It supports a flexible and limitless API-driven development approach while providing enterprise grade DevOps-in-a-Box to spin up a hosted site and go live in minutes. You can build any solution including community sites, membership based ecommerce club sites, product marketplaces with complex logistics and payment integration, service marketplaces, advanced auctions sites, QA and forums — even stand-alone SaaS applications.  (We already have a &lt;a href="https://www.platformos.com/blog/post/platformos-master-partner-siteglide-ranked-number-1-best-digital-experience-platform-dxp-for-small-business"&gt;G2.com&lt;/a&gt; #1 ranked Digital Experience Platform built on platformOS)&lt;/p&gt;

&lt;p&gt;This article will help you get started with pOS using our Product and Community Marketplace template — a fully functional marketplace built on pOS with features like user onboarding, product/service listings and ads, add-to-cart and checkout process, including online payment via Stripe.&lt;/p&gt;

&lt;p&gt;Following the tutorial, you can deploy this code within minutes to have a list of working features and then be able to start customizing the back- and front-end code (without any limits!). You can preview the demo marketplace solution template at &lt;a href="https://getmarketplace.co"&gt;getmarketplace.co&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RQZTkD9e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/je7lsn2a43wltblz7fca.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RQZTkD9e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/je7lsn2a43wltblz7fca.jpg" alt="Screen shot of the marketplace demo site"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What you'll need 
&lt;/h1&gt;

&lt;p&gt;To get started, you'll need to register on the &lt;a href="https://partners.platformos.com"&gt;platformOS Partner Portal&lt;/a&gt;, an online interface where you can create, manage and configure your sites (called Instances). &lt;/p&gt;

&lt;p&gt;To register on the Partner Portal, go to&lt;a href="https://partners.platformos.com/accounts/sign_up"&gt;  https://partners.platformos.com/accounts/sign_up&lt;/a&gt;, complete the form or use your GitHub or Google account. Once registered, you will get an email verification. Click on the Accept verification link to activate your account.&lt;/p&gt;

&lt;h1&gt;
  
  
  Setup and configuration
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Step 1: Install the pos-cli
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://documentation.platformos.com/developer-guide/pos-cli/pos-cli"&gt;pos-cli&lt;/a&gt; is a command line interface that helps you deploy configuration files and assets to your pOS site. You will need a recent version of NPM (Node Package Manager) that comes with Node.js installed on your computer to install the pos-cli.&lt;/p&gt;

&lt;p&gt;Once you have Node.js installed, start your command-line tool (for example, Terminal on a Mac, or Git Bash on Windows), and enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g @platformos/pos-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your Node.js is installed for all users you might need to use sudo to install npm packages globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo npm install -g @platformos/pos-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the following command to test the pos-cli:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pos-cli -v
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the pos-cli has been installed correctly, running this command displays the version of your pos-cli. If the pos-cli hasn't been installed, running this command gives a command not found error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create Instance
&lt;/h2&gt;

&lt;p&gt;To be able to deploy your site, you have to create an Instance on the Partner Portal. Instances have a URL, and they represent different development environments, like staging or production. We recommend creating a staging environment for going through the steps in this tutorial.&lt;/p&gt;

&lt;p&gt;On the Partner Portal, in the menu on the left under Create, select Instance, and fill in the form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UeadfWKd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1s8t1plf808gp2yqmqdu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UeadfWKd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1s8t1plf808gp2yqmqdu.png" alt="Instance creation form"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;The name of your Instance&lt;/td&gt;
&lt;td&gt;mymarketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;Enter up to 5 tags (optional). You can use tags to group your Instances, for example by project or client.&lt;/td&gt;
&lt;td&gt;marketplace, test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partner&lt;/td&gt;
&lt;td&gt;Select the Partner the Instance will belong to.&lt;/td&gt;
&lt;td&gt;MarketplacePartner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Center&lt;/td&gt;
&lt;td&gt;Select an endpoint (staging or production).&lt;/td&gt;
&lt;td&gt;STAGING&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Select the &lt;strong&gt;Staging/Unbilled Billing Plan&lt;/strong&gt; that appears, and click on the Create button. &lt;/p&gt;

&lt;p&gt;Developing and deploying to a staging Instance is free. Once the Instance is created (in a couple of minutes at most), you'll get an email with a link to your Instance and other useful information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add Instance to pos-cli
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;WARNING: Make sure you remember your Partner Portal account email and password --- you will need them to authenticate your environments. If you logged in using Google or Github, go to the Instance details view in Partner Portal where you will find the pos-cli command ready for copy and paste. It shows all the parameters you need, so in this case, you won't need to remember your password.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On your local environment create a new directory and change to it. This is where you will put the codebase for your marketplace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir marketplace
cd marketplace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your Instance to the pos-cli. This will ensure that you can use the pos-cli to download your codebase, and sync or deploy to the Instance.&lt;/p&gt;

&lt;p&gt;Use the pos-cli env add command and authenticate with your Partner Portal credentials.&lt;/p&gt;

&lt;p&gt;TIP: You can copy this command pre-filled with your email and Instance URL from your Instance page on the Partner Portal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6iBdNJta--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uk6m2ghxzcd9c3nr9m8k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6iBdNJta--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uk6m2ghxzcd9c3nr9m8k.png" alt="pos-cli commande cheat sheet"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pos-cli env add [YOUR_ENV_NAME] --email [YOUR_EMAIL] --url [YOUR_INSTANCE_URL]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pos-cli env add staging --email john.smith@example.com --url https://mymarketplace.staging.oregon.platform-os.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A message "Environment [your Instance URL] as staging has been added successfully." is displayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Clone the repository
&lt;/h2&gt;

&lt;p&gt;Create your codebase by cloning our marketplace GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pos-cli init --url &amp;lt;https://github.com/mdyd-dev/product-marketplace-template.git&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Build assets
&lt;/h2&gt;

&lt;p&gt;Install the marketplace package and any packages that it depends on then build it using npm commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Deploy
&lt;/h2&gt;

&lt;p&gt;To be able to display your marketplace on your site, you have to deploy your codebase using the pos-cli deploy command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pos-cli deploy [YOUR_ENV_NAME]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pos-cli deploy staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A progress indicator shows that the deployment is in progress, and once it finishes, a "Deploy succeeded after [time]" is displayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Set up your marketplace
&lt;/h2&gt;

&lt;p&gt;To access the admin panel of your marketplace:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Register a user with this email address: &lt;code&gt;admin@example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Enter the Admin section from the main menu.&lt;/li&gt;
&lt;li&gt; Go to the Marketplace Setup section.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Congratulations, you have successfully created and deployed your first pOS site. Visit your Instance URL to check it out. &lt;/p&gt;

&lt;h1&gt;
  
  
  Next steps 
&lt;/h1&gt;

&lt;p&gt;Visit the pOS &lt;a href="https://documentation.platformos.com/"&gt;documentation&lt;/a&gt; to explore all that you can do with pOS. Follow step-by-step &lt;a href="https://documentation.platformos.com/get-started"&gt;Get Started&lt;/a&gt; tutorials for beginners, or delve into more advanced topics using the &lt;a href="https://documentation.platformos.com/developer-guide"&gt;Developer Guide&lt;/a&gt;. To meet and learn from fellow developers developing solutions on pOS, join the &lt;a href="https://documentation.platformos.com/community"&gt;platformOS Community&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>platformos</category>
      <category>devops</category>
    </item>
    <item>
      <title>The platformOS UX/UI Process</title>
      <dc:creator>Mónika Alföldi-Zörgő</dc:creator>
      <pubDate>Thu, 21 Jan 2021 11:00:49 +0000</pubDate>
      <link>https://forem.com/platformos/the-platformos-ux-ui-process-32b2</link>
      <guid>https://forem.com/platformos/the-platformos-ux-ui-process-32b2</guid>
      <description>&lt;p&gt;We are not a UX/UI agency, but there’s consensus at platformOS that User Experience and User Interface Design are integral part of our processes.  Although UX/UI teams usually choose from the same pool of methods and tools, no two processes are the same. This article describes how we do things at platformOS including UX/UI research and design, communication and learnings.&lt;/p&gt;

&lt;h1&gt;
  
  
  People and tools
&lt;/h1&gt;

&lt;p&gt;We believe that one of the main strengths of our UX/UI process is teamwork. Our team consists of a UX researcher, a UX designer and a UI designer. &lt;br&gt;
We have always &lt;a href="https://www.youtube.com/watch?v=s4cMXOtUkUY&amp;amp;feature=youtu.be"&gt;worked remotely&lt;/a&gt;: we use &lt;a href="https://zoom.us/"&gt;Zoom&lt;/a&gt; for our calls, &lt;a href="https://www.figma.com/"&gt;Figma&lt;/a&gt; for everything UX/UI related, and &lt;a href="https://miro.com/"&gt;Miro&lt;/a&gt; for remote workshops.&lt;/p&gt;

&lt;h1&gt;
  
  
  First steps first
&lt;/h1&gt;

&lt;p&gt;We ask the client to fill out our Strategic and design brief document. It contains basic questions about their challenges, strategic objectives, unique value propositions, history, purpose, problems they want to solve, their target audience, and competition. Depending on the project complexity and how detailed answers we get in the homework, the actual kick-off can be done in a maximum 3-hour call with the main stakeholders. Here we break the ice, go through the answers the client gave, and make an inventory of the client’s resources. &lt;br&gt;
We also agree on the processes we would like to use: means and frequency of communication, who to reach out to for various resources (copy, design guide, etc.) and the project timeline.&lt;/p&gt;

&lt;h1&gt;
  
  
  Research phase
&lt;/h1&gt;

&lt;h2&gt;
  
  
  User interviews
&lt;/h2&gt;

&lt;p&gt;Organizing and conducting the interviews might be time consuming but it almost certainly will be worth the time and effort: UX only makes sense if you know who you are designing for. We can only solve our customer's problems by design if we understand their user's goals and the context of use. This is a key moment for a successful project.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Learn” the client and the product
&lt;/h2&gt;

&lt;p&gt;From the UX point of view we study all the resources we have from the client to learn their vocabulary, understand their way of thinking and get in the same boat. &lt;br&gt;
As for UI design, we study the brand guide, the current site if it is a redesign, and create an inspirational mood board with pictures and illustrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get to know the competition
&lt;/h2&gt;

&lt;p&gt;We get a full picture of the features, strengths and weaknesses of competitors, to be able to place the product correctly. It also helps us to understand the unique value proposition of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deliverables
&lt;/h2&gt;

&lt;p&gt;We visualize and analyze the research findings in an &lt;strong&gt;affinity map&lt;/strong&gt;. It shows us the primary needs and current challenges of the users. This visualization has proven to be useful not only because it makes it easy to present and understand data, but it will still be easily scannable months later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CeyoBGWC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/tibhxig8mfgic9jon2ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CeyoBGWC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/tibhxig8mfgic9jon2ui.png" alt="Affinity map"&gt;&lt;/a&gt;&lt;em&gt;The affinity map we created based on our research for the pOS Community site&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Based on the research findings and the interviews we also identify the differences in the target group and summarize them in &lt;strong&gt;user personas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iaSq0z8Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/mxilfptxn4vbkerrs0ry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iaSq0z8Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/mxilfptxn4vbkerrs0ry.png" alt="User persona template"&gt;&lt;/a&gt;&lt;em&gt;Our template for user personas&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We like to keep all the deliverables in the same &lt;strong&gt;Figma&lt;/strong&gt; file: this way we literally have everything in one place. It’s not only good for our team but also makes it easier for the customers to follow the project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dJnVzIpd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/axwem4e24aw4ecry6eyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dJnVzIpd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/axwem4e24aw4ecry6eyq.png" alt="Project structure in Figma"&gt;&lt;/a&gt;&lt;em&gt;The typical structure of a project’s Figma file&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  UX Design
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Journeys and IA
&lt;/h2&gt;

&lt;p&gt;platformOS is a limitless development platform that gives you the flexibility to customize every aspect of your digital build. This also means that our clients often require complex features and flows. It is essential to discover and discuss all &lt;strong&gt;user journeys&lt;/strong&gt; in detail together with the client. We always start with the main and the most complex journey, and then add all the others in detail. In many sites that we work on (many to many marketplaces, community marketplaces, community hubs) user journeys often cross each other so we use different colors to reflect the journeys of different target groups. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9vj5PBit--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/hb2e5v1aylivp4g4dw4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9vj5PBit--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/hb2e5v1aylivp4g4dw4y.png" alt="User flows"&gt;&lt;/a&gt;&lt;em&gt;3 user journeys that make up the main flow of &lt;a href="https://ondehero.com/"&gt;OndeCare&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is also the first point where we like to discuss the project with developers. They can give us invaluable feedback that can also eliminate problems that otherwise would emerge during the development phase and would make us change the central workings of the product. Cooperation between the project teams is essential for a successful project.&lt;/p&gt;

&lt;p&gt;After we have enough information about the journeys and have all agreed on them we continue with the &lt;strong&gt;information architecture&lt;/strong&gt; and the &lt;strong&gt;sitemap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cbZ-mV6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/mfekebzll111zagllsp6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cbZ-mV6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/mfekebzll111zagllsp6.png" alt="Sitemap"&gt;&lt;/a&gt;&lt;em&gt;The sitemap of our latest project: &lt;a href="https://ondehero.com/"&gt;OndeCare&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wireframes
&lt;/h2&gt;

&lt;p&gt;The journeys and the sitemap tell us what screens we need, so we have almost everything to create the wireframes. However, we strongly believe that only &lt;strong&gt;content driven design&lt;/strong&gt; can ensure the best UX and design solutions. On one hand the sitemap delineates what copy is needed for the wireframes, so the client can prepare it. On the other hand, by this point we have “learned the product’s and the client’s language” and can use it for the UX copy. If needed we work together on the content strategy and UX copy.&lt;/p&gt;

&lt;p&gt;Our &lt;strong&gt;wireframing&lt;/strong&gt; process starts with sketches about various ideas that, depending on their complexity, we sometimes discuss in the design team. We create high-fidelity wireframes from one or maximum two of the ideas that emerged during our discussions. We ask for feedback on these wireframes from two sources: from the developers to have their feedback on feasibility and possible unseen glitches, and from the clients to see if we managed to realize their ideas. These first round discussions can save a lot of time later: we don’t need to modify pixel perfect designs let alone implemented pages. The most time consuming part at this stage is to think through all the possible user journeys, plan for edge cases, create notifications and different states. We always use real content on the plans to make it flexible enough for all cases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F-OSg-3M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/b9i7w3drbjww0hbbk2nd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F-OSg-3M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/b9i7w3drbjww0hbbk2nd.png" alt="Wireframe example"&gt;&lt;/a&gt;&lt;em&gt;Ondecare homepage wireframe with actual content&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We also create &lt;strong&gt;prototypes&lt;/strong&gt; from the wireframes which can be a great tool to check the most complicated flows internally just as with &lt;strong&gt;usability tests&lt;/strong&gt;. Depending on the project timeline we like to run usability tests during the planning phase to gather feedback from users fast and iterate the design based on their needs. &lt;/p&gt;

&lt;h1&gt;
  
  
  UI Design
&lt;/h1&gt;

&lt;p&gt;The actual UI design phase starts with creating the &lt;strong&gt;style guide&lt;/strong&gt; based on the brand defined colors and fonts. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cCEQHVjI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/yf013wh0w4paresaub5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cCEQHVjI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/yf013wh0w4paresaub5z.png" alt="pOS style guide"&gt;&lt;/a&gt;&lt;em&gt;Style guide of the pOS design system&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Style guides are a great resource for designers and developers but we can’t expect our clients to put together the site from the components in their minds. The first wireframe that we create is always a landing page, most often the homepage, because it contains the greatest variety of sections and elements: graphics, texts and layouts. We create two versions of the homepage and the client can choose which one they prefer. Then we add all the elements that are used on the site to the style guide so that we can reuse them and have consistent design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--56OPtRjK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/sng49wnuv85ro64hiytd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--56OPtRjK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/sng49wnuv85ro64hiytd.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;em&gt;Two versions of the Ondecare homepage&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layouts and grids&lt;/strong&gt; are an example of the essential things that we create during the high fidelity design phase. We always create layouts that can work both on mobile and desktops, and align all the elements based on these. We add the &lt;strong&gt;illustrations&lt;/strong&gt; and modify them to match the brand colors. Talking about colors, we also put great emphasis on accessibility and contrast ratios. We create smaller (but equally important) pages like 404, under maintenance pages, and usually even an email template to be consistent everywhere.&lt;/p&gt;

&lt;p&gt;As I mentioned we work as a team, which means that the wireframing and high fidelity design phase happen parallely: in our meetings with the client we present the wireframes and ask for feedback. It’s best if we can do the majority of necessary modifications on the wireframes, where we can focus more on the functionalities. Once a wireframe is accepted by the client, it is turned into a &lt;strong&gt;pixel perfect screen&lt;/strong&gt;. Mobile versions can be created either in the wireframing or in the high fidelity design phase. &lt;/p&gt;

&lt;p&gt;Figma is a great tool for design handoff, developers can easily export assets, but if necessary we collect all images and graphics for them. &lt;/p&gt;

&lt;h1&gt;
  
  
  Retrospective
&lt;/h1&gt;

&lt;p&gt;Our process has been working well for us, but since there is no such thing as a perfect process we try to learn from each project. As a closing step for our process, we always hold a &lt;strong&gt;retrospective meeting&lt;/strong&gt; where we check back to our flow and collect everyone’s experience. Our goal is to &lt;strong&gt;learn about the highlights and the improvement areas&lt;/strong&gt; and to identify the necessary modifications for our next project.&lt;/p&gt;

&lt;p&gt;For example, during our last project we learned that we needed to improve the way we sort our screens in Figma. We had two separate folders for wireframes and for pixel perfect designs which might not be optimal in each case: during implementation developers consult pixel perfect designs, but there are functional wireframes that don’t need to be turned into pixel perfect design, like confirmation popups. Based on the team's feedback in the next project we will put these next to each other on one page, so that we only have to check one page for the different states of a screen. &lt;/p&gt;

&lt;p&gt;Of course every project is unique and they all have their own challenges, that we can only address if we handle our processes flexibly. But having this general framework helps us to stay consistent and not to leave out anything important, plus the client can have a clear picture throughout the project and know what to expect. &lt;/p&gt;

&lt;p&gt;The platformOS UX/UI process was defined by our Director of UX &lt;a href="https://www.linkedin.com/in/nagygyorgykatalin/"&gt;Kata Nagygyörgy&lt;/a&gt; and Director of Visual Design &lt;a href="https://www.linkedin.com/in/gora-gy%C3%B6ngy-43008a27/"&gt;Gyöngy Gora&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>ux</category>
      <category>ui</category>
      <category>design</category>
      <category>process</category>
    </item>
    <item>
      <title>How to catch Front-end performance regressions?</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Tue, 29 Dec 2020 15:14:01 +0000</pubDate>
      <link>https://forem.com/platformos/how-to-catch-front-end-performance-regressions-4dl</link>
      <guid>https://forem.com/platformos/how-to-catch-front-end-performance-regressions-4dl</guid>
      <description>&lt;p&gt;If you try to take care of your visitors by making your website fast and light, you probably measure its speed using some tool. Chances are you use one of these free tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/measure/"&gt;Lighthouse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webpagetest.org/"&gt;WebpageTest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/speed/pagespeed/insights/"&gt;PageSpeed Insights&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today I will show you how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run Lighthouse easily from your browser on your current page&lt;/li&gt;
&lt;li&gt;Save report to JSON file&lt;/li&gt;
&lt;li&gt;Compare past report with current report to see if your page is going in the right direction&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you will need
&lt;/h3&gt;

&lt;p&gt;Only two things are needed to compare past report with current one.&lt;/p&gt;

&lt;p&gt;Lighthouse extension for your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/google-lighthouse/"&gt;Firefox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk"&gt;Chrome&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a website that will compare two reports: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://googlechrome.github.io/lighthouse-ci/viewer/"&gt;Lighthouse CI Diff&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Generate report
&lt;/h3&gt;

&lt;p&gt;After you have installed Lighthouse extension go to your website. Open extension by clicking on its icon, change settings to include all reports and use Desktop strategy, and click generate report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BKQhmdAr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/dqf1vyu85xssed6gk9ix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BKQhmdAr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/dqf1vyu85xssed6gk9ix.png" alt="Lighthouse Extension" width="544" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: You can use mobile strategy, or skip PWA - we do. The important thing is to stick to the settings because if Lighthouse report is generated using different settings, compare tool might have issues with detecting changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zloA2P6p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/wmwfnfoq4hbekpjhsbmf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zloA2P6p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/wmwfnfoq4hbekpjhsbmf.png" alt="Example report" width="880" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can export this report to JSON file for future reference. In top-right corner there is a "three dot" menu, click it and generate JSON report. Save it in a safe place - you will need it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--93jONvhw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4h6r7uyd3r7zv0hluqip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--93jONvhw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4h6r7uyd3r7zv0hluqip.png" alt="Saving report" width="398" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you have two reports, ideally generated between some changes, you can go to &lt;a href="https://googlechrome.github.io/lighthouse-ci/viewer/"&gt;https://googlechrome.github.io/lighthouse-ci/viewer/&lt;/a&gt; and upload both of them to compare. Upload the older one on the left (Base), and newer one on the right (Compare) - this way it will tell you if you made progress or not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OedLyNSL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/mni386txfvm96e94fcxn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OedLyNSL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/mni386txfvm96e94fcxn.png" alt="Lighthouse CI Diff tool" width="880" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is how generated comparison looks for our last two builds on our &lt;a href="https://documentation.platformos.com"&gt;platformOS documentation site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XnUuDAur--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/jxq1pr0cj4aab96bazyb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XnUuDAur--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/jxq1pr0cj4aab96bazyb.png" alt="Example comparison" width="880" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Future improvements
&lt;/h3&gt;

&lt;p&gt;In the next post I will write how you can use Github Actions to do the reports automatically. It will give you link to comparison report on every pull request, so you can skip the boring parts and get to the good stuff. :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you like web performance, take a look at my other articles, you might find some inspiration there how to make progress without much effort. And if you really like web performance, consider following me :)&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-tips-on-preserving-website-speed-5g0c" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Tips on Preserving Website Speed&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 24 '20 ・ 6 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-performance-tips-for-your-next-project-2bdm" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Performance Tips for Your Next Project&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 29 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>webperf</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How platformOS uses mixpanel while keeping users privacy?</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Sun, 20 Dec 2020 14:44:04 +0000</pubDate>
      <link>https://forem.com/platformos/how-platformos-uses-mixpanel-and-keeping-users-privacy-118n</link>
      <guid>https://forem.com/platformos/how-platformos-uses-mixpanel-and-keeping-users-privacy-118n</guid>
      <description>&lt;p&gt;We at platformOS value our visitors privacy, time and money. And because we live by our values, we always dive deeper into technologies to manifest them. Today we will dive look into how we use mixpanel without breaking our website's performance or sacrificing our visitors privacy.&lt;/p&gt;

&lt;p&gt;We use mixpanel to learn where our visitors are confused, leaving our pages&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mixpanel
&lt;/h2&gt;

&lt;p&gt;Mixpanel is an industry standard when it comes to getting to know what visitors are doing on your site.&lt;br&gt;
It does not track everything like Google Analytics, but only what you tell it to. At least we hoped that would be the case, but looking at the documentation and training materials led us to believe that it is actually gathering more data than we were comfortable with giving or needed.&lt;/p&gt;

&lt;p&gt;Having said that, mixpanel has a very comprehensive set of tools to dive deeper into where your visitors journey on your website. This is very important for onboarding process because you can focus on exactly the part that is a bottleneck, so you don't waste time optimizing the wrong part of it.&lt;/p&gt;
&lt;h3&gt;
  
  
  How mixpanel is usually implemented
&lt;/h3&gt;

&lt;p&gt;Just like most third-party services mixpanel usually is implemented on the frontend, using javascript. This has a lot of consequences, no matter if you load the script from your CDN or from mixpanel CDN.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It has to load - your page becomes bigger (around 80KB bigger)&lt;/li&gt;
&lt;li&gt;It has to parse and execute - your page becomes slower, especially on slower devices (mobile)&lt;/li&gt;
&lt;li&gt;You don't really know what it does (or track), unless you are inspecting mixpanel javascript client codebase before installation and every update&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mixpanel.com/docs/javascript"&gt;According to documentation&lt;/a&gt; turning on GDPR compliance is an opt-in, so this might create additional friction and work to be done&lt;/li&gt;
&lt;li&gt;AdBlockers have mixpanel CDN blocked by default, so there will be gaps in data. How big of a gaps? Well, some sources report 50% gaps, which is big enough to not rely on client-side measurements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://twitter.com/garybernhardt/status/1338679291182080000"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yIZJ_o6J--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/esx4kv3chnptk3x8u8ty.jpg" alt="Tweet" width="880" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All those disadvantages are directly against our values. But mixpanel is good enough product to do the research and our great engineers found a way to use mixpanel without any javascript.&lt;/p&gt;
&lt;h2&gt;
  
  
  How we use mixpanel
&lt;/h2&gt;

&lt;p&gt;Sending HTTP requests in platformOS is very simple, and we decided to leverage that in our implementation.&lt;/p&gt;

&lt;p&gt;First we defined API call that will do the network call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mixpanel_create_event&lt;/span&gt;
&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.mixpanel.com/track&lt;/span&gt;
&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
&lt;span class="na"&gt;request_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
&lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;{%- assign response_data = response.body | to_hash -%}&lt;/span&gt;
  &lt;span class="s"&gt;{% if response_data.error %}&lt;/span&gt;
    &lt;span class="s"&gt;{%- log response_data.error, type: 'modules/monitoring/mixpanel_create_event' -%}&lt;/span&gt;
  &lt;span class="s"&gt;{% endif %}&lt;/span&gt;
&lt;span class="na"&gt;request_headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;{% if data %}&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"Content-Type": "application/x-www-form-urlencoded"&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
  &lt;span class="s"&gt;{% endif %}&lt;/span&gt;
&lt;span class="s"&gt;---&lt;/span&gt;
&lt;span class="s"&gt;data={{ data }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Everything between &lt;code&gt;---&lt;/code&gt; is definition of the api call, everything below, is the body of the request that will be sent. Additionally, &lt;code&gt;callback&lt;/code&gt; is executed after the API call has been sent. In our case it is logging error if mixpanel server returned one.&lt;/p&gt;

&lt;p&gt;Having defined API call, we need to execute whenever we need to, with proper data. Here we created another abstraction to be able to pass different events from different sources:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;liquid&lt;/span&gt;
  &lt;span class="nv"&gt;graphql instance = 'modules/monitoring/instance' | dig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;instance'&lt;/span&gt;
  &lt;span class="nv"&gt;unless data&lt;/span&gt;
    &lt;span class="nv"&gt;assign data = '&lt;/span&gt;&lt;span class="pi"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;parse_json&lt;/span&gt;
  &lt;span class="s"&gt;endunless&lt;/span&gt;
  &lt;span class="s"&gt;hash_assign&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data['&lt;/span&gt;&lt;span class="nv"&gt;distinct_id'&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt; &lt;span class="nv"&gt;= instance.id&lt;/span&gt;
  &lt;span class="nv"&gt;hash_assign data&lt;/span&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;instance_id'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="nv"&gt;= instance.id&lt;/span&gt;
  &lt;span class="nv"&gt;hash_assign data&lt;/span&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;token'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="nv"&gt;= "377404cb3e579051250ca9a2b129ea7b"&lt;/span&gt;
&lt;span class="nv"&gt;%&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;parse_json mixpanel_data %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;event_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;endparse_json %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;liquid&lt;/span&gt;
  &lt;span class="nv"&gt;graphql r = 'modules/monitoring/api_call'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;mixpanel_data&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;modules/monitoring/mixpanel_create_event'&lt;/span&gt;
  &lt;span class="nv"&gt;log r&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;monitoring/migration/track_first_deploy'&lt;/span&gt;
  &lt;span class="nv"&gt;return r&lt;/span&gt;
&lt;span class="nv"&gt;%&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To every event we attach some basic data, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public token (required by mixpanel),&lt;/li&gt;
&lt;li&gt;instance id - to know which website sent the event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And some variables that are passed down from different parts of the system, one is &lt;code&gt;event_name&lt;/code&gt; which is just a name to filter them easier in the mixpanel dashboard, and &lt;code&gt;data&lt;/code&gt; which is a JSON object that can contain anything we want, including user data, like id, email, browser data (extracted from user agent), etc.&lt;/p&gt;

&lt;p&gt;Now we can call this piece of code (partial) anywhere in our application, passing &lt;code&gt;event_name&lt;/code&gt; and some &lt;code&gt;data&lt;/code&gt;. Simplest example would be our &lt;code&gt;marketplace_install&lt;/code&gt; event which is called every time someone installs our Marketplace Template for the first time.&lt;/p&gt;

&lt;p&gt;Event name is called &lt;code&gt;marketplace_install&lt;/code&gt; to group all those events in one bucket. Our template has different variants (community site, ecommerce), so our &lt;code&gt;data&lt;/code&gt; object identifies that so we know what type of marketplace our partners and clients are installing.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;parse_json data %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketplace"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products"&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;endparse_json %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;liquid&lt;/span&gt;
  &lt;span class="nv"&gt;if context.location.host != 'getmarketplace-qa.staging.gapps.platformos.com'&lt;/span&gt;
    &lt;span class="nv"&gt;function res = 'modules/monitoring/commands/track_event'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;event_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;marketplace_install'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;
  &lt;span class="nv"&gt;endif&lt;/span&gt;
&lt;span class="nv"&gt;%&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Profits
&lt;/h3&gt;

&lt;p&gt;Using mixpanel server-side has some consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is no performance penalty at all on the frontend&lt;/li&gt;
&lt;li&gt;There is no performance penalty on the backend, because code can be run asynchronously, so visitor does not have to wait until data has been sent to mixpanel when browsing website&lt;/li&gt;
&lt;li&gt;Server only knows as much as browser tells it plus whatever logged in person provided, if anything&lt;/li&gt;
&lt;li&gt;Mixpanel does not run any code on neither browser side or server side, so hypothetical hack, our clients are safe. You might think it is far-fetched and a non factor, but history teaches us that hack/leak is not a matter of "if" but "when", no matter how good of a security company implement. Thats one of the reasons we try to minimize data we keep, and give.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Tracking across multiple sites
&lt;/h2&gt;

&lt;p&gt;When you have multiple sites like we do (Documentation, Marketing page, Partner Portal) you might want to track visitor's journey across all of them. We had this thought too, but this would mean one thing: identifying single users.&lt;/p&gt;

&lt;p&gt;Long story short, it all comes down to &lt;a href="https://pixelprivacy.com/resources/browser-fingerprinting/"&gt;fingerprinting&lt;/a&gt; which is a standard practice for most big outlets, ad networks, etc. but is unacceptable by us. Not only it is sending a lot of javascript down the wire that visitors does not want, they would probably answer: "NO" if you asked them if they want to be fingerprinted, no matter what.&lt;/p&gt;

&lt;p&gt;Instead of tracking across multiple pages, we decided to restructure our onboarding flow a little bit to minimize jumping across different domains/websites. It is more consistent visually and more performant for the visitor as well. &lt;/p&gt;
&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=46SPsRshtXw"&gt;[Video] What is mixpanel by Danny Lambert&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mixpanel.com/docs"&gt;Mixpanel Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://documentation.platformos.com/"&gt;platformOS Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.platformos.com/"&gt;platformOS Marketing page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mdyd-dev/product-marketplace-template"&gt;platformOS product template github repository&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/using-webp-in-your-existing-webpage-809" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Using WebP in Your Existing Webpage&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 17 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#design&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;




&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/optimizing-images-for-the-web-18gc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Optimizing Images For The Web&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Apr 24 '20 ・ 8 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#node&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-tips-on-preserving-website-speed-5g0c" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Tips on Preserving Website Speed&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 24 '20 ・ 6 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>beginners</category>
      <category>analytics</category>
      <category>privacy</category>
      <category>performance</category>
    </item>
    <item>
      <title>platformOS Documentation Site Webpack Setup</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Fri, 04 Dec 2020 18:25:30 +0000</pubDate>
      <link>https://forem.com/platformos/platformos-documentation-site-webpack-setup-93l</link>
      <guid>https://forem.com/platformos/platformos-documentation-site-webpack-setup-93l</guid>
      <description>&lt;p&gt;Webpack is a powerful tool. Back in the day, it had a reputation of being hard to learn and hard to use. Nowadays, it has excellent documentation, sensible defaults, plugins, and loader - all this to help you keep your config small while achieving great results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important features in 2020
&lt;/h3&gt;

&lt;p&gt;The most important features of webpack for our projects are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tree shaking&lt;/li&gt;
&lt;li&gt;Code splitting&lt;/li&gt;
&lt;li&gt;Dynamic async loading chunks of code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article explains how we used webpack in our documentation for the past couple of years for easy development and performant production builds.&lt;/p&gt;

&lt;p&gt;The final webpack config looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;const path &lt;span class="o"&gt;=&lt;/span&gt; require&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
const MiniCssExtractPlugin &lt;span class="o"&gt;=&lt;/span&gt; require&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mini-css-extract-plugin'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
const WebpackRequireFrom &lt;span class="o"&gt;=&lt;/span&gt; require&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webpack-require-from'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
const webpack &lt;span class="o"&gt;=&lt;/span&gt; require&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'webpack'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

const production &lt;span class="o"&gt;=&lt;/span&gt; process.env.NODE_ENV &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'production'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

module.exports &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  entry: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'app'&lt;/span&gt;: &lt;span class="s1"&gt;'./src/app'&lt;/span&gt;,
    &lt;span class="s1"&gt;'graphql'&lt;/span&gt;: &lt;span class="s1"&gt;'./modules/graphql/public/assets/graphql'&lt;/span&gt;,
  &lt;span class="o"&gt;}&lt;/span&gt;,
  output: &lt;span class="o"&gt;{&lt;/span&gt;
    chunkFilename: &lt;span class="s1"&gt;'[name].[chunkhash:3].js'&lt;/span&gt;,
    publicPath: &lt;span class="s1"&gt;''&lt;/span&gt;,
    path: path.resolve&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app/assets'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;,
  &lt;span class="o"&gt;}&lt;/span&gt;,
  module: &lt;span class="o"&gt;{&lt;/span&gt;
    rules: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;test&lt;/span&gt;: /&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;css&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$/&lt;/span&gt;,
        use: &lt;span class="o"&gt;[&lt;/span&gt;MiniCssExtractPlugin.loader, &lt;span class="o"&gt;{&lt;/span&gt; loader: &lt;span class="s1"&gt;'css-loader'&lt;/span&gt;, options: &lt;span class="o"&gt;{&lt;/span&gt; url: &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;, &lt;span class="s1"&gt;'postcss-loader'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
      &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;test&lt;/span&gt;: /&lt;span class="se"&gt;\.&lt;/span&gt;js&lt;span class="nv"&gt;$/&lt;/span&gt;,
        loader: &lt;span class="s1"&gt;'babel-loader'&lt;/span&gt;,
        options: &lt;span class="o"&gt;{&lt;/span&gt;
          exclude: /node_modules/,
          plugins: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'@babel/plugin-syntax-dynamic-import'&lt;/span&gt;, &lt;span class="s1"&gt;'@babel/transform-object-assign'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
          cacheDirectory: &lt;span class="nb"&gt;true&lt;/span&gt;,
          presets: &lt;span class="o"&gt;[&lt;/span&gt;
            &lt;span class="o"&gt;[&lt;/span&gt;
              &lt;span class="s1"&gt;'@babel/preset-env'&lt;/span&gt;
            &lt;span class="o"&gt;]&lt;/span&gt;
          &lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="o"&gt;]&lt;/span&gt;,
  &lt;span class="o"&gt;}&lt;/span&gt;,
  plugins: &lt;span class="o"&gt;[&lt;/span&gt;
    new MiniCssExtractPlugin&lt;span class="o"&gt;({&lt;/span&gt;
      filename: &lt;span class="s1"&gt;'[name].css'&lt;/span&gt;,
      chunkFilename: &lt;span class="s1"&gt;'[name].[chunkhash:3].css'&lt;/span&gt;,
    &lt;span class="o"&gt;})&lt;/span&gt;,
    new WebpackRequireFrom&lt;span class="o"&gt;({&lt;/span&gt;
      variableName: &lt;span class="s1"&gt;'window.__CONTEXT__.cdnUrl'&lt;/span&gt;,
    &lt;span class="o"&gt;})&lt;/span&gt;,
    new webpack.DefinePlugin&lt;span class="o"&gt;({&lt;/span&gt;
      &lt;span class="s1"&gt;'process.env'&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
        NODE_ENV: JSON.stringify&lt;span class="o"&gt;(&lt;/span&gt;process.env.NODE_ENV&lt;span class="o"&gt;)&lt;/span&gt;,
      &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="o"&gt;})&lt;/span&gt;,
  &lt;span class="o"&gt;]&lt;/span&gt;,
  mode: production ? &lt;span class="s1"&gt;'production'&lt;/span&gt; : &lt;span class="s1"&gt;'development'&lt;/span&gt;,
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Note about static assets
&lt;/h3&gt;

&lt;p&gt;We do not process static assets with webpack. Fonts, images are just sitting already optimized in their proper place in our platformOS assets directory - app/assets. There are a couple of reasons we do that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Manual optimization is usually better than automatic&lt;/li&gt;
&lt;li&gt;Config is simpler&lt;/li&gt;
&lt;li&gt;Fewer dependencies to break&lt;/li&gt;
&lt;li&gt;Build is faster&lt;/li&gt;
&lt;li&gt;We don't waste CPU time (= money) on CI doing the same operation on every build&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Technologies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://postcss.org/"&gt;PostCSS&lt;/a&gt;&lt;/strong&gt; is a framework for creating CSS parsing plugins. It allowed us to migrate away from SASS to reduce complexity. It handles some legacy browser fixes. It is also a base for TailwindCSS, a PostCSS plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="http://tailwindcss.com/"&gt;TailwindCSS&lt;/a&gt;&lt;/strong&gt; is a CSS framework that makes it easy to create maintainable views with a minimal CSS footprint.&lt;/p&gt;

&lt;p&gt;If you are interested in how we set up TailwindCSS and PostCSS, visit &lt;a href="https://github.com/mdyd-dev/platformos-documentation/"&gt;our documentation GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Base config
&lt;/h2&gt;

&lt;p&gt;Out of the box, webpack can work with no config at all. &lt;a href="https://auth0.com/blog/zero-config-javascript-app-prototyping-with-webpack/"&gt;Read more on prototyping with no config&lt;/a&gt;. By default, it looks for the &lt;code&gt;index.js&lt;/code&gt; file in the &lt;code&gt;src/&lt;/code&gt; directory and outputs to &lt;code&gt;dist/main.js&lt;/code&gt;. We need to use a couple of features, so we need a config file. The webpack settings we use are pretty simple. &lt;/p&gt;

&lt;h3&gt;
  
  
  Entry points
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;entry: &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s1"&gt;'app'&lt;/span&gt;: &lt;span class="s1"&gt;'./src/app'&lt;/span&gt;,
  &lt;span class="s1"&gt;'graphql'&lt;/span&gt;: &lt;span class="s1"&gt;'./modules/graphql/public/assets/graphql'&lt;/span&gt;,
&lt;span class="o"&gt;}&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use two entry points (files that we will pull in from our HTML) because we have completely separate CSS files for our documentation page and autogenerated documentation for GraphQL endpoints. &lt;code&gt;key&lt;/code&gt; is to define how the output will be named, &lt;code&gt;value&lt;/code&gt; is the relative path to the file webpack will treat as an entry point. For example: &lt;code&gt;app.js&lt;/code&gt; will be generated from entry point &lt;code&gt;src/app.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output
&lt;/h3&gt;

&lt;p&gt;We don't define the output filename because by default, it is &lt;code&gt;[name].js&lt;/code&gt;, and it works for our case. But for &lt;code&gt;chunkFilename&lt;/code&gt;, we need to add chunkhash for cache invalidation purposes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;publicPath&lt;/code&gt; is set to empty because we will not use webpack's built in publicPath — we will use the one from &lt;code&gt;WebpackRequireFrom&lt;/code&gt;, explained later.&lt;/p&gt;

&lt;p&gt;path is the absolute path where the output files will be placed. In the case of platformOS, it is &lt;code&gt;app/assets&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;output: &lt;span class="o"&gt;{&lt;/span&gt;
    chunkFilename: &lt;span class="s1"&gt;'[name].[chunkhash:3].js'&lt;/span&gt;,
    publicPath: &lt;span class="s1"&gt;''&lt;/span&gt;,
    path: path.resolve&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app/assets'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;,
  &lt;span class="o"&gt;}&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Plugins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/webpack-contrib/mini-css-extract-plugin"&gt;MiniCssExtractPlugin&lt;/a&gt;&lt;/strong&gt; extracts CSS to external files. Without it, webpack would package everything into one JS and one CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;new MiniCssExtractPlugin&lt;span class="o"&gt;({&lt;/span&gt;
    filename: &lt;span class="s1"&gt;'[name].css'&lt;/span&gt;,
    chunkFilename: &lt;span class="s1"&gt;'[name].[chunkhash:3].css'&lt;/span&gt;,
&lt;span class="o"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The platformOS Liquid filter for getting the URL to assets placed on the CDN adds the query param to bust the cache, so the main files that we require in Liquid do not need to vary in between different builds. We add &lt;code&gt;chunkhash:3&lt;/code&gt; in the file name for chunks because those are loaded dynamically from webpack, and there won't be a query param to handle that.&lt;/p&gt;

&lt;p&gt;Why all that? To prevent browsers from loading a file from cache (old asset could break something) if it changed.&lt;/p&gt;

&lt;p&gt;Files loaded using the native platformOS &lt;code&gt;asset_url&lt;/code&gt; filter:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ygtcn7MN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/8nr7km8tm2h6m0g2xnc6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ygtcn7MN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/8nr7km8tm2h6m0g2xnc6.png" alt="asset_url cache bust" width="646" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chunks loaded with chunkhash in the filename:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wPNpxrIF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1l265an5z4adyryqxnlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wPNpxrIF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1l265an5z4adyryqxnlp.png" alt="chunks filename cache bust" width="348" height="104"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/agoldis/webpack-require-from"&gt;WebpackRequireFrom&lt;/a&gt;&lt;/strong&gt; sets the CDN URL during runtime for dynamic chunks, so we don't have to hardcode anything. We just pull this URL from the HTML source, which is rendered server-side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;new WebpackRequireFrom&lt;span class="o"&gt;({&lt;/span&gt;
    variableName: &lt;span class="s1"&gt;'window.__CONTEXT__.cdnUrl'&lt;/span&gt;,
&lt;span class="o"&gt;})&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This option tells webpack where to look to get the CDN URL in the browser when the application is running. In Liquid, we define &lt;code&gt;window.__CONTEXT__.cdnUrl&lt;/code&gt; to our CDN URL to let webpack know where to look for dynamically loaded chunks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  window.__CONTEXT__ &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; cdnUrl: &lt;span class="s2"&gt;"{{ '' | asset_url }}"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in chunks loaded correctly on all environments provided by platformOS: dev, staging, and production.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://webpack.js.org/plugins/define-plugin/"&gt;webpack.DefinePlugin&lt;/a&gt;&lt;/strong&gt; allows us to pass build mode (production or dev) to the runtime. This is needed by the Algolia search script that we use as our search engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;new webpack.DefinePlugin&lt;span class="o"&gt;({&lt;/span&gt;
 &lt;span class="s1"&gt;'process.env'&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
     NODE_ENV: JSON.stringify&lt;span class="o"&gt;(&lt;/span&gt;process.env.NODE_ENV&lt;span class="o"&gt;)&lt;/span&gt;,
  &lt;span class="o"&gt;}&lt;/span&gt;,
&lt;span class="o"&gt;})&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that for this line to work, we execute webpack-cli with NODE_ENV defined in the npm script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;: &lt;span class="s2"&gt;"NODE_ENV=production npx webpack-cli --no-color"&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Loaders
&lt;/h2&gt;

&lt;p&gt;Loaders are specific plugins for webpack that extend its functionality to operate on files that it does not understand by default. By default, webpack can only parse and bundle JavaScript.&lt;/p&gt;

&lt;p&gt;The order of loaders matters. For example, css-loader cannot parse syntax written for PostCSS, so PostCSS needs to be executed first. Loaders are executed from right to left.&lt;/p&gt;

&lt;p&gt;postcss-loader allows webpack to use the PostCSS plugins ecosystem. It usually needs the postcss.config.js file to add PostCSS plugins because PostCSS on its own doesn't do anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;test&lt;/span&gt;: /&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;css&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$/&lt;/span&gt;,
    use: &lt;span class="o"&gt;[&lt;/span&gt;
            MiniCssExtractPlugin.loader,
            &lt;span class="o"&gt;{&lt;/span&gt; loader: &lt;span class="s1"&gt;'css-loader'&lt;/span&gt;, options: &lt;span class="o"&gt;{&lt;/span&gt; url: &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
            &lt;span class="s1"&gt;'postcss-loader'&lt;/span&gt;
        &lt;span class="o"&gt;]&lt;/span&gt;,
&lt;span class="o"&gt;}&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For CSS files, PostCSS will do its thing first, pass the output to css-loader, it will do its thing (without resolving URLs in CSS) and pass the output to MiniCssExtractPlugin.loader which will save the CSS to separate files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/babel/babel-loader"&gt;babel-loader&lt;/a&gt; transpiles JavaScript using babel and its plugins. We use &lt;code&gt;preset-env&lt;/code&gt; and two plugins: &lt;code&gt;syntax-dynamic-import&lt;/code&gt; and &lt;code&gt;transform-object-assign&lt;/code&gt; for browsers defined in &lt;a href="https://github.com/mdyd-dev/platformos-documentation/blob/1505cf2c7ace239d46abec979f2c01f0b4d5bd1b/package.json#L27"&gt;package.json&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code splitting and conditional loading
&lt;/h2&gt;

&lt;p&gt;This article wouldn't be complete if I did not include how to split code in webpack build time so that it asynchronously loads chunks while running in the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;import &lt;span class="s1"&gt;'./app.css'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

import &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; from &lt;span class="s1"&gt;'./js/helpers/dom'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

import &lt;span class="s1"&gt;'./js/sidebarMenu'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
import &lt;span class="s1"&gt;'./js/deepLinks'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
import &lt;span class="s1"&gt;'./js/autosteps'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; // this HAS to be after deepLinks
import &lt;span class="s1"&gt;'./js/toc'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
import &lt;span class="s1"&gt;'./js/externalLinks'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
import &lt;span class="s1"&gt;'./js/feedback'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

import&lt;span class="o"&gt;(&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt; webpackChunkName: &lt;span class="s2"&gt;"search"&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;/ &lt;span class="s1"&gt;'./js/search'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'code[class*="language-"]'&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  import&lt;span class="o"&gt;(&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt; webpackChunkName: &lt;span class="s2"&gt;"syntaxHighlighting"&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;/ &lt;span class="s1"&gt;'./js/syntaxHighlighting'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;All the ES6 imports (from &lt;code&gt;sidebarMenu&lt;/code&gt; to &lt;code&gt;feedback&lt;/code&gt;) will be bundled directly into app.js.&lt;/li&gt;
&lt;li&gt;Dynamic imports (function named import - &lt;code&gt;search&lt;/code&gt;) will generate chunks, that will be loaded asynchronously.&lt;/li&gt;
&lt;li&gt;Dynamic imports wrapped in an &lt;code&gt;if&lt;/code&gt; statement will be loaded asynchronously only when the &lt;code&gt;if&lt;/code&gt; condition evaluates to a &lt;code&gt;truthy&lt;/code&gt; value. In our case, when a certain selector is present on the webpage. Webpack will not load those modules on pages that do not require them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example requests for the homepage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fc297_QN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/trfr3c1kt07u7jtb7wla.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fc297_QN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/trfr3c1kt07u7jtb7wla.png" alt="Conditional loading - no syntax highlighting" width="500" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example requests for a page where syntax highlighting is needed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aBT_6nqh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/fhabisiigwglhyq3nr87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aBT_6nqh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/fhabisiigwglhyq3nr87.png" alt="Conditional loading - with syntax highlighting" width="500" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing thoughts
&lt;/h3&gt;

&lt;p&gt;Webpack in 2020 is a totally different beast than it was in 2015, but it can still be intimidating. If you don't feel its syntax, you can try different code bundlers; some of them are simpler to some people (&lt;a href="https://www.rollupjs.org/"&gt;rollup&lt;/a&gt;), and some are much faster (&lt;a href="https://esbuild.github.io/"&gt;esbuild&lt;/a&gt;). We chose webpack a long time ago because it was reliable, the most feature-rich (thanks to plugins), powerful, and very well documented.&lt;/p&gt;

&lt;p&gt;If you are interested in a Webpack + TailwindCSS setup for static websites (with HTML that you can deploy to platformOS, netlify, vercel, or just AWS S3), we created an &lt;a href="https://github.com/pavelloz/webpack-tailwindcss-purgecss"&gt;opensource boilerplate&lt;/a&gt; for you to quickly jump in and test it out.&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>webpack</category>
      <category>platformos</category>
    </item>
    <item>
      <title>How to Test Slack Notifications</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Thu, 26 Nov 2020 17:25:29 +0000</pubDate>
      <link>https://forem.com/platformos/how-to-test-slack-notifications-2leb</link>
      <guid>https://forem.com/platformos/how-to-test-slack-notifications-2leb</guid>
      <description>&lt;p&gt;Marketing forms are often connected to a third-party system to help the marketing team provide faster and more accurate responses.&lt;/p&gt;

&lt;p&gt;We integrated our contact page with Slack, HubSpot, SendGrid, and emails. Whenever someone fills in the form, we notify the appropriate people via email, Slack message, and create records in HubSpot and SendGrid for future management of those contacts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9qyvtud4azy2jbos1fpq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9qyvtud4azy2jbos1fpq.png" alt="Contact form"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One day we received the email, but the Slack notification did not come through, and this made me think about testing Slack notifications to catch regressions in this area.&lt;/p&gt;

&lt;p&gt;To achieve this, we need two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A test channel for test messages&lt;/li&gt;
&lt;li&gt;A test that will send the form for us&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  platformOS notification setup
&lt;/h3&gt;

&lt;p&gt;platformOS API Call notifications support WebHooks, so that's what we used for Slack messages. The setup involves only a couple of lines of code, but the most important in the context of this article is the WebHook URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack_landing_sales&lt;/span&gt;
&lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;
&lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Content-Type":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}'&lt;/span&gt;
&lt;span class="na"&gt;request_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hooks.slack.com/services/XXX/XXX/XXX&lt;/span&gt;
&lt;span class="na"&gt;trigger_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="s"&gt;Heads&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up!&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Contact&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;form.properties.url&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.&lt;/span&gt;
&lt;span class="s"&gt;Name:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;form.properties.first_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;form.properties.last_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;
&lt;span class="s"&gt;Email:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;form.properties.email&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;
&lt;span class="s"&gt;Phone:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;form.properties.phone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;
&lt;span class="s"&gt;Comments:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;form.properties.description&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The line to change is the one with &lt;code&gt;to: [https://hooks.slack.com/services/XXX/XXX/XXX](https://hooks.slack.com/services/XXX/XXX/XXX)&lt;/code&gt; which I censored. You need your own webhook URL to be able to send messages to your channel.&lt;/p&gt;

&lt;p&gt;To not send test data to the production Slack channel, it is a good idea to differentiate production from everything else:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;{% if form.properties.url contains 'https://www.platformos.com' %}&lt;/span&gt;
  &lt;span class="s"&gt;https://hooks.slack.com/services/XXX/XXX/XXX&lt;/span&gt;
  &lt;span class="s"&gt;{% else %}&lt;/span&gt;
  &lt;span class="s"&gt;https://hooks.slack.com/services/YYY/YYY/YYY&lt;/span&gt;
  &lt;span class="s"&gt;{% endif %}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This if condition will check if the form field called &lt;code&gt;url&lt;/code&gt; contains a particular string. Because we are populating it before sending the form, it is filled in with the URL of the instance. In the case of our production site, it is &lt;code&gt;https://www.platformos.com&lt;/code&gt;. &lt;/p&gt;
&lt;h3&gt;
  
  
  E2E test in TestCafe
&lt;/h3&gt;

&lt;p&gt;Now, we need to programmatically go through the form, fill it, send it, and validate that it has been sent successfully.&lt;/p&gt;

&lt;p&gt;We use TestCafe for E2E tests. It is not too difficult to start using it if you have experience with testing.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Selector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;testcafe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;faker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/contact`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Contact form works&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="form[properties_attributes][first_name]"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ln&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="form[properties_attributes][last_name]"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="form[properties_attributes][email]"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="form[properties_attributes][phone]"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="form[properties_attributes][description]"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[role="alert"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;withText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your contact form has been sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phoneNumber&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lorem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submitBtn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A short summary of what is going on here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Importing TestCafe and faker (generates fake data so that the test varies every time it is run)&lt;/li&gt;
&lt;li&gt;Telling TestCafe the URL at which the test will be run &lt;/li&gt;
&lt;li&gt;Defining selectors for all the DOM elements we will be interacting with&lt;/li&gt;
&lt;li&gt;Filling in all the data in the form&lt;/li&gt;
&lt;li&gt;Submitting the form&lt;/li&gt;
&lt;li&gt;Checking if the &lt;code&gt;Your contact form has been sent&lt;/code&gt; message is visible after the form has been sent&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running the test in chromium results in a pretty video:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fctr2y7b51yuc85fx6swl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fctr2y7b51yuc85fx6swl.gif" alt="TestCafe running"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In your terminal, you should see something similar after running TestCafe:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; testcafe chromium tests &lt;span class="nt"&gt;--video&lt;/span&gt; artifacts &lt;span class="nt"&gt;--video-encoding-options&lt;/span&gt; &lt;span class="nv"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20

 Running tests &lt;span class="k"&gt;in&lt;/span&gt;:
 - Chrome 88.0.4295.0 / macOS 10.15.7

 Contact
 ✓ Contact form works

 1 passed &lt;span class="o"&gt;(&lt;/span&gt;38s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This means that the test passed, so now it is time to verify if the Slack notification came through. This is what this test generated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fckfj1lgyidt6s0dmi982.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fckfj1lgyidt6s0dmi982.png" alt="Slack message"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Continuous Integration
&lt;/h3&gt;

&lt;p&gt;To make our lives even more automated, we add the &lt;code&gt;npm run test-ci&lt;/code&gt; command to our CI workflow. For this project, we use Jenkins, but you might use Github Actions, Travis, or CircleCI if you prefer. Remember that your CI server has to have a browser supported by TestCafe (&lt;a href="https://devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#officially-supported-browsers" rel="noopener noreferrer"&gt;see the list of officially supporter browsers here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In our case, making tests run on every master branch run was similar to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stage&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test on URL'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    agent &lt;span class="o"&gt;{&lt;/span&gt; docker &lt;span class="o"&gt;{&lt;/span&gt; image &lt;span class="s2"&gt;"platformos/testcafe"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    environment &lt;span class="o"&gt;{&lt;/span&gt; MP_URL &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.MP_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    steps &lt;span class="o"&gt;{&lt;/span&gt;
      sh &lt;span class="s1"&gt;'npm run test-ci'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    post &lt;span class="o"&gt;{&lt;/span&gt; failure &lt;span class="o"&gt;{&lt;/span&gt; archiveArtifacts &lt;span class="s2"&gt;"screenshots/"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We also use a different command on CI because we want to take screenshots of failures only and run the test in a headless browser (it is much faster — the test above runs in 6 seconds instead of 38):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;testcafe &lt;span class="s1"&gt;'chromium:headless'&lt;/span&gt; &lt;span class="nt"&gt;--screenshots-on-fails&lt;/span&gt; &lt;span class="nt"&gt;--screenshots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;screenshots tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now every time any code is merged into the master branch, TestCafe will test this form and trigger the Slack webhook. If the message will be missing one day, or if the test fails, we will know something is wrong either on our side or Slack's.&lt;/p&gt;
&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;I hope this guide will help you avoid some of the regressions. Here are some resources that you might find useful if you decide to follow our path:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.platformos.com" rel="noopener noreferrer"&gt;platformOS Website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://documentation.platformos.com/" rel="noopener noreferrer"&gt;platformOS Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://slack.com/intl/en-it/help/articles/115005265063-Incoming-webhooks-for-Slack" rel="noopener noreferrer"&gt;Slack - Creating App with WebHooks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devexpress.github.io/testcafe/" rel="noopener noreferrer"&gt;TestCafe testing framework&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/faker" rel="noopener noreferrer"&gt;faker npm package&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/using-webp-in-your-existing-webpage-809" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Using WebP in Your Existing Webpage&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 17 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#design&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;




&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/optimizing-images-for-the-web-18gc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Optimizing Images For The Web&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Apr 24 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#node&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-tips-on-preserving-website-speed-5g0c" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Tips on Preserving Website Speed&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 24 '20&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>testing</category>
    </item>
    <item>
      <title>Using WebP in Your Existing Webpage</title>
      <dc:creator>Paweł Kowalski</dc:creator>
      <pubDate>Tue, 17 Nov 2020 14:44:16 +0000</pubDate>
      <link>https://forem.com/platformos/using-webp-in-your-existing-webpage-809</link>
      <guid>https://forem.com/platformos/using-webp-in-your-existing-webpage-809</guid>
      <description>&lt;p&gt;If you are checking your website's performance using Lighthouse, you might have noticed that one recommendation is to use "&lt;em&gt;Serve images in next-gen formats&lt;/em&gt;" and by that, they mean WebP.&lt;/p&gt;

&lt;p&gt;According to Google, if you compress PNG images, you will save 26% on average.&lt;/p&gt;

&lt;p&gt;Compared to JPEG with a similar SSIM setting, the difference is even more significant — 25%-34% depending on image type. We tested those claims on our JPEG photos from a smartphone, read on to learn about the results. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Be careful what type of image you compress with WebP because it has no progressive loading like JPEG, so your hero image in WebP might negatively impact First Contentful Paint. &lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;
      &lt;div class="ltag__twitter-tweet__media"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YZWEzxnU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/media/EmFbAxXWkAAbQCB.png" alt="unknown tweet media content"&gt;
      &lt;/div&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--AjHtxuRG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1265585417849634816/OqwdR83A_normal.jpg" alt="Harry Roberts profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Harry Roberts
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @csswizardry
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      Fascinating issue on a current client site where we reduced their masthead image weight by over 20% by switching it to WebP. It’s now rendering almost 2× later because WebP doesn’t offer progressive rendering like the previous JPG did. 
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      19:55 PM - 05 Nov 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1324440103792578562" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1324440103792578562" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1324440103792578562" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  If WebP is so good, why isn't everyone using it?
&lt;/h2&gt;

&lt;p&gt;In short: Tooling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your phone takes photos in JPEG.&lt;/li&gt;
&lt;li&gt;Your computer takes screenshots as PNG or JPEG.&lt;/li&gt;
&lt;li&gt;When you download images from a stock photo service, it's JPEG.&lt;/li&gt;
&lt;li&gt;Logotypes are often SVG.&lt;/li&gt;
&lt;li&gt;Your operating system most likely does not know how to preview WebP natively, so you need to download special software to do it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these are barriers to adoption for this format, which apart from having better compression, has an alpha channel, which is missing from JPEG.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our case
&lt;/h2&gt;

&lt;p&gt;One day, looking at a performance report from our &lt;a href="https://github.com/mdyd-dev/product-marketplace-template"&gt;social commerce marketplace template&lt;/a&gt; (see live demo at: &lt;a href="https://getmarketplace.co/"&gt;https://getmarketplace.co/&lt;/a&gt; ), I decided to do something about it and add WebP versions of the biggest images.&lt;/p&gt;

&lt;p&gt;Why only the biggest images? Because the bigger the image, the bigger the savings.&lt;/p&gt;

&lt;p&gt;Saving 20% from a 9 KB lazy-loaded image is a good thing, but saving 20% from 3 images eagerly-loaded, 500 KB each, is much better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser compatibility
&lt;/h3&gt;

&lt;p&gt;Not all browsers support WebP, so you'll need a fallback for those browsers. Luckily, the picture HTML tag takes care of that.&lt;/p&gt;

&lt;p&gt;Read about browser support for WebP at caniuse.com: &lt;a href="https://caniuse.com/?search=webp"&gt;https://caniuse.com/?search=webp&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;In any case, you can use WebP nowadays with a fallback to JPEG - read how to do it (and more) in this &lt;a href="https://css-tricks.com/using-webp-images/"&gt;excellent CSS-Tricks article&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  platformOS image versions
&lt;/h3&gt;

&lt;p&gt;Users very rarely have images in an optimal format or version. Often users upload photos straight from their camera or photos in PNG format. More often than not, you want to recompress those images to fit your performance and quality vision. We implemented a feature that allows the site owner to decide what to do with user-uploaded images.&lt;/p&gt;

&lt;p&gt;After the user uploads the image &lt;strong&gt;directly to cloud storage&lt;/strong&gt; to speed up things by eliminating intermediaries, a function takes the original file and generates new versions based on the configuration defined in a YML file. It usually takes so little time that the user will not notice when it happens, as it happens in the background, asynchronously.&lt;/p&gt;

&lt;p&gt;In our case, the basic configuration looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;photo&lt;/span&gt;
&lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;photo&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upload&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uncropped&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jpeg&lt;/span&gt;
            &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This means whatever image the user uploads will be available to use in its original form and an additional JPEG file will be generated called &lt;code&gt;uncropped&lt;/code&gt; with quality 80. We wanted to also have this image in WebP format, so I added the second version to the array:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uncropped_webp&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webp&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  HTML and Liquid
&lt;/h3&gt;

&lt;p&gt;To show the images we have to get objects from GraphQL and retrieve URLs to the generated versions:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"{{ p.photo.versions.uncropped_webp }}"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"{{ item.name }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"{{ p.photo.versions.uncropped }}"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/jpeg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"{{ item.name }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ p.photo.versions.uncropped }}"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"{{ item.name }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;I uploaded three photos straight from my smartphone:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;3.5M]  IMG_20201105_144639.jpg
&lt;span class="o"&gt;[&lt;/span&gt;4.2M]  IMG_20201109_194144.jpg
&lt;span class="o"&gt;[&lt;/span&gt;3.2M]  IMG_20201115_001933.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;They are very big in terms of dimensions (4608x2592 px) and size, so I expected to see a lot smaller files at the end of this experiment.&lt;/p&gt;

&lt;p&gt;Recompression to quality 80 in JPEG gave pretty good results:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;1.5M]  uncropped_IMG_20201105_144639.jpg
&lt;span class="o"&gt;[&lt;/span&gt;1.9M]  uncropped_IMG_20201109_194144.jpg
&lt;span class="o"&gt;[&lt;/span&gt;1.3M]  uncropped_IMG_20201115_001933.jpg 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let's compare them to the WebP version (quality 70):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;1.0M]  uncropped_webp_IMG_20201105_144639.webp
&lt;span class="o"&gt;[&lt;/span&gt;1.0M]  uncropped_webp_IMG_20201109_194144.webp
&lt;span class="o"&gt;[&lt;/span&gt;996K]  uncropped_webp_IMG_20201115_001933.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The images vary in many ways, including color distribution or the numbers of edges. Because results depend on image content, an average is most often used when describing compression results. The smallest difference was 23%, and the biggest was 48%. That's a huge variance. In total, those three images saved 1.7 MB.&lt;/p&gt;
&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;├── &lt;span class="o"&gt;[&lt;/span&gt; 11M]  original
│   ├── &lt;span class="o"&gt;[&lt;/span&gt;3.5M]  IMG_20201105_144639.jpg
│   ├── &lt;span class="o"&gt;[&lt;/span&gt;4.2M]  IMG_20201109_194144.jpg
│   └── &lt;span class="o"&gt;[&lt;/span&gt;3.2M]  IMG_20201115_001933.jpg
├── &lt;span class="o"&gt;[&lt;/span&gt;4.7M]  processed-jpeg
│   ├── &lt;span class="o"&gt;[&lt;/span&gt;1.5M]  uncropped_IMG_20201105_144639.jpg
│   ├── &lt;span class="o"&gt;[&lt;/span&gt;1.9M]  uncropped_IMG_20201109_194144.jpg
│   └── &lt;span class="o"&gt;[&lt;/span&gt;1.3M]  uncropped_IMG_20201115_001933.jpg
└── &lt;span class="o"&gt;[&lt;/span&gt;3.0M]  processed-webp
    ├── &lt;span class="o"&gt;[&lt;/span&gt;1.0M]  uncropped_webp_IMG_20201105_144639.webp
    ├── &lt;span class="o"&gt;[&lt;/span&gt;1.0M]  uncropped_webp_IMG_20201109_194144.webp
    └── &lt;span class="o"&gt;[&lt;/span&gt;996K]  uncropped_webp_IMG_20201115_001933.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Saving 1.7 MB (or 36%) on three images without noticeable quality degradation is a success in my book. We could turn up the quality knob higher, and we'd still have plenty of headroom not to lose any detail at all. It is a good result for a couple of lines of code. Especially if the image you are optimizing is a crucial piece of content (in our case, an item photo) and not some stock photo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to check out how the files are different visually, you can &lt;a href="https://github.com/pavelloz/jpeg-webp"&gt;view them on my GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Additional resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/speed/webp/gallery1"&gt;Lossy gallery by Google&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/speed/webp/gallery2"&gt;Lossless and Alpha gallery by Google&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/should-you-always-care-about-your-website-size-2jcc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Should You Always Care about Your Website Size?&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Nov 12 '20 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;




&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/optimizing-images-for-the-web-18gc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Optimizing Images For The Web&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Apr 24 '20 ・ 8 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#node&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/platformos" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l3LNsE6N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--XQubx9HX--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/2088/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png" alt="platformOS" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l5wkmzn0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--dz8eofez--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/51061/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/platformos/3-tips-on-preserving-website-speed-5g0c" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;3 Tips on Preserving Website Speed&lt;/h2&gt;
      &lt;h3&gt;Paweł Kowalski for platformOS ・ Jun 24 '20 ・ 6 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webperf&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webpack&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;
 

</description>
      <category>webperf</category>
      <category>design</category>
    </item>
  </channel>
</rss>
