<?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>DEV Community: Jérôme Pott</title>
    <description>The latest articles on DEV Community by Jérôme Pott (@mornir).</description>
    <link>https://dev.to/mornir</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%2Fuser%2Fprofile_image%2F59837%2F0792d72b-002a-41e5-8bae-485b0741c784.jpeg</url>
      <title>DEV Community: Jérôme Pott</title>
      <link>https://dev.to/mornir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mornir"/>
    <language>en</language>
    <item>
      <title>Weekend Hack: Patreon Pledges Calculator</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Thu, 26 Aug 2021 18:28:10 +0000</pubDate>
      <link>https://dev.to/mornir/weekend-hack-patreon-pledges-calculator-19m9</link>
      <guid>https://dev.to/mornir/weekend-hack-patreon-pledges-calculator-19m9</guid>
      <description>&lt;p&gt;Last weekend I coded a simple web app. It lets users visualize how much money they've pledged to each creator they've supported on Patreon.com&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hDWQO5gf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/1411843/130349351-a2145b0d-ae0f-4693-bda9-0aca4388c568.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hDWQO5gf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/1411843/130349351-a2145b0d-ae0f-4693-bda9-0aca4388c568.PNG" alt="Patreon Pledges Calculator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;link: &lt;a href="https://patreon-pledges-calculator.netlify.app"&gt;https://patreon-pledges-calculator.netlify.app&lt;/a&gt;&lt;br&gt;
repo: &lt;a href="https://github.com/mornir/patreon-pledges-calculator"&gt;https://github.com/mornir/patreon-pledges-calculator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Surprisingly, it is not possible to get that information from the official website. One can only view its billing history. However, from that history data, it's possible to calculate the total amounts of pledges per Patreon creator.&lt;/p&gt;

&lt;p&gt;The data is delivered through a &lt;code&gt;/bills&lt;/code&gt; endpoint in the JSON format. My first thought was to the Patreon API and have the users authenticate with their Patreon account. For that I needed create a Creator account, which I did. But then I realized that I could only get access to bills/payments made to my account. &lt;/p&gt;

&lt;p&gt;This actually makes sense: billing data are private and sensitive and shouldn't be exposed to all Patreon creators. My guess is that the main purpose of the Patreon API is to let creators manage their supporters (for example for gated content).&lt;/p&gt;

&lt;p&gt;So I decided to just let users dump their JSON payload in a &lt;code&gt;textarea&lt;/code&gt; 😆&lt;br&gt;
It does the job and users don't need to worry about privacy (btw I should probably mention on the web page that all data are processed by the browser).&lt;/p&gt;
&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;I used Vue 3 with Vite. I kept to the good ol' Options API, as I'm not a fan of the new Composition API. Anyway, &lt;a href="https://v3.vuejs.org/guide/composition-api-introduction.html"&gt;in the Vue 3 docs&lt;/a&gt;, the Options is still taught first and the Composition is said to be reserved to big and complex application 🤷‍♂️&lt;/p&gt;

&lt;p&gt;Since it's just a one page website, I thought that this time I could just go with Vanilla CSS, but it seems that I've already developed a severe addiction to &lt;a href="https://tailwindcss.com/"&gt;tailwindcss&lt;/a&gt; to the point that I use it anywhere and anytime. 🍹 &lt;br&gt;
I especially love how easy it was to create a nice list animation using &lt;a href="https://v3.vuejs.org/guide/transitions-list.html#list-move-transitions"&gt;Vue List Move Transitions&lt;/a&gt; and Tailwind utilities classes while still caring about the user's motion settings (&lt;code&gt;prefers-reduced-motion&lt;/code&gt;).&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;transition-group&lt;/span&gt;
  &lt;span class="na"&gt;move-class=&lt;/span&gt;&lt;span class="s"&gt;"motion-safe:duration-1000 motion-safe:ease-in-out"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/transition-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What else? Well I just love playing with &lt;a href="https://www.cypress.io/"&gt;Cypress&lt;/a&gt;. Seeing those tests passing just feels me with joy. So I added a few test cases for good measure. 🧪&lt;/p&gt;

&lt;p&gt;And that's it! Try it out here if you have a Patreon account: &lt;a href="https://patreon-pledges-calculator.netlify.app/"&gt;https://patreon-pledges-calculator.netlify.app/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>vue</category>
      <category>cypress</category>
    </item>
    <item>
      <title>Weekend project: creating a media viewer</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Tue, 10 Aug 2021 15:41:33 +0000</pubDate>
      <link>https://dev.to/mornir/weekend-project-creating-a-media-viewer-3ef0</link>
      <guid>https://dev.to/mornir/weekend-project-creating-a-media-viewer-3ef0</guid>
      <description>&lt;p&gt;Try it out: &lt;a href="https://media-viewer.netlify.app/"&gt;https://media-viewer.netlify.app/&lt;/a&gt;&lt;br&gt;
Source code: &lt;a href="https://github.com/mornir/media-viewer-ts"&gt;https://github.com/mornir/media-viewer-ts&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I was unhappy with the media viewer applications available on Windows. After searching for a few hours, I couldn't find any program that fulfilled my short list of requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads both images and videos&lt;/li&gt;
&lt;li&gt;Navigates between media with keyboard or mouse wheel&lt;/li&gt;
&lt;li&gt;Autoplays video, preferably in a loop&lt;/li&gt;
&lt;li&gt;Works locally (direct read from the disk, no upload to server)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I thought I could just code my own application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Electron or not?
&lt;/h2&gt;

&lt;p&gt;I'm a front-end developer, which limits my possibilities. Since I needed read access to the disk, my first thought was to use Electron. I've never worked with Electron before and actually I've been avoiding it so far. My guess is that PWA or another tech is going to replace it in the short to medium term. So I was not too eager to learn how to build apps with Electron in 2021.&lt;br&gt;
While doing some research about Electron, I came across &lt;a href="https://www.electronjs.org/apps/lightgallery"&gt;this Electron application&lt;/a&gt;. It was exactly what I needed, except that it couldn't read videos.😫&lt;/p&gt;

&lt;p&gt;However, the same author also published &lt;a href="https://www.lightgalleryjs.com/"&gt;a great npm package&lt;/a&gt; which had more capabilities, among other reading video files.🥳&lt;/p&gt;

&lt;p&gt;Remembering hearing about &lt;a href="https://web.dev/file-system-access/"&gt;the new File Access API&lt;/a&gt;, I told myself &lt;em&gt;why not just build a website?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  webkitdirectory vs File Access API
&lt;/h2&gt;

&lt;p&gt;At first I thought that using the &lt;a href="https://web.dev/file-system-access/"&gt;File Access API&lt;/a&gt; was the only way to read the content of a folder. But actually the &lt;a href="https://www.smashingmagazine.com/2017/09/uploading-directories-with-webkitdirectory/"&gt;webkitdirectory&lt;/a&gt; attribute on the &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; element allows just that! So a basic &lt;code&gt;input&lt;/code&gt; tag with the &lt;code&gt;webkitdirectory&lt;/code&gt; attribute is exactly what I needed.&lt;/p&gt;

&lt;p&gt;I think that the File Access API is more for advanced usages, like letting users save changes directly back to the disk (see for example this &lt;a href="https://googlechromelabs.github.io/text-editor/"&gt;text editor demo&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  createObjectURL
&lt;/h2&gt;

&lt;p&gt;Media files are to be read locally, directly from the disk. But I still needed some sort of URL for the &lt;code&gt;src&lt;/code&gt; attributes of &lt;code&gt;img&lt;/code&gt; and &lt;code&gt;video&lt;/code&gt; tags. First I thought about converting images to base64, but this is a expensive process and it doesn't work for videos. &lt;a href="https://forweb.dev/en/blog/2020-05-05-object-url/"&gt;Like stated in this post&lt;/a&gt;, the right way is with URL.createObjectURL. This method creates a temporary local URL that can be used by the &lt;code&gt;src&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;The lifespan of the URL is bound to the HTML document. When the tab is closed, the URL is removed from memory. However it's best practice to free up memory as soon as the URL is no longer needed, using the URL.revokeObjectURL method and passing it the blob URL. In my case, I call it once images and videos have loaded or when the gallery is closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vite &amp;amp; Typescript
&lt;/h2&gt;

&lt;p&gt;I've always used Vite exclusively for Vue 3 projects, and I was pleasantly surprised to see that it works also great for "pure" TypeScript (&lt;code&gt;vanilla-ts&lt;/code&gt; Vite template). From now on, I will use Vite for all my front-end projects, even when I'm not using Vue.&lt;/p&gt;

&lt;p&gt;Try it out: &lt;a href="https://media-viewer.netlify.app/"&gt;https://media-viewer.netlify.app/&lt;/a&gt;&lt;br&gt;
Source code: &lt;a href="https://github.com/mornir/media-viewer-ts"&gt;https://github.com/mornir/media-viewer-ts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>pwa</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Sanity Backup Function with GitHub Actions and Artifacts</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Fri, 02 Apr 2021 16:18:21 +0000</pubDate>
      <link>https://dev.to/mornir/sanity-backup-function-with-github-actions-and-artifacts-18ca</link>
      <guid>https://dev.to/mornir/sanity-backup-function-with-github-actions-and-artifacts-18ca</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Warning: In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sanity-io/github-action-sanity#backup-routine" rel="noopener noreferrer"&gt;https://github.com/sanity-io/github-action-sanity#backup-routine&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Sanity is a hosted content platform that offers many features going beyond what we expect from a traditional CMS. One of their core focus is the developer experience. Thanks the Sanity CLI, it takes only two commands to bootstrap a project and deploy it to a free subdomain. Data are stored safely and in multiple copies on &lt;a href="https://cloud.google.com/customers/sanity" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt; servers. Thanks to the &lt;a href="https://www.sanity.io/docs/history-experience" rel="noopener noreferrer"&gt;document history&lt;/a&gt;, we can restore our documents to a previous state. However, deleted documents and datasets cannot be recovered. &lt;/p&gt;

&lt;p&gt;Even if those scenarios are unlikely to happen, it is worth creating a simple backup routine, just in case. And the method I'm going to show you here is easy to set up, won't cost you any money, and doesn't require you to register with a 3rd party service (I assume that all my readers have a GitHub account🙃).&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to backup Sanity datasets
&lt;/h2&gt;

&lt;p&gt;There are three ways to backup datasets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.sanity.io/docs/export" rel="noopener noreferrer"&gt;cURL request to an export URL endpoint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Using the &lt;a href="https://www.sanity.io/docs/dataset" rel="noopener noreferrer"&gt;Sanity CLI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Using the &lt;code&gt;@sanity/export&lt;/code&gt; &lt;a href="https://github.com/sanity-io/sanity/tree/next/packages/%40sanity/export" rel="noopener noreferrer"&gt;npm package&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In an another blog post, I explained how to use the &lt;code&gt;@sanity/export&lt;/code&gt; npm package inside a serverless function to back up content to &lt;a href="https://gist.github.com/mornir/066d3cf37a3d34857da7c8629465ab01" rel="noopener noreferrer"&gt;Google Drive&lt;/a&gt; or &lt;a href="https://gist.github.com/dan-dr/b2266796bdd7cf601868d93bb3cb4ded" rel="noopener noreferrer"&gt;Dropbox&lt;/a&gt;:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/mornir" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__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%2F59837%2F0792d72b-002a-41e5-8bae-485b0741c784.jpeg" alt="mornir"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/mornir/use-netlify-cloud-function-to-back-up-data-to-google-drive-49ep" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Use Netlify cloud function to back up data to Google Drive&lt;/h2&gt;
      &lt;h3&gt;Jérôme Pott ・ Sep 13 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#tutorial&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#serverless&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;There's however an easier way: GitHub Actions (GA). Here are their advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backup files are stored alongside your studio code.&lt;/li&gt;
&lt;li&gt;They only require a few lines of YAML config.&lt;/li&gt;
&lt;li&gt;They support CRON jobs.&lt;/li&gt;
&lt;li&gt;They are cheap (execution time + storage).&lt;/li&gt;
&lt;li&gt;We can make use of the GitHub ecosystem (notifications for failed workflows, access management, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Going full onboard with GitHub Actions
&lt;/h1&gt;

&lt;p&gt;There is a &lt;a href="https://github.com/sanity-io/github-action-sanity" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; that wraps the Sanity CLI. Basically, it means that we can run &lt;code&gt;sanity dataset export&lt;/code&gt; inside our GA workflow.&lt;br&gt;
Before we can export the dataset, we need to generate a read token from the Sanity project dashboard and &lt;a href="https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets" rel="noopener noreferrer"&gt;store it as a secret in the GitHub repository&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This is how the first workflow step looks like:&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;Export dataset&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sanity-io/github-action-sanity@v0.2-alpha&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SANITY_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SANITY_AUTH_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dataset export production backups/backup.tar.gz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we need to upload the generated backup file so that it will be available for download as a workflow artifact. For this, we use the &lt;a href="https://github.com/actions/upload-artifact" rel="noopener noreferrer"&gt;upload-artifact action&lt;/a&gt; and we specify the same path as above: &lt;code&gt;backups/backup.tar.gz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By default, this step passes even if GitHub cannot find our generated backup file. That is why I recommend setting the &lt;code&gt;if-no-files-found&lt;/code&gt; &lt;a href="https://github.com/actions/upload-artifact#customization-if-no-files-are-found" rel="noopener noreferrer"&gt;option&lt;/a&gt; to &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And here's the details of the step:&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;Upload backup.tar.gz&lt;/span&gt;
&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v2&lt;/span&gt;
&lt;span class="na"&gt;with&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;backup-tarball&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backups/backup.tar.gz&lt;/span&gt;
  &lt;span class="c1"&gt;# Fails the workflow if no files are found; defaults to 'warn'&lt;/span&gt;
  &lt;span class="na"&gt;if-no-files-found&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to running the backup routine on a schedule, you also add an option to trigger the backup process manually from the GA dashboard. This can be useful in various situations, e.g. right after content editors added a large amount of data, or right before manipulating datasets.&lt;/p&gt;

&lt;p&gt;Here's an example of a workflow triggered manually or by a CRON job:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Runs at 04:00 UTC on the 1st and 17th of every month&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&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="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/16&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="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We now have set a solid backup routine in place. You can of course tune the frequency of the backups to your needs. Make sure to also read the latest information about pricing, size limits and file retention from GitHub. For example, as of writing this, backup files are automatically deleted after 90 days on public repo. I personally think that 90 days is long enough, even too long maybe. If you want to keep backups files for a shorter time, you can do so in the repository settings under &lt;em&gt;Actions&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Finally, if you would like to see the workflow described in this post along with the generated artifacts, you can visit this page: &lt;a href="https://github.com/mornir/movies-studio/actions/workflows/main.yml" rel="noopener noreferrer"&gt;https://github.com/mornir/movies-studio/actions/workflows/main.yml&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.sanity.io/" rel="noopener noreferrer"&gt;Sanity.io&lt;/a&gt;: Get the most out of your content
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.sanity.io/" rel="noopener noreferrer"&gt;Sanity.io&lt;/a&gt; is a platform to build websites and applications. It comes with great APIs that let you treat content like data. Give your team exactly what they need to edit and publish their content with the customizable Sanity Studio. Get real-time collaboration out of the box. Sanity.io comes with a hosted datastore for JSON documents, query languages like GROQ and GraphQL, CDNs, on-demand asset transformations, presentation agnostic rich text, plugins, and much more.&lt;/p&gt;

&lt;p&gt;Don't compromise on developer experience. Join thousands of developers and trusted companies and power your content with Sanity.io. Free to get started, pay-as-you-go on all plans.&lt;/p&gt;

</description>
      <category>github</category>
      <category>tutorial</category>
      <category>sanity</category>
      <category>headless</category>
    </item>
    <item>
      <title>?? noopener, noreferrer, nofollow ¿¿</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Sun, 21 Mar 2021 07:59:32 +0000</pubDate>
      <link>https://dev.to/mornir/noopener-noreferrer-nofollow-42cl</link>
      <guid>https://dev.to/mornir/noopener-noreferrer-nofollow-42cl</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;noopener&lt;/strong&gt; &lt;br&gt;
It's no longer necessary to write &lt;code&gt;rel="noopener"&lt;/code&gt; because &lt;code&gt;noopener&lt;/code&gt; is now the default behavior in all modern browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;noreferrer&lt;/strong&gt; &lt;br&gt;
I couldn't find any example where I would want to use this keyword. If you know about one, please share it below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nofollow&lt;/strong&gt; &lt;br&gt;
Great for user generated content, combined with the new &lt;code&gt;ugc&lt;/code&gt; keyword: &lt;code&gt;rel="nofollow ugc"&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've never been 100% clear about the difference between the values of the &lt;code&gt;rel&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags. I decided to do some research and take some notes. Here is a summary for myself and anybody interested. Please do post a comment below if I'm wrong about anything and I'll update the post accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;noopener&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;External links with &lt;code&gt;target=blank&lt;/code&gt; open in a new tab. By default, that new tab has access to &lt;code&gt;window.opener&lt;/code&gt;, which points to the origin page. The newly opened website has access to that object and can even change the value of &lt;code&gt;window.opener.location&lt;/code&gt;. Yes, it can force the origin page to navigate to a new URL. The worst part is that the user may not even notice the programmatic navigation because the new tab is focused by default. &lt;/p&gt;

&lt;p&gt;Imagine a user scrolling through his Facebook thread. He sees an interesting article teaser and clicks the external link that opens in a new tab. Once he's done reading the article, he goes back to the Facebook tab. Strangely, Facebook is asking for his credentials again. He naively enters them again and falls into the trap: when he opened the article, his Facebook page was programmatically navigated to another website that pretends to be Facebook. This method of deceiving users and stealing their information is called &lt;em&gt;tabnagging&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Adding the keyword &lt;code&gt;noopener&lt;/code&gt; prevents the opened external tab from accessing &lt;code&gt;window.opener&lt;/code&gt;. &lt;br&gt;
Why is this &lt;code&gt;noopener&lt;/code&gt; not the default behavior you may ask? Well, it is now in all in all modern browsers. You can test it yourself &lt;a href="https://mathiasbynens.github.io/rel-noopener/"&gt;with this web experiment&lt;/a&gt;. However, since the change is very recent, it doesn't hurt to keep it around a little longer, especially since most of the time it is outputted by default by tools like CMS or components libraries.&lt;/p&gt;

&lt;p&gt;But this update to browsers is good news for new web developers who will never have to worry about this security vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;noreferrer&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;noferrer&lt;/code&gt; keyword prevents access to &lt;code&gt;window.opener&lt;/code&gt; (exactly like &lt;code&gt;noopener&lt;/code&gt;), but also prevents the HTTP &lt;code&gt;Referer&lt;/code&gt; header from being sent to the linked website.&lt;/p&gt;

&lt;p&gt;While it has no impact on SEO, it affects the web analytics of the linked website. Visits from your website to the linked website will be counted as &lt;strong&gt;direct traffic&lt;/strong&gt; in referral traffic (instead of showing your website as the source). As a webmaster, I want to appear in the analytics of the websites I link to. When they see traffic from my website, they will most probably check it out and maybe share the page on social media, follow me or even decide to return the favor by linking back to my website.&lt;/p&gt;

&lt;p&gt;So write &lt;code&gt;rel="noreferrer"&lt;/code&gt; for outgoing links when you don’t want other sites to know that you are linking to them. &lt;strong&gt;However I cannot think of any concrete example.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, you obviously shouldn't use the &lt;code&gt;noreferrer&lt;/code&gt; keyword on internal links as it will mess up your analytics reports.&lt;/p&gt;

&lt;p&gt;The reason you may find this keyword recommended in best practices is because &lt;code&gt;noopener&lt;/code&gt; is not supported by Internet Explorer, thus &lt;code&gt;noreferrer&lt;/code&gt; is used on external links to prevent &lt;em&gt;tabnagging&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What about user privacy?&lt;br&gt;
Sharing full URL paths with external websites is not good for user privacy and it's not useful for analytics.&lt;br&gt;
Here, you should turn to the referrer policy, by setting it to &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt;. In some web browsers (e.g. Firefox v87), &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; is already the default value.&lt;br&gt;
This topic is however out of the scope of this blog post and I strongly encourage you to read this excellent article:&lt;br&gt;
&lt;a href="https://web.dev/referrer-best-practices/"&gt;https://web.dev/referrer-best-practices/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;nofollow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Spiderbots crawl websites, collect and follow all links they encounter. If you don't want crawlers to follow certain links, you can add &lt;code&gt;nofollow&lt;/code&gt;. Keep in mind that it's only a hint: the spiderbots may choose to ignore the directive and use &lt;code&gt;nofollow&lt;/code&gt; links for rankings&lt;/p&gt;

&lt;p&gt;When dealing with user-generated content, it's complicated to check all the external links that are posted. Some links may lead to websites with a bad reputation and this will hurt your SEO ranking. This is where the &lt;code&gt;nofollow&lt;/code&gt; keyword comes in handy. You can also use the new &lt;code&gt;ugc&lt;/code&gt; keyword, but not all spiderbots understand it, so keep both keywords: &lt;code&gt;rel="nofollow ugc"&lt;/code&gt;. You can see an example of it on any links posted in the comment sections of css-tricks.com.&lt;/p&gt;

&lt;p&gt;You can also use &lt;code&gt;nofollow&lt;/code&gt; for pages on your website that you don't want to be indexed, but I think it's better to use the &lt;code&gt;robots.txt&lt;/code&gt; file rather than to add &lt;code&gt;nofollow&lt;/code&gt; on links that point to those pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.reliablesoft.net/noreferrer-noopener/"&gt;What rel="noreferrer noopener" Mean and How it Affects SEO&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ahrefs.com/blog/nofollow-ugc-sponsored/"&gt;The State of Nofollow, UGC, &amp;amp; Sponsored Link Attributes in 2020&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://moz.com/blog/nofollow-sponsored-ugc"&gt;How Google's Nofollow, Sponsored, &amp;amp; UGC Links Impact SEO&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://web.dev/external-anchors-use-rel-noopener/"&gt;Links to cross-origin destinations are unsafe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://searchfacts.com/rel-noreferrer-guide/"&gt;What rel="noreferrer" Does and How to Remove It&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.forbes.com/sites/johnrampton/2017/11/06/the-difference-between-nofollow-and-noreferrer-and-why-it-matters/"&gt;The Difference Between Nofollow and Noreferrer, and Why it Matters&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Webhooks with Serverless Function</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Sat, 13 Mar 2021 12:23:40 +0000</pubDate>
      <link>https://dev.to/mornir/webhooks-with-serverless-function-2h5k</link>
      <guid>https://dev.to/mornir/webhooks-with-serverless-function-2h5k</guid>
      <description>&lt;p&gt;&lt;em&gt;In this blog post, I'm going to show you how I set up a serverless function on Cloudflare Workers that reposts messages originally published on Habitica's chat to a Discord channel&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Habitica is a great platform for building good habits and boosting productivity. Its social aspect is also one of its key features. Users can form parties in order to hold each other accountable. Every party has its own simple chat page where users can discuss and where system messages are posted (skills used, damages dealt, quests started, etc.). But when it comes to creating a social space for chatting and talking, Discord is currently the best free solution out there. So my party and I moved to a Discord server. The problem is that system messages are still posted on Habitica.&lt;/p&gt;

&lt;p&gt;That is why I looked into a way to automatically repost those messages to a dedicated Discord channel. And it turned out that it is quite easy to set up thanks to the flexible webhooks provided by both Discord and Habitica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a Discord Bot
&lt;/h2&gt;

&lt;p&gt;From &lt;a href="https://support.discord.com/hc/en-us/articles/228383668" rel="noopener noreferrer"&gt;Discord's official instructions&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your &lt;strong&gt;Server Settings&lt;/strong&gt; and head into the &lt;strong&gt;Integrations&lt;/strong&gt; tab:&lt;/li&gt;
&lt;li&gt;Click the "&lt;strong&gt;Create Webhook&lt;/strong&gt;" button to create a new webhook!&lt;/li&gt;
&lt;li&gt;You can edit the avatar and the name of the webhook&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose what channel the Webhook posts to.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You can copy the Webhook URL&lt;/li&gt;
&lt;/ol&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%2Fuploads%2Farticles%2Fmr0m9liatycduce65yuc.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%2Fuploads%2Farticles%2Fmr0m9liatycduce65yuc.PNG" alt="Discord webhook settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a Serverless Function
&lt;/h2&gt;

&lt;p&gt;Habitica cannot post directly to Discord. We need an intermediate. No-code solutions like Zapier are neat, but here we need more fine-grained control over the request in order to format the messages in the way we like and serverless functions are ideals for this use case.&lt;/p&gt;

&lt;p&gt;There are many different cloud function providers out there, but for this project, I wanted to try out &lt;a href="https://workers.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt;. Cloudflare Workers have two serious advantages over other providers: they are incredibly cheap and their start-up time (cold start) is near-instant, not that we need those features for the current project.&lt;/p&gt;

&lt;p&gt;Cloudflare also provides a simple yet convenient online code editor to code the serverless function. Since our code won't be complex, we'll write our function directly inside the online editor. Here's the complete code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// See below the structure of the data object&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// Only repost system messages&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&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="c1"&gt;// Discord expects a JSON payload that looks like this { content: 'hello world'}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&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;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DISCORD_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&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="s2"&gt;Content-Type&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;application/json&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Habitica always expects a 200 response, otherwise it will disable the webhook&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body of the post request coming from the Habitica webhook will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XXXXX-c888-4dbf-aa0e-fc317c9c8f8c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"super_squad "&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"flagCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"03e9b0d6-XXX-442a-9659-fde24aec2842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"03e4b0d6-XXX-442a-9659-fde24aec2841"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mornir casts Earthquake for the party."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unformattedText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mornir casts Earthquake for the party."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spell_cast_party"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mornir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wizard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"spell"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"earth"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2021-03-10T17:00:40.141Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"likes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groupId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5f38dbe06649-555-aa0e-fc317c9cbf8c"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"webhookType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"groupChatReceived"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5454544ea-134d-4e37-XXX-310351b35729"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DISCORD_WEBHOOK_URL&lt;/code&gt; is the Webhook URL we got in step 1, stored here as an environment variable: &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%2Fuploads%2Farticles%2F178ws09x1r5j7munnpbv.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%2Fuploads%2Farticles%2F178ws09x1r5j7munnpbv.png" alt="Cloudflare environment variables"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create a Habitica Webhook
&lt;/h2&gt;

&lt;p&gt;The official UI for creating webhooks, found &lt;a href="https://habitica.com/user/settings/api" rel="noopener noreferrer"&gt;on the settings page&lt;/a&gt;, is a bit lacking compared to &lt;a href="https://habitica.com/apidoc/#api-Webhook-AddWebhook" rel="noopener noreferrer"&gt;what is available on the Habitica API&lt;/a&gt;. We can only add and delete URLs, but in our case, we want to listen to a specific event occurring in a specific chat room. That is why I used an alternative editor for Habitica webhooks, which includes all of the webhook options that are missing from the main site.&lt;br&gt;
Website: &lt;a href="https://robwhitaker.com/habitica-webhook-editor/" rel="noopener noreferrer"&gt;https://robwhitaker.com/habitica-webhook-editor/&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://habitica.fandom.com/wiki/Habitica_Webhook_Editor" rel="noopener noreferrer"&gt;https://habitica.fandom.com/wiki/Habitica_Webhook_Editor&lt;/a&gt;&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%2Fuploads%2Farticles%2F7ka98d75gx4ur1457v6x.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%2Fuploads%2Farticles%2F7ka98d75gx4ur1457v6x.PNG" alt="Habitica alternative webhook editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The newly created hook will then appear on our &lt;a href="https://habitica.com/user/settings/api" rel="noopener noreferrer"&gt;Habitica account settings page&lt;/a&gt;:&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%2Fuploads%2Farticles%2F7azqbsk0k4pnbznsqopx.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%2Fuploads%2Farticles%2F7azqbsk0k4pnbznsqopx.PNG" alt="Habitica webhooks settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Voilà! We now have a working Habitica bot that reposts system messages!&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%2Fuploads%2Farticles%2F4gxijzxb17kqzbe8rd9q.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%2Fuploads%2Farticles%2F4gxijzxb17kqzbe8rd9q.PNG" alt="Discord chat preview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, there are much more things we can do! Here are some ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Link abilities and bosses to their corresponding wiki page or bosses&lt;/li&gt;
&lt;li&gt;Send warning if member deal too much damage to its party&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudflare Workers let us also &lt;a href="https://developers.cloudflare.com/workers/learning/how-kv-works" rel="noopener noreferrer"&gt;store data inside a database&lt;/a&gt;. Having information that persists between function invocations opens new exciting possibilities!&lt;/p&gt;

</description>
      <category>webhook</category>
      <category>serverless</category>
      <category>discord</category>
      <category>bot</category>
    </item>
    <item>
      <title>My personal experience with XState</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Fri, 01 Jan 2021 12:17:31 +0000</pubDate>
      <link>https://dev.to/mornir/my-personal-experience-with-xstate-39kd</link>
      <guid>https://dev.to/mornir/my-personal-experience-with-xstate-39kd</guid>
      <description>&lt;blockquote class="ltag__twitter-tweet"&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--F1YgPzW6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1336281436685541376/fRSl8uJP_normal.jpg" alt="Dan Abramov profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Dan Abramov
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="mentioned-user" href="https://dev.to/dan_abramov"&gt;@dan_abramov&lt;/a&gt;

      &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;
      State machines are great, so why aren’t we rewriting all of our async/await code to them?
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      04:37 AM - 31 Dec 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1344502932704788480" 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=1344502932704788480" 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=1344502932704788480" 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;p&gt;Over the Christmas holiday, I dove into &lt;a href="https://xstate.js.org/docs/about/concepts.html#finite-state-machines"&gt;finite state machines&lt;/a&gt; because I was intrigued by its popularity. I built a couple of small apps using &lt;a href="https://xstate.js.org/docs/"&gt;XState&lt;/a&gt; with React hooks, Vue 2 and Vue 3 (composition API), using TypeScript for every project.&lt;/p&gt;

&lt;p&gt;I was seduced by the concept of finite state machines. For example, in my project with Vue 2 I was dealing with multiple intricate states, and I was eager to clear things out thanks to XState. It took me a while to get a grasp on how to use that library, which has a large surface area and is not easy to get started with. I took &lt;a href="https://www.udemy.com/course/introduction-to-state-machines-with-xstate-and-react"&gt;a course on Udemy&lt;/a&gt;, which provided me with a solid foundation.&lt;/p&gt;

&lt;p&gt;However, I quickly found out that XState doesn't play well with Vue 2 and that I would need to use &lt;a href="https://vuex.vuejs.org/"&gt;Vuex&lt;/a&gt; anyway. . My original thought was to replace Vuex with XState and I don't see the point of having to state management solutions.&lt;/p&gt;

&lt;p&gt;Then I built a second, smaller project with Vue 3 and its composition API. This time, the integration of XState was smooth and I was able to manage my data globally thanks to XState context. Since the project was quite small, I can't really tell if this approach would scale well in a larger codebase.&lt;/p&gt;

&lt;p&gt;The experience with React Hooks was very similar to Vue 3 Composition API. I also would like to say that XState integrates very well with TypeScript.&lt;/p&gt;

&lt;p&gt;In conclusion, I think that XState is not a good fit for the websites I usually build for clients, which are JAMstack websites with simple state logic. But it's true that XState forces developers to think of all the possible outcomes and a seemingly simple state logic can actually hide complex workflows and possible paths. However, covering all those paths takes time, and time is money for the client. It's sad, but it's not rare that I have to build forms where in case of an error I just log it to the console.&lt;/p&gt;

&lt;p&gt;However, if I had more time, I wouldn't implement XState, but rather add a bug tracking solution (LogRocket, Sentry, Bugsnap, etc.) or write E2E tests.&lt;/p&gt;

&lt;p&gt;I'm persuaded that XState is the perfect tool for specific applications, but it definitely doesn't make sense to use it every time. For example, I find the &lt;a href="https://xstate.js.org/docs/examples/todomvc.html"&gt;TodoMVC example on the XState website&lt;/a&gt; to be incredibly complex and convoluted.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://www.reddit.com/r/reactjs/comments/ilfi4c/my_experience_with_using_state_machines_xstate_in/"&gt;More opinions on XState on Reddit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>learning</category>
      <category>codenewbie</category>
    </item>
    <item>
      <title>Change the favicon color in dark mode! 🤯</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Mon, 09 Nov 2020 13:44:24 +0000</pubDate>
      <link>https://dev.to/mornir/change-the-favicon-color-in-dark-mode-3amj</link>
      <guid>https://dev.to/mornir/change-the-favicon-color-in-dark-mode-3amj</guid>
      <description>&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1321083538494676994-642" src="https://platform.twitter.com/embed/Tweet.html?id=1321083538494676994"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1321083538494676994-642');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1321083538494676994&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;h3&gt;
  
  
  Favicon using a light theme
&lt;/h3&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%2Fxeyi0n7vc4u1yz8f2zo1.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%2Fxeyi0n7vc4u1yz8f2zo1.PNG" alt="favicon light mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Favicon using a dark theme
&lt;/h3&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%2F1nhas9il92s8irejwrpq.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%2F1nhas9il92s8irejwrpq.PNG" alt="favicon dark mode"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>html</category>
      <category>svg</category>
      <category>css</category>
    </item>
    <item>
      <title>Building my first web extension</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Sun, 25 Oct 2020 09:30:38 +0000</pubDate>
      <link>https://dev.to/mornir/building-my-first-web-extension-4h71</link>
      <guid>https://dev.to/mornir/building-my-first-web-extension-4h71</guid>
      <description>&lt;p&gt;Since I've learned that web extensions speak the same languages as websites (HTML, CSS and JavaScript), I've always wanted to give it a try. This post is less a tutorial and more a summary of my experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Getting started is indeed very easy:&lt;br&gt;
&lt;a href="https://css-tricks.com/how-to-build-a-chrome-extension/"&gt;https://css-tricks.com/how-to-build-a-chrome-extension/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was able to quickly get something working, however, I also quickly faced some limitations that required me to set up a build pipeline in order to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use npm modules&lt;/li&gt;
&lt;li&gt;work with promises (browser APIs use callbacks)&lt;/li&gt;
&lt;li&gt;have my extension automatically work both in Chrome and Firefox &lt;/li&gt;
&lt;li&gt;have hot module replacement (HMR)&lt;/li&gt;
&lt;li&gt;use Vue components and TailwindCSS&lt;/li&gt;
&lt;li&gt;in short: have my dev environment as close a possible to my usual environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, I found this awesome plugin that takes care of all the above-mentioned points. &lt;br&gt;
This plugin scaffolds a web extension project structure and includes the essential &lt;a href="https://github.com/mozilla/webextension-polyfill"&gt;webextension-polyfill&lt;/a&gt;.&lt;br&gt;
All I had left to do with to &lt;a href="https://dev.to/mornir/add-tailwind-to-your-vue-app-5hea"&gt;set up TailwindCSS&lt;/a&gt; and I was ready to code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging web extensions
&lt;/h2&gt;

&lt;p&gt;I didn't find how to use the Vue devtools extension for debugging, but in my case, logging to the console was sufficient.&lt;/p&gt;

&lt;p&gt;To see the console logs when debugging the popup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome: Open dev tools, right-click inside the popup and select "inspect"&lt;/li&gt;
&lt;li&gt;Firefox: Click the inspect button on the screen where you added your extension as a temporary extensions
[image]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the settings page, you can set &lt;code&gt;options_page&lt;/code&gt; directly to &lt;code&gt;options.html&lt;/code&gt;, which will open it as a full page.&lt;/p&gt;

&lt;p&gt;You can also open the popup as a normal web page by typing its URL: &lt;code&gt;chrome-extension://your_extension_id/popup.html&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What about testing?
&lt;/h2&gt;

&lt;p&gt;Thanks to the simplicity of Cypress, I've started to add tests to my projects. However, &lt;a href="https://github.com/cypress-io/cypress/issues/1965"&gt;Cypress currently cannot visit web extension&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Then I found &lt;a href="https://www.streaver.com/blog/posts/testing-web-extensions.html"&gt;a great blog post about testing web extensions with Jest and Puppeteer&lt;/a&gt;. The showcased extension is even built with Vue.&lt;/p&gt;

&lt;p&gt;Thanks to that post, I was able to quickly set up Jest. However, I quickly realized that the History API I was using was not mocked by the &lt;a href="https://github.com/clarkbw/jest-webextension-mock"&gt;jest-webextension-mock library&lt;/a&gt;🙄 &lt;/p&gt;

&lt;p&gt;What I ended up doing is to only unit test the main functions with Jest. As fixtures for the tests, I exported a sample browsing history as JSON. &lt;/p&gt;

&lt;p&gt;The post later mentioned E2E testing with Puppeteer. I tried to set it up but ran into errors from the Jest integration. At that point, I told myself that I shouldn't spend more time on that topic and move on to finally publishing my extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing my extension
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Chrome Web Store
&lt;/h3&gt;

&lt;p&gt;The developer dashboard seemed to have recently been redesign. I found it very easy to navigate. There are many contextual info-bubbles that provide useful information. The whole process was very straightforward. I just had to fill out all the required information and pay the entrance fee of $5 to submit my extension for review. Two days later, it was published.&lt;/p&gt;

&lt;p&gt;Link to Chrome store page: &lt;a href="https://chrome.google.com/webstore/detail/track-it/kjdclicjmhibgokfflkhfccdillnkfbk"&gt;https://chrome.google.com/webstore/detail/track-it/kjdclicjmhibgokfflkhfccdillnkfbk&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add-ons for Firefox
&lt;/h3&gt;

&lt;p&gt;At Firefox, the submission process was also easy and the review also only took two days. Unlike Chrome, I had to upload the source code. I am actually surprised that Chrome didn't ask for it. Maybe paying the fee with a credit card number provided enough insurance.&lt;/p&gt;

&lt;p&gt;Link to Firefox store page: &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/track-it/"&gt;https://addons.mozilla.org/en-US/firefox/addon/track-it/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Now go build your own extension!
&lt;/h2&gt;

&lt;p&gt;Don't fret too much about the fact that there are probably extensions that will be similar/better than yours. With that kind of thinking, you'll never get started. If this doesn't help, take it as a learning exercise.&lt;/p&gt;

&lt;p&gt;See my extension: it tracks the time since the last visit to websites... There are plenty of apps that do a similar job and boast more features. But it solved a specific need I had and I use it daily.&lt;/p&gt;

&lt;p&gt;You check its source code here:&lt;br&gt;
&lt;a href="https://github.com/mornir/track-it"&gt;https://github.com/mornir/track-it&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@aronvisuals"&gt;Aron Visuals&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>showdev</category>
      <category>learning</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to handle content previews from Sanity in Nuxt</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Sun, 16 Aug 2020 10:28:49 +0000</pubDate>
      <link>https://dev.to/mornir/how-to-handle-content-previews-from-sanity-in-nuxt-3127</link>
      <guid>https://dev.to/mornir/how-to-handle-content-previews-from-sanity-in-nuxt-3127</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the Jamstack, pages are generated at build time. The static assets can then be deployed to a CDN network and served quickly to the visitors. However, this approach means that the pages cannot be built by the server &lt;em&gt;on the fly&lt;/em&gt;, for example when content editors want to preview their content before publishing it.&lt;/p&gt;

&lt;p&gt;So how can we solve that problem? Well, this is still a domain where innovation is currently happening, but a few solutions already exist. The easiest solution is to simply generate a preview deploy, so basically rebuild the whole website on a test URL. This approach is only viable if rebuild time is very short. Incremental builds would be the solution here, but we're not there yet.&lt;/p&gt;

&lt;p&gt;Another approach is to take advantage of the fact that our statically generated Nuxt website hydrates into a full-blown single page application. We can then use JavaScript on the client side to dynamically fetch the content from the CMS. Thankfully, Nuxt &amp;gt; v2.13 makes our lives easy with its new and shiny &lt;a href="https://nuxtjs.org/guides/features/live-preview"&gt;preview mode&lt;/a&gt;. ✨&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling preview mode in Nuxt (&amp;gt; v2.13)
&lt;/h2&gt;

&lt;p&gt;Create a plugin named &lt;code&gt;preview.client.js&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// plugins/preview.client.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enablePreview&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enablePreview&lt;/span&gt;&lt;span class="p"&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;p&gt;Yes! That's it! Now if the URL contains the query param &lt;code&gt;?preview=true&lt;/code&gt;, the enablePreview method will be invoked. Nuxt will then "discard" the data coming from the server and calls nuxtServerInit, asyncData and fetch on the client side.&lt;br&gt;
To test the preview mode locally, you need to run &lt;code&gt;nuxt generate&lt;/code&gt; and then &lt;code&gt;nuxt start&lt;/code&gt;. You can now see in the network tab that Nuxt makes calls to the API when the preview query parameter is set.&lt;/p&gt;
&lt;h3&gt;
  
  
  Previewing brand new pages
&lt;/h3&gt;

&lt;p&gt;By &lt;em&gt;brand new pages&lt;/em&gt;, I mean pages that have never been deployed. We don't want Nuxt to show the 404 page to the content editor when he/she wants to preview a brand new page.&lt;/p&gt;

&lt;p&gt;There's a SPA fallback that we can activate by setting &lt;code&gt;generate.fallback&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in &lt;code&gt;nuxt.config.js&lt;/code&gt;. Now Nuxt won't default to a 404 and will try to render the page by making an API call to the CMS.&lt;/p&gt;

&lt;p&gt;But we still want to show the 404 page when normal users of the website visit such pages. The validate hook was designed for this situation.&lt;br&gt;
What I usually do is store all slugs in Vuex (via the nuxtServerInit action) and check against the store in the validate hook if the page exists. However, don't forget to provide a "escape hatch" for the preview mode:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If FALSE redirect to 404 page&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;moviesSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&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;h2&gt;
  
  
  Generating the target URL in the CMS
&lt;/h2&gt;

&lt;p&gt;In Sanity, content editors can open in a new tab the target URL of the page they want to preview or they can display it inside an iframe directly in the Studio.&lt;/p&gt;
&lt;h3&gt;
  
  
  Opening the preview in a new tab
&lt;/h3&gt;

&lt;p&gt;Follow &lt;a href="https://www.sanity.io/docs/preview-content-on-site"&gt;these instructions&lt;/a&gt; and add the content below to the file you created.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;resolveProductionUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only show the preview option for documents for which a preview makes sense.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;movie&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://nuxt-sanity-movies.netlify.app/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/?preview=true`&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Showing the preview inside an iFrame
&lt;/h3&gt;

&lt;p&gt;Create a JS file with the following content and add it as a new part in sanity.json, in the same fashion you did above.&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="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sanity/desk-tool/structure-builder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://nuxt-sanity-movies.netlify.app/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WebPreview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nb"&gt;document&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;displayed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;displayed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`/?preview=true`&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;iframe&lt;/span&gt;
      &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;targetURL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;frameBorder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getDefaultDocumentNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schemaType&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="c1"&gt;// Conditionally return a different configuration based on the schema type&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schemaType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;movie&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WebPreview&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Web Preview&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;WebPreview&lt;/code&gt; is a React component (Sanity is a React SPA), but otherwise the code is just JavaScript and should be easy to follow for Vue developers. You can always refer to Sanity's &lt;a href="https://www.sanity.io/docs/structure-builder-typical-use-cases#tabs-with-content-previews-a8e5cc70dbc0"&gt;extensive documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Handling draft preview
&lt;/h2&gt;

&lt;p&gt;You will notice that drafts are not fetched. The reason is that draft documents don't appear on the API to unauthenticated users. In order to preview drafts, the easiest way is to use auth cookie. When content editors log into the CMS, an auth cookie is automatically set by Sanity in their browser. If the Sanity client was initialized with &lt;code&gt;withCredentials&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt;, the cookie will be passed along with each request. And don't forget to allow credentials for your API point in the Sanity Dashboard.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sanity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xxxxxxx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useCdn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;withCredentials&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="c1"&gt;// Add this line&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The problem now is that both versions will be returned: the published document and the draft. So if there's a draft, we'll get an array with two objects and using the default alphanumeric ordering, we don't know which version comes first. That is why we need to filter the result by &lt;code&gt;_updateDate&lt;/code&gt; in our Nuxt page to be sure to get the draft first.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// _slug.vue&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$sanity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*[_type == 'movie' &amp;amp;&amp;amp; slug.current == $slug] | order(_updatedAt desc)[0]&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="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&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;h3&gt;
  
  
  Be careful with content validation when previewing drafts
&lt;/h3&gt;

&lt;p&gt;Validation rules set in the Sanity Studio &lt;strong&gt;do not&lt;/strong&gt; affect drafts. So make sure that empty required fields don't break the front end preview. The easy way guard against this is to add some v-if directives where code might break.&lt;/p&gt;

&lt;p&gt;One important information that we should check for beforehand is the slug field. If this field isn't set, we should show no preview options.&lt;/p&gt;

&lt;p&gt;Disable the &lt;em&gt;Open preview&lt;/em&gt; option in the document context menu:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the iframe, the best way is to simply show a custom HTML page:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Please set a slug to see a preview&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Already done!
&lt;/h2&gt;

&lt;p&gt;What I like the most about this whole approach is that we only had to make a few edits to our pages or Vue components. The need to add a filter to get the draft first is a but unfortunate, but I couldn't find a better way.&lt;br&gt;
Another interesting detail I noticed is that once the enabePreview method has been called, the Nuxt app stays in preview mode after navigating to different pages. So a content editor can preview his blog post and then navigate to the blog listing page to see how his post teaser looks like.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/3oF1WdsJ-s8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;
&lt;h2&gt;
  
  
  Going the extra mile 🏃
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Preview banner with $nuxt.refresh
&lt;/h3&gt;

&lt;p&gt;In our current implementation, content editors need to reload the page (= reinitialize the whole Nuxt app) when they want to refresh the preview.&lt;br&gt;
When the preview is opened in a new tab, this is not an issue, but when it's opened inside the iframe in the Sanity Studio, we don't want content editors to reload the whole CMS or have to close and reopen the iframe (and thus losing their scroll position on the page).&lt;/p&gt;

&lt;p&gt;In Nuxt v2.9.0, a nifty feature was quietly added and was only &lt;a href="https://nuxtjs.org/guides/concepts/context-helpers#refreshing-page-data"&gt;properly documented&lt;/a&gt; later: the &lt;code&gt;$nuxt.refresh&lt;/code&gt; context helper. Basically, it allows us to call asyncData (and fetch) on the client side, without doing a full reload.&lt;/p&gt;

&lt;p&gt;This gave me the idea of showing a banner when the preview mode is activated. That banner contains a button which invokes $nuxt.refresh. Content editors can click that button to refresh the preview.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/DFQDVkN6hck"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's a GitHub repo with an example about how to implement such a banner. I had to wrap the banner inside client-only tags because otherwise there would be a mismatch between the client and the server node tree and the hydration would break.&lt;/p&gt;

&lt;p&gt;Another built-in property that is useful for our banner component is &lt;code&gt;$nuxt.isPreview&lt;/code&gt;, which returns &lt;code&gt;true&lt;/code&gt; if we're in preview mode. It means that we can easily show our banner conditionally in this way:&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;PreviewBanner&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"$nuxt.isPreview"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Real-time preview (the holy grail?)
&lt;/h2&gt;

&lt;p&gt;In Sanity, every edit is saved in real-time and the Sanity client even comes with a listen method to react to content changes. You can set up the listener in the mounted hook of the page to be previewed.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$sanity&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*[_type == "movie" &amp;amp;&amp;amp; slug.current == $slug][0]&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="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&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;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;update&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/VyEbXg2n-1I"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;However, this method doesn't retrieve the &lt;a href="https://www.sanity.io/docs/reference-type"&gt;references&lt;/a&gt; of the fetched document. If your document has no references, the Sanity listener is perfect. Otherwise one possible solution would be to call $nuxt.refresh in the subscribe callback:&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;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;update&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$nuxt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&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;&lt;strong&gt;But I would advise against that, for four reasons:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do content editors really need a real-time preview? As a web developer, I'm excited about the real-time preview, but I personally doubt that it's very useful in practice.&lt;/li&gt;
&lt;li&gt;It could get expensive (&lt;em&gt;financially and computationally&lt;/em&gt;). While the Sanity listen method uses WebSockets, our implementation with $nuxt.refresh will fire additional HTTP requests at every keystroke in the Studio.&lt;/li&gt;
&lt;li&gt;In my implementation, the content shown in the front end was always on step/update behind. (This can probably be fixed.)&lt;/li&gt;
&lt;li&gt;I prefer using the lightweight &lt;a href="https://github.com/rexxars/picosanity"&gt;PicoSanity&lt;/a&gt; client, which doesn't support the listen method.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Glad to hear your feedback on this
&lt;/h2&gt;

&lt;p&gt;What are your thoughts about the preview banner? Are you already using the new Nuxt preview mode? How do usually handle previews from Sanity in your Nuxt apps? What do you think of a real-time preview for content editing? I personally have never used Sanity as a content editor, so it's hard for me to judge.&lt;br&gt;
Share your opinion and experience in the comment section below!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All the code snippets are taken from the following repositories. The Studio uses the sci-fi movies test dataset offered by the Sanity CLI when initializing a new project.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i3JOwpme--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/mornir"&gt;
        mornir
      &lt;/a&gt; / &lt;a href="https://github.com/mornir/movies-web"&gt;
        movies-web
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🍿 Simple Nuxt (full static) + Sanity with preview mode
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i3JOwpme--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/mornir"&gt;
        mornir
      &lt;/a&gt; / &lt;a href="https://github.com/mornir/movies-studio"&gt;
        movies-studio
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🎬 A Sanity Studio initialized with the sci-fi movies dataset
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>jamstack</category>
      <category>nuxt</category>
      <category>sanity</category>
      <category>headless</category>
    </item>
    <item>
      <title>How to add Mapbox to your Nuxt static site</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Thu, 11 Jun 2020 16:45:17 +0000</pubDate>
      <link>https://dev.to/mornir/how-to-add-mapbox-to-your-nuxt-static-site-b59</link>
      <guid>https://dev.to/mornir/how-to-add-mapbox-to-your-nuxt-static-site-b59</guid>
      <description>&lt;p&gt;In this blog post, I'm going to show how to display a Mapbox map in a Nuxt static app using the official Mapbox GL JS library. &lt;/p&gt;

&lt;p&gt;In my experience I wouldn't reach for a Vue plugin. Such plugins often end up introducing an additional layer of complexity. When something doesn't work, you don't know if it's because you used incorrectly the plugin or the underlying Mapbox library and you need to check two documentation.&lt;/p&gt;

&lt;p&gt;As for the plugin docs, they are usually either lacking or redirecting to the official Mapbox docs. Plus those plugins tend to be abandoned after awhile.&lt;/p&gt;

&lt;p&gt;Finally, as you will see, it's not complicated to work with the Mapbox GL JS library. It's just JavaScript™&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing and initializing
&lt;/h2&gt;

&lt;p&gt;1。Install the npm package: &lt;code&gt;npm install mapbox-gl&lt;/code&gt;&lt;br&gt;
2。Link to the CSS stylesheet in the the head of the page where the map will be used (no need to load the Mapbox CSS globally if it is only used on one page).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stylesheet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.mapbox.com/mapbox-gl-js/v1.10.0/mapbox-gl.css&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="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;3。Import the module in the &lt;strong&gt;mounted hook&lt;/strong&gt; of your page or component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&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;mapboxgl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mapbox-gl&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;p&gt;4。Initialize the map with your access token and &lt;a href="https://docs.mapbox.com/mapbox-gl-js/api/#map"&gt;the options you want&lt;/a&gt;. As for the style, you can either create your own in the amazing &lt;a href="https://www.mapbox.com/mapbox-studio/"&gt;Mapbox Studio&lt;/a&gt;, use one of the &lt;a href="https://docs.mapbox.com/api/maps/#mapbox-styles"&gt;default styles&lt;/a&gt; or choose among the &lt;a href="https://www.mapbox.com/gallery/"&gt;publicly available styles&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mapboxgl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;accessToken&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_TOKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;div id="map"&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mapbox://styles/mapbox/streets-v9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default style&lt;/span&gt;
          &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;21.9270884&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;64.1436456&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// starting position as [lng, lat]&lt;/span&gt;
          &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding markers with popups
&lt;/h2&gt;

&lt;p&gt;Further down in your &lt;code&gt;mounted hook&lt;/code&gt;, you can add markers with popups.&lt;br&gt;
&lt;em&gt;Tip: You can use HTML for the content of the popup.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;yourMarkersArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;marker&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;LngLat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&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;popup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mapboxgl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setLngLat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LngLat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;popup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Initialized above&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-world example
&lt;/h2&gt;

&lt;p&gt;I made this map for a client: &lt;a href="https://www.gemuese-lieferdienst.ch/depots/karte/"&gt;https://www.gemuese-lieferdienst.ch/depots/karte/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository is private, but here's a gist with the relevant source code: &lt;a href="https://gist.github.com/mornir/9e85e65caba46e55269302e8a134e04e"&gt;https://gist.github.com/mornir/9e85e65caba46e55269302e8a134e04e&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing words
&lt;/h2&gt;

&lt;p&gt;I hope this post was useful to you. Don't hesisate to reach out in the comments below if something is not clear or if you know a better/simpler approach.&lt;/p&gt;

&lt;p&gt;Go make awesome maps now!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source of hero image&lt;/em&gt;: &lt;a href="https://www.flickr.com/photos/mapbox/23757790495"&gt;https://www.flickr.com/photos/mapbox/23757790495&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>mapbox</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Truly Responsive Grid with CSS Grid</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Mon, 01 Jun 2020 08:17:15 +0000</pubDate>
      <link>https://dev.to/mornir/truly-responsive-grid-with-css-grid-3c46</link>
      <guid>https://dev.to/mornir/truly-responsive-grid-with-css-grid-3c46</guid>
      <description>&lt;p&gt;This article describes really well how to make a responsive grid with CSS Grid:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link"&gt;
  &lt;a href="/scrimba" 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%2F2180%2F272ec0dc-f5e7-4574-83be-fc3482c93cd9.jpg" alt="Scrimba"&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%2F1929%2Fimage.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/scrimba/how-to-make-your-html-responsive-by-adding-a-single-line-of-css-29h" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;How to make your HTML responsive by adding a single line of CSS&lt;/h2&gt;
      &lt;h3&gt;Per for Scrimba ・ Jun 12 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#html&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#css&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#cssgrid&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;But we can add one final ingredient to make that CSS line even better.&lt;/p&gt;

&lt;p&gt;In the article, the minimum width of the grid items is set to &lt;code&gt;100px&lt;/code&gt;. This means that if the availabe size of the grid container is narrower than &lt;code&gt;100px&lt;/code&gt;, the grid items will overflow. This is quite unlikely to happen with a minimum width of only &lt;code&gt;100px&lt;/code&gt;. But the higher that number, the more likely the columns is to overflow on small viewports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cutting edge CSS to the rescue!
&lt;/h3&gt;

&lt;p&gt;Instead of having to resort to a media query, we can use: the CSS function &lt;code&gt;min()&lt;/code&gt;, &lt;a href="https://caniuse.com/#feat=css-math-functions" rel="noopener noreferrer"&gt;which is now widely supported&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto-fill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&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;&lt;code&gt;min()&lt;/code&gt; accepts one or more values and returns the smallest value. As soon as the full width of the columns is narrower than &lt;code&gt;300px&lt;/code&gt;, the columns will observe the &lt;code&gt;100%&lt;/code&gt; CSS declaration and shrink below &lt;code&gt;300px&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a better understanding of the issue and of its solution with the &lt;code&gt;min()&lt;/code&gt; function, I highly recommend reading this article:&lt;br&gt;
&lt;a href="https://evanminto.com/blog/intrinsically-responsive-css-grid-minmax-min/" rel="noopener noreferrer"&gt;https://evanminto.com/blog/intrinsically-responsive-css-grid-minmax-min/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>cssgrid</category>
    </item>
    <item>
      <title>Nuxt, Netlify and the trailing slash</title>
      <dc:creator>Jérôme Pott</dc:creator>
      <pubDate>Sun, 24 May 2020 15:46:06 +0000</pubDate>
      <link>https://dev.to/mornir/nuxt-netlify-and-the-trailing-slash-3gge</link>
      <guid>https://dev.to/mornir/nuxt-netlify-and-the-trailing-slash-3gge</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I recently noticed that &lt;em&gt;by default&lt;/em&gt; Netlify redirects routes that don't end with a slash to their slash equivalent: for example &lt;code&gt;www.example.com/about&lt;/code&gt; → &lt;code&gt;www.example.com/about/&lt;/code&gt; &lt;a href="https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash"&gt;Netlify docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This only happens to pages served directly by Netlify, in other words, pages served on the first load. As you know it, Nuxt then hydrates into a SPA and Vue Router takes care of the navigation. However, as soon as you refresh the page, you will be redirected to the URL with a trailing slash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Negative Impact
&lt;/h2&gt;

&lt;p&gt;If you don't use a trailing slash for both your internal links and your sitemap, the default redirection by Netlify can have some unwanted consequences. I can think of two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: I didn't measure it myself, but a &lt;a href="https://github.com/gatsbyjs/gatsby/issues/9207#issuecomment-431008517"&gt;comment on the Gatsby repository&lt;/a&gt; reports a delay of 100-300 ms, and even of 1s on a slow network. By the way, when doing a lighthouse audit on the page without the trailing slash, you'll get a warning about the redirection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO&lt;/strong&gt;: Google may be confused by the two URLs. Its crawler will index the routes with a trailing slash because of the Netlify redirect, but then will also index the routes without the slash because of the sitemap (by default, the Nuxt sitemap module doesn't add trailing slashes). If you don't use canonical links, the crawler might consider the content as duplicates. Fortunately in all my projects, I saw from the Google Search Console that the crawler was smart enough to discard one version. But SEO is too important to rely on the cleverness of Google.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;How to solve this issue? First let's get this out of the way: it doesn't make any difference whether we use a trailing slash or not. What's important is to be coherent. So is it a matter of taste? At first I would have said yes. But actually I personally realized that when browsing websites, I really don't pay much attention to the URL, let alone to the presence of that slanted bar.&lt;/p&gt;

&lt;p&gt;I would say that it's more a matter of tooling: don't fight the tooling! In our case, we want to choose one version, but Netlify can only force the trailing slash and not the opposite (at least I didn't find how, but at least you can still &lt;a href="https://github.com/gatsbyjs/gatsby/issues/15317#issuecomment-530048373"&gt;disable the forced redirection to the trailing slash&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a trailing slash to our internal links
&lt;/h3&gt;

&lt;p&gt;Now we need to update all our internal links with a trailing slash. Depending on the size and the complexity of the project, it's likely that we miss a couple. But no worry, even without a trailing slash, the navigation will still work as expected. By default, Nuxt handles both versions. However, for new projects, I recommend enforcing the trailing in nuxt.config.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;trailingSlash&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="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When developing, we'll now be able to easily spot &lt;code&gt;nuxt-link&lt;/code&gt; without a trailing slash, because they will return a 404. &lt;/p&gt;

&lt;h3&gt;
  
  
  Editing our sitemap
&lt;/h3&gt;

&lt;p&gt;Then we need to take care of our sitemap. We're in luck again! There's a &lt;code&gt;trailingSlash&lt;/code&gt; property which can be set to true:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.mywebsite.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;trailingSlash&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="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Voilà! Now we've fully committed ourselves to the trailing slash. 🤗&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Don't hesitate to correct me in the comments if I said something inaccurate.&lt;/em&gt; 🙊&lt;/p&gt;

&lt;h3&gt;
  
  
  A little reminder
&lt;/h3&gt;

&lt;p&gt;I take the opportunity to remind my fellow devs that the following properties are not needed for the sitemap module when using &lt;code&gt;nuxt generate&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setting &lt;strong&gt;cacheTime.&lt;/strong&gt; &lt;a href="https://github.com/nuxt-community/sitemap-module/issues/126"&gt;This option has no effect in static mode&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;setting &lt;strong&gt;routes&lt;/strong&gt; to the same value as generate.routes. By default, the sitemap module will use the dynamic routes defined in &lt;code&gt;generates.routes&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/gatsbyjs/gatsby/issues/9207"&gt;Preventing 301 redirects on URLs with no trailing slashes (Netlify)&lt;/a&gt;&lt;br&gt;
Hero image by &lt;a href="https://unsplash.com/@nh7_"&gt;Noora AlHammadi&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>netlify</category>
      <category>webdev</category>
      <category>jamstack</category>
    </item>
  </channel>
</rss>
