<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Benjamin Rancourt]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://www.benjaminrancourt.ca/</link><image><url>https://www.benjaminrancourt.ca/favicon.png</url><title>Benjamin Rancourt</title><link>https://www.benjaminrancourt.ca/</link></image><generator>Ghost 5.74</generator><lastBuildDate>Fri, 23 Feb 2024 07:31:10 GMT</lastBuildDate><atom:link href="https://www.benjaminrancourt.ca/blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Quick and Efficient Way to Backup a Remote Folder using Rsync]]></title><description><![CDATA[Rsync stands as a formidable utility. However, to harness its full potential, it's essential to recall the specific arguments that it needs. Herein lies my personal aide-memoire to address this very concern.]]></description><link>https://www.benjaminrancourt.ca/backup-remote-folder-rsync/</link><guid isPermaLink="false">64ef5860428dc70001de7f16</guid><category><![CDATA[bash]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 05 Sep 2023 11:15:32 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/08/pexels-sergei-starostin-6429127.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/08/pexels-sergei-starostin-6429127.jpg" alt="Quick and Efficient Way to Backup a Remote Folder using Rsync"><p>Occasionally, there arises a need for me to download directories stored on my remote server, often for backup purposes. Yet, on each occasion, I find myself scouring the Internet to determine the precise arguments required for the <strong>rsync</strong> utility.</p><p>Those days are now relegated to history, as I have chosen today to write down a future-oriented reminder right here. &#x1F605;</p><p>Without further ado, here is the definitive command to download a remote directory on your computer:</p><figure class="kg-card kg-code-card"><pre><code># Adapt the variables below
SSH_PORT=22
SSH_USER=root
SSH_SERVER=138.197.142.29
REMOTE_SOURCE_DIRECTORY=/var/opt/mealie
LOCAL_DESTINATION_DIRECTORY=.

# From your local machine
rsync \
  --archive \
  --compress \
  --human-readable \
  --partial \
  --progress \
  --rsh &quot;ssh -p ${SSH_PORT}&quot; \
  --stats \
  --verbose \
  &quot;${SSH_USER}@${SSH_SERVER}:${REMOTE_SOURCE_DIRECTORY}&quot; \
  &quot;${LOCAL_DESTINATION_DIRECTORY}&quot;</code></pre><figcaption>An example of the rsync command to download a remote directory.</figcaption></figure><p><strong>Explanation of the arguments</strong></p><ol><li><code>--archive</code>: Preserves file permissions, ownership, timestamps, and more.</li><li><code>--compress</code>: Enables compression of data during transfer, reducing the amount of data transmitted.</li><li><code>--human-readable</code>: File sizes are displayed in a human-readable format (e.g., kB, Mb or gB).</li><li><code>--partial</code>: If the transfer is interrupted, partially transferred files are keep instead of discarding them.</li><li><code>--progress</code>: Displays a progress indicator.</li><li><code>--rsh &quot;ssh -p ${SSH_PORT}&quot;</code>: The communication should be done over SSH using port 22. It is useful if your server use another port for SSH.</li><li><code>--stats</code>: Provides a summary of the transfer statistics once the process is completed.</li><li><code>--verbose</code>: Increases the verbosity of the output, offering more detailed information.</li><li><code>&quot;${SSH_USER}@${SSH_SERVER}:${REMOTE_SOURCE_DIRECTORY}:</code>: The source directory on the remote server that you wish to back up.</li><li><code>&quot;${LOCAL_DESTINATION_DIRECTORY}&quot;</code>: The destination directory on your local machine where the backup will be saved.</li></ol>]]></content:encoded></item><item><title><![CDATA[How to Find the Largest Files and Directories on Debian]]></title><description><![CDATA[Ever experienced the frustration of a server running out of storage space? It can be difficult to locate the problem, unless you have THE command to investigate. 😎]]></description><link>https://www.benjaminrancourt.ca/largest-files-directories/</link><guid isPermaLink="false">64dcd323b4003f0001dede02</guid><category><![CDATA[bash]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 29 Aug 2023 11:15:22 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/08/pexels-tima-miroshnichenko-6549919.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/08/pexels-tima-miroshnichenko-6549919.jpg" alt="How to Find the Largest Files and Directories on Debian"><p>Running out of <strong>server space</strong> can be a real headache, often requiring thorough investigation to pinpoint the issue. In this quick guide, I&apos;ll show you an <strong>invaluable command</strong> that can help you identify the culprits behind <strong>excessive storage consumption</strong> on your Debian server.</p><p>To uncover the largest files and directories eating up your server&apos;s precious space, you can employ the following command:</p><pre><code class="language-bash">sudo du --all / | sort --numeric-sort --reverse | head --lines 15</code></pre><p>Or alternatively:</p><pre><code class="language-bash">sudo du -a / | sort -n -r | head -n 15</code></pre><p>The output of this command provides a list of the <strong>largest files</strong> and <strong>directories</strong>, accompanied by the space they occupy:</p><figure class="kg-card kg-code-card"><pre><code>25072264  /
20175684  /var
8949680   /var/lib
8783876   /var/lib/docker
8629384   /var/lib/docker/overlay2
6131060   /var/log
[...]</code></pre><figcaption>An example of the output of the previous command.</figcaption></figure><p>This resulted list helped me to swiftly identify the storage-intensive elements on my server. Armed with this knowledge, I implemented measures to mitigate their impact and free up valuable space.</p><p>Should you need to delve into a specific directory for a more thorough investigation, the command can be tailored accordingly. For instance, if you want to focus on the <code>/var/log</code> directory:</p><pre><code class="language-bash">sudo du -a /var/log | sort -n -r | head -n 15</code></pre><p>By integrating this command into your diagnostic toolkit, you can confidently tackle storage issues and prevent future space shortages on your server. The satisfaction of maintaining an organized and efficient server environment awaits&#x2014;enjoy the journey! &#x1F60E;</p>]]></content:encoded></item><item><title><![CDATA[Streamlining LinkedIn Verification for Quebec Businesses]]></title><description><![CDATA[In this blog post, I share the documents I provided to LinkedIn to prove that my business was legitimate.]]></description><link>https://www.benjaminrancourt.ca/linkedin-verification-quebec-businesses/</link><guid isPermaLink="false">64da4022ffe2530001626341</guid><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 15 Aug 2023 11:15:45 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/08/pexels-airam-datoon-16450744.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/08/pexels-airam-datoon-16450744.jpg" alt="Streamlining LinkedIn Verification for Quebec Businesses"><p>A few weeks ago, I embarked on the journey of obtaining access to the LinkedIn <a href="https://developer.linkedin.com/product-catalog/marketing/community-management-api"><strong>Community Management API</strong></a><strong> </strong>for one of my application. However, I soon discovered that this privilege was exclusively reserved for <strong>verified companies</strong>. &#x1F648;</p><p>As the automatic verification failed for my business, I had to provide some documentation that prove that <strong><a href="https://www.merisia.ca/">Merisia</a></strong> was a <strong>legal business</strong>. And today, I&apos;m excited to share my experience and the key documents I submitted, in case you find yourself in a similar situation:</p><ul><li><strong>Business Registration</strong>: I provided a meticulous screenshot displaying <a href="https://www.registreentreprises.gouv.qc.ca/RQAnonymeGR/GR/GR03/GR03A2_19A_PIU_RechEnt_PC/PageRechSimple.aspx?T1.CodeService=S00436&amp;NEQ=1178573383">Merisia&apos;s entry on the <em>Registraire des entreprises</em></a>.</li><li><strong>Domain Ownership Validation</strong>: I submitted an invoice confirming our ownership of the domain <a href="merisia.ca">merisia.ca</a>.</li><li><strong>Business Tax Identification</strong>: I also included a screenshot detailing our business tax numbers, sourced from <a href="https://www.revenuquebec.ca/fr/entreprises/mon-dossier-pour-les-entreprises/"><em>Mon dossier - Entreprise</em></a>.</li><li><strong>Inclusion in Business Registries</strong>: and to further substantiating our legitimacy, I included a screenshot affirming <a href="https://beta.canadasbusinessregistries.ca/search/results?search=%7BMerisia%7D&amp;status=Active">Merisia&apos;s inclusion in the public Canada&apos;s Business Registries</a>.</li></ul><p>With these meticulously gathered documents, I submitted them to the LinkedIn support team, who then channelled them to their legal department. The subsequent weeks were a waiting game, filled with anticipation. The culmination of this process was the exhilarating moment when <strong>I received confirmation of the granted access</strong>! &#x1F924;</p><p>This experience stands as a testament to the efficacy of providing <strong>comprehensive documentation</strong> to establish the authenticity of a business. If you find yourself navigating the intricacies of verification, take heart from my journey &#x2013; it truly works! &#x1F60E;</p>]]></content:encoded></item><item><title><![CDATA[How to List All Changed Files Inside Docker Containers]]></title><description><![CDATA[Discover how to use the "docker container diff" command to effortlessly identify added, changed, and deleted files within your Docker containers' file systems.]]></description><link>https://www.benjaminrancourt.ca/how-to-list-all-changed-files-inside-docker-containers/</link><guid isPermaLink="false">64779af211087b0001a05daa</guid><category><![CDATA[Docker]]></category><category><![CDATA[bash]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 20 Jun 2023 11:15:29 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/05/pexels-david-dibert-1117210.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/05/pexels-david-dibert-1117210.jpg" alt="How to List All Changed Files Inside Docker Containers"><p>Did you know that you can <strong>easily identify</strong> the <strong>files</strong> that have been <strong>changed</strong> within a <strong><a href="https://www.docker.com/">Docker</a> container&apos;s file system</strong>? This information can be valuable, especially when you want to ensure that all necessary files are backed up and <strong>aren&apos;t lost</strong> when the container restarts. &#x1F644;</p><p>In this article, we will explore how to use the command made available by Docker effectively and even automate the process for multiple containers.</p><h2 id="listing-changed-files-and-directories">Listing changed files and directories</h2><p>To list all the files and directories that have been <strong>added</strong>, <strong>changed</strong>, or <strong>deleted </strong>within a Docker container, you can use the &quot;<strong><a href="https://docs.docker.com/engine/reference/commandline/container_diff/">docker container diff</a></strong>&quot; command. Here is the syntax:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker container diff &lt;CONTAINER_NAME_OR_ID&gt;</code></pre><figcaption>Make sure to replace <code>&lt;CONTAINER_NAME_OR_ID&gt;</code> with the container&apos;s name or ID!</figcaption></figure><p>Here is an example of its output for a <a href="https://mariadb.org/">MariaDB</a> container that I use:</p><figure class="kg-card kg-code-card"><pre><code>C /run
C /run/mysqld
A /run/mysqld/mysqld.pid
A /run/mysqld/mysqld.sock</code></pre><figcaption>Example of the result of a <em>docker container diff</em> command.</figcaption></figure><p>This command will output a list of files and directories that fall into the following categories:</p><ul><li><strong>A</strong>: <em>added</em></li><li><strong>C</strong>: <em>changed</em></li><li><strong>D</strong>: <em>deleted</em></li></ul><h2 id="automating-the-process">Automating the process</h2><p>If you need to perform this check for multiple containers, <em>manually</em> finding and pasting each container&apos;s name or ID can be <strong>time-consuming</strong>. However, you can <strong>automate this process</strong> using the following script:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">#!/bin/bash

# Get the names of all containers
container_names=$(docker container ls --format &apos;{{.Names}}&apos;)

# Iterate over all container names
for container_name in $container_names
do
    # Execute `docker container diff` on the container
    docker container diff ${container_name}

    # Check if the command was successful or not 
    if [ $? -eq 0 ]
    then
        echo &quot;The command &apos;docker container diff&apos; has been executed with success on container ${container_name}.&quot;
    else
        echo &quot;The command &apos;docker container diff&apos; has failed on container ${container_name}.&quot;
    fi
done</code></pre><figcaption>The Bash script that you can use to list all changed files and directories for all your containers.</figcaption></figure><p>By running this script, you will get the results for <em>all your containers</em>. The output will appear on the standard output, but you can easily modify the script to <em>redirect</em> the results to a <strong>file</strong> or <strong>multiple files</strong> according to your needs. &#x1F609;</p><h2 id="conclusion">Conclusion</h2><p>By utilizing the <code>docker container diff</code> command and the provided script, you now have the ability to identify all the changed files within your Docker containers. This knowledge is crucial for ensuring that all necessary files are appropriately preserved, especially when working with container volumes. &#x1F917;</p>]]></content:encoded></item><item><title><![CDATA[How to Resolve “Pull Access Denied for registry.gitlab.com” Error]]></title><description><![CDATA[Resolving the 'Pull access denied for registry.gitlab.com' error is crucial when migrating private GitLab repositories to a group. Follow these steps to overcome the issue and get your CI/CD pipeline back on track. 🤗]]></description><link>https://www.benjaminrancourt.ca/pull-access-denied-for-registry-gitlab-com/</link><guid isPermaLink="false">6466649f47dd0b0001e71ce3</guid><category><![CDATA[GitLab]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 13 Jun 2023 11:15:40 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/05/pexels-sarah-chai-7262478.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/05/pexels-sarah-chai-7262478.jpg" alt="How to Resolve &#x201C;Pull Access Denied for registry.gitlab.com&#x201D; Error"><p>If you&apos;re migrating your <strong>private GitLab repositories</strong> to a <strong>group</strong>, like I did for my <a href="https://www.merisia.ca/">personal business</a>, you may encounter an error message like this in your pipelines:</p><pre><code>WARNING: Failed to pull image with policy &quot;always&quot;: Error response from daemon: pull access denied for registry.gitlab.com/merisia/tools/git-rsync, repository does not exist or may require &apos;docker login&apos;: denied: requested access to the resource is denied (manager.go:237:0s)

ERROR: Job failed: failed to pull image &quot;registry.gitlab.com/merisia-inc/outils/git-rsync:1.2.0&quot; with specified policies [always]: Error response from daemon: pull access denied for registry.gitlab.com/merisia/tools/git-rsync, repository does not exist or may require &apos;docker login&apos;: denied: requested access to the resource is denied (manager.go:237:0s)</code></pre><p>Despite confirming that the URL and tag exist in the repository&apos;s Container Registry, the problem persists. &#x1F613;</p><p>After conducting some research, I discovered a related <a href="https://stackoverflow.com/questions/65097315/pull-access-denied-for-registry-gitlab-com-when-the-project-is-transfered-to-a-g">StackOverflow question</a> where another user faced a similar issue. Although no solution was provided, I stumbled upon a <strong>helpful clue</strong>. The user encountered the same issue when <strong>moving a private repository to a group</strong>. &#x1F914;</p><p>The thread mention the <code>DOCKER_AUTH_CONFIG</code> variable, which picked my curiosity. So I began to look on it and I found <a href="https://microfluidics.utoronto.ca/gitlab/help/ci/docker/using_docker_images.md#define-an-image-from-a-private-container-registry">this documentation</a> from a team of UToronto that helped me solve my problem. &#x1F929;</p><p>As it&apos;s not impossible that I encountered again this problem in a distant future,I decided to <strong>share the solution I used</strong> through this blog post. I&apos;ll skip the detailed explanations, since the UToronto team has already covered the necessary commands. &#x1F917;</p><h2 id="solution">Solution</h2><h3 id="encode-your-gitlab-credentials-in-base64">Encode your GitLab credentials in base64</h3><pre><code class="language-bash"># The use of &quot;-n&quot; - prevents encoding a newline in the password.
echo -n &apos;GITLAB_USERNAME:GITLAB_PASSWORD&apos; | base64</code></pre><p></p><h3 id="create-the-dockerauthconfig-variable-in-your-cicd-group-settings">Create the DOCKER_AUTH_CONFIG variable in your CI/CD group settings</h3><ul><li>Go to your GitLab group (or project).</li><li>Under <code>Settings</code> &gt; <code>CI/CD</code> &gt; <code>Variable</code>, click the <code>Add variable</code> button.</li><li>Set the key as <code>DOCKER_AUTH_CONFIG</code> and set the content with the following JSON:</li></ul><pre><code class="language-json">{
    &quot;auths&quot;: {
        &quot;registry.gitlab.com&quot;: {
            &quot;auth&quot;: &quot;(Base64 content from above)&quot;
        }
    }
}
</code></pre><ul><li>Make sure to uncheck the <code>Protect variable</code> and <code>Expand variable reference</code> options as they are optionals.</li><li>Click the <code>Add variable</code> button to create this new variable.</li></ul><h2 id="rerun-your-previously-failed-cicd-pipeline">Rerun your previously failed CI/CD pipeline</h2><p>By following the previous steps, your pipeline should now be back on track! &#x1F973;</p><p>If the issue persists, you can <strong>retry</strong> the previous steps or <strong>continue searching</strong> for an <strong>alternative solution</strong>! &#x1F605;</p>]]></content:encoded></item><item><title><![CDATA[How to Update Ubuntu in WSL from 18.04 to 22.04]]></title><description><![CDATA[Are you stuck with Ubuntu 18.04 for your WSL distribution? In this article, I finally managed to upgrade to the latest LTS version! 😎]]></description><link>https://www.benjaminrancourt.ca/how-to-update-ubuntu-in-wsl-from-18-04-to-22-04/</link><guid isPermaLink="false">64188a457e8210000134ec76</guid><category><![CDATA[bash]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 02 May 2023 11:15:56 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/03/pexels-monica-silvestre-3562689.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/03/pexels-monica-silvestre-3562689.jpg" alt="How to Update Ubuntu in WSL from 18.04 to 22.04"><p>For a long time, I was <strong>not able to run </strong><a href="https://nodejs.org/en"><strong>Node.js</strong></a><strong> 18</strong> on my desktop. When I choose one of its version with <a href="https://github.com/nvm-sh/nvm">nvm</a>, I always ran into an error stating that a library was missing: <code>node: /lib/x86_64-linux-gnu/libc.so.6: version &apos;GLIBC_2.28&apos; not found (required by node)</code>. I was stuck with the latest Node.js 16 version available... &#x1F625;</p><p>But, it was something that was bothered me. I couldn&apos;t use the <strong>new features</strong> of Node.js LTS version, and I hopped that somehow, it would resolve by itself. <em>Disclaimer</em>: it hasn&apos;t. I tried from time to time to upgrade, but my attempts have been unsuccessful. Until now. &#x1F62E;</p><p>I&apos;m now happy to says that I manage <strong>to install</strong> and <strong>to use</strong> Node.js 18. How? By <strong>upgrading my <a href="https://ubuntu.com/">Ubuntu</a> version</strong> of <a href="https://learn.microsoft.com/en-us/windows/wsl/install">Windows Subsystem for Linux</a> (WSL) from 18.04 to 22.04. After a new search, I stumbled on <a href="https://stackoverflow.com/a/74586477">this answer on StackOverflow</a> from <a href="https://stackoverflow.com/users/20609953/hallexcosta">hallexcosta</a> and it worked for me! I was able to <strong>update to Ubuntu 20.04</strong> and with <a href="https://askubuntu.com/questions/1428423/upgrade-ubuntu-in-wsl2-from-20-04-to-22-04">another answer</a> by <a href="https://askubuntu.com/users/1165986/notthedr01ds">NotTheDr01ds</a>, I updated to <strong>Ubuntu 22.04</strong> as well. So, thanks a lot to these people! &#x1F970;</p><p>As it&apos;s possible that I would need to update my WSL distribution on another computer, I decided to <strong>write here</strong> the commands I used <em>for posterity</em>. It could also be useful for you if you aren&apos;t either on the latest version of Ubuntu. &#x1F609;</p><p>Without further ado, here is the <strong>complete procedure</strong>. &#x1F917;</p><h2 id="from-1804-to-2004">From 18.04 to 20.04</h2><p>Before continuing, make sure that <strong>you aren&apos;t</strong> on the <a href="https://ubuntu.com/download/desktop">latest version of Ubuntu</a> by running the following command in your WSL terminal:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">lsb_release -a

# No LSB modules are available.     
# Distributor ID: Ubuntu
# Description:    Ubuntu 18.04.6 LTS
# Release:        18.04
# Codename:       bionic</code></pre><figcaption>Make sure that you&apos;re using Ubuntu 18 with the <code>lsb_release</code> command.</figcaption></figure><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x1F4A3;</div><div class="kg-callout-text">Sometimes, I&apos;m a little bit reckless, so I <strong>didn&apos;t do a backup</strong>. But if you don&apos;t want to take the same risks that I took, hallexcosta also provide the steps to <strong>create </strong>and <strong>restore </strong>a backup. &#x1F609;</div></div><h3 id="remove-the-snapd-package">Remove the Snapd package</h3><blockquote>&quot;For me it was necessary to remove the <code>snapd</code> package, because ubuntu was not allowing me to upgrade to 20.04 LTS version.&quot; &#x2013; hallexcosta</blockquote><p>As I didn&apos;t want to take any chances, I <strong>uninstall </strong>as well the <strong>Snapd package</strong>. I don&apos;t know if it would have made a difference for me, but just to be sure... &#x1F60B;</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">sudo apt purge snapd</code></pre><figcaption>This command remove and delete the snapd package, along with its configuration files.</figcaption></figure><p>According to <a href="https://en.wikipedia.org/wiki/Snap_(software)">its Wikipedia page</a>, <strong>snapd </strong>is a tool used to package and to deploy applications. The procedure doesn&apos;t reinstall it, but at least, it is noted here that it has been removed. To add it back should be simple <strong>if it is needed</strong>. &#x1F643;</p><h3 id="update-and-upgrade-packages">Update and upgrade packages</h3><p>Always make sure that the operating system is <strong>up-to-date</strong> before taking a major upgrade! &#x1F604;</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Get updated software list for Ubuntu
sudo apt update

# Apply updates and patches
sudo apt upgrade</code></pre><figcaption>The default commands to update all packages.</figcaption></figure><h3 id="install-the-update-manger-core-package">Install the update-manger-core package</h3><blockquote>&quot;It&#x2019;s essential to install this update manager core package this will trick the system into thinking there is a new LTS available and allow you to do an in-place upgrade.&quot; &#x2013; hallexcosta</blockquote><p>Again, I proceed exactly as I&apos;ve been told. I wanted to quickly see if the procedure would work for me. &#x1F605;</p><pre><code class="language-bash">sudo apt install update-manager-core</code></pre><p><a href="https://packages.ubuntu.com/bionic/admin/update-manager-core">update-manager-core</a> seems to be a simple package that manage release upgrades...</p><h3 id="install-the-new-version">Install the new version</h3><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Update Ubuntu
sudo do-release-upgrade</code></pre><figcaption>This command will check if there is a new version and will proceed to install it.</figcaption></figure><p>The command should take a <strong>couple of minutes</strong> to run. In my case, it has more than 600 MB to download. Go take a walk or grab a drink; once it&apos;s finished it will ask for a confirmation. &#x1F609;</p><h3 id="restart">Restart</h3><p><strong>Restart your computer</strong> to make sure everything has been updated and <strong>check</strong> if Ubuntu has been updated by typing the following command:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">lsb_release -a

# No LSB modules are available.     
# Distributor ID: Ubuntu
# Description:    Ubuntu 20.04.5 LTS
# Release:        20.04
# Codename:       focal</code></pre><figcaption>You should now see that Ubuntu has been updated to 20.04, the next major LTS version after 18.04.</figcaption></figure><p>Unfortunately, at the time of writing this article, Ubuntu 20.04 <em>is not</em> the latest LTS version. But the steps from passing to Ubuntu 22.04 are smaller than those before. Let&apos;s see them. &#x1F917;</p><h2 id="from-2004-to-2204">From 20.04 to 22.04</h2><p>With the <strong>three following commands</strong>, you should be able to upgrade to the latest LTS version:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Get updated software list for Ubuntu
sudo apt update

# Apply updates and patches
sudo apt upgrade

# Update Ubuntu
sudo do-release-upgrade</code></pre><figcaption>You may recognize some commands that we have used to install Ubuntu 20.04.</figcaption></figure><p>Again, <strong>restart your computer</strong> and check if your Ubuntu distribution has been upgraded:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">lsb_release -a

# No LSB modules are available.     
# Distributor ID: Ubuntu
# Description:    Ubuntu 22.04.2 LTS
# Release:        22.04
# Codename:       jammy</code></pre><figcaption>We are now on the latest LTS version of Ubuntu! &#x1F973;</figcaption></figure><p>When Ubuntu 24.04 will be released, I hope that the previous procedure remains the same, as it&apos;s really simple. &#x1F91E;</p><h2 id="conclusion">Conclusion</h2><p>After <strong>upgrading to Ubuntu 22.04</strong>, you should now be able to <strong>use Node.js 18</strong> on your computer. Install it if it&apos;s not already done and run a simple command to check if the error about the missing library is gone.</p><pre><code class="language-bash"># Install the Node.js latest version available
nvm install 18.15.0

# Check if Node.js is able to run without errors
node --version
# v18.15.0</code></pre><p><strong>Thanks </strong>again to <a href="https://stackoverflow.com/users/20609953/hallexcosta">hallexcosta</a> and <a href="https://askubuntu.com/users/1165986/notthedr01ds">NotTheDr01ds</a>! &#x1F970;</p>]]></content:encoded></item><item><title><![CDATA[How to Redirect a Domain to Another Domain with Traefik]]></title><description><![CDATA[In this blog post, you will learn how to use Traefik to redirect traffic from one domain to another temporarily.]]></description><link>https://www.benjaminrancourt.ca/how-to-redirect-a-domain-to-another-domain-with-traefik/</link><guid isPermaLink="false">641e0878b7496b000104a985</guid><category><![CDATA[Traefik]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 11 Apr 2023 11:15:53 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/03/pexels-yevhen-sukhenko-11921224.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/03/pexels-yevhen-sukhenko-11921224.jpg" alt="How to Redirect a Domain to Another Domain with Traefik"><p>One month ago, I submitted a <strong>business plan</strong> as part of the <a href="https://www.osentreprendre.quebec/defi-osentreprendre/"><strong>Defi OSEntreprendre</strong></a>, using a <strong>new domain</strong> that I had not yet created a website for. I was worried that if a member of the jury clicked on one of my links, they would encounter a <strong>404 error page</strong>, which would not be great for my project&apos;s credibility. &#x1F643;</p><p>To buy myself some time, I decided to <strong>redirect the whole website</strong> to my <strong>existing website</strong>, where I could temporarily host the content for the challenge. </p><p>Initially, I tried to create a solution that would allow me to create multiple redirects, but it didn&apos;t work out. Instead, I created a <strong>quick proof-of-concept</strong> using <a href="https://doc.traefik.io/traefik/"><strong>Traefik</strong></a>, a tool I often use in my infrastructure. &#x1F60E;</p><h2 id="solution">Solution</h2><p>Here is the final solution I came up with using Traefik:</p><pre><code class="language-yaml">version: &quot;3.8&quot;

services:
  merisia:
    # As we are using Docker Swarm, labels need to be set inside deploy
    deploy:
      labels:
        traefik.docker.network: &quot;traefik-net&quot;
        traefik.enable: &quot;true&quot;
        traefik.http.routers.merisia.entrypoints: &quot;https&quot;
        traefik.http.routers.merisia.middlewares: &quot;redirect-merisia-benjaminrancourt,default@file&quot;
        traefik.http.routers.merisia.rule: &quot;Host(`merisia.ca`,`www.merisia.ca`)&quot;
        traefik.http.routers.merisia.tls.certresolver: &quot;letsEncrypt&quot;
        traefik.http.routers.merisia.tls.options: &quot;intermediate@file&quot;
        traefik.http.routers.merisia.tls: &quot;true&quot;
        traefik.http.services.merisia.loadbalancer.server.port: 80
        traefik.http.services.merisia.loadbalancer.sticky.cookie.httpOnly: &quot;true&quot;
        traefik.http.services.merisia.loadbalancer.sticky.cookie.secure: &quot;true&quot;
        traefik.http.middlewares.redirect-merisia-benjaminrancourt.redirectregex.regex: &quot;^https?://(www\\.)?merisia\\.ca/(.*)&quot;
        traefik.http.middlewares.redirect-merisia-benjaminrancourt.redirectregex.replacement: &quot;https://www.benjaminrancourt.ca/$${2}&quot;
        traefik.http.middlewares.redirect-merisia-benjaminrancourt.redirectregex.permanent: &quot;false&quot;
      resources:
        limits:
          cpus: &apos;0.750&apos;
          memory: 16M
        reservations:
          cpus: &apos;0.001&apos;
          memory: 6M
    # https://hub.docker.com/r/traefik/whoami/tags?page=1&amp;ordering=last_updated
    image: &quot;traefik/whoami:v1.8.7&quot;
    networks:
      - traefik-net

networks:
  traefik-net:
    driver: overlay
    external: true
</code></pre><p>Let me explain how this solution works below. &#x1F609;</p><h2 id="explanations">Explanations</h2><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">&#x1F4A3;</div><div class="kg-callout-text"><strong>Note</strong>: to improve the readability of the code, I removed the <strong>indentation </strong>and renamed the <strong>middleware </strong>as <code>redirection</code>. Because of that, the excerpts below would not work with the solution above without modification.</div></div><h3 id="1-%E2%80%93-router-rule">1 &#x2013; Router rule</h3><pre><code class="language-yaml">traefik.http.routers.merisia.rule: &quot;Host(`merisia.ca`,`www.merisia.ca`)&quot;</code></pre><p>This rule tells Traefik the <strong>original domain</strong> to redirect from. In this case, the domain is <a href="https://merisia.ca/">merisia.ca</a> and its subdomain is <a href="https://www.merisia.ca/">www</a>. </p><h3 id="2-%E2%80%93-redirect-regex-middleware">2 &#x2013; Redirect regex middleware</h3><pre><code class="language-yaml"> traefik.http.middlewares.redirection.redirectregex.regex: &quot;^https?://(www\\.)?merisia\\.ca/(.*)&quot;
 traefik.http.middlewares.redirection.redirectregex.replacement: &quot;https://www.benjaminrancourt.ca/$${2}&quot;
 traefik.http.middlewares.redirection.redirectregex.permanent: &quot;false&quot;</code></pre><p>The regex <strong>redirects </strong>the original traffic to the <strong>replacement domain</strong>. It captures the original traffic using a <strong>regular expression</strong> (in <code>regex</code>) and redirects it to the replacement domain using a replacement pattern (in <code>replacement</code>).</p><p>The regex is designed to work with both <strong>HTTP </strong>and <strong>HTTPS </strong>(<code>^https?://</code>) and to capture all traffic that come from its <code>www</code> <strong>subdomain</strong> and its <strong>bare domain</strong> (<code>(www\\.)?</code>). All dots were also escaped accordingly (<code>\\.</code>).</p><p>The <strong>pathname </strong>is captured in the <strong>second </strong>and last <strong>capture group</strong> (<code>$${2}</code>).</p><h3 id="3-%E2%80%93-middlewares">3 &#x2013; Middlewares</h3><p>This label tells Traefik to use the <strong>previous <a href="https://doc.traefik.io/traefik/middlewares/overview/">middleware</a></strong>.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">traefik.http.routers.merisia.middlewares: &quot;redirection,default@file&quot;</code></pre><figcaption>Two middlewares is used here: our redirect middleware and the unknown <code>default</code> from the configuration file.</figcaption></figure><h3 id="4-%E2%80%93-image">4 &#x2013; Image</h3><p>And finally, we also need to have a container that simply exists, as I don&apos;t think it&apos;s possible to have a docker-compose stack <strong>without a container</strong>! &#x1F605;</p><pre><code class="language-yaml">image: &quot;traefik/whoami:v1.8.7&quot;</code></pre><p>The <a href="https://hub.docker.com/r/traefik/whoami">traefik/whoami</a> image is a <strong>tiny web server</strong> that prints OS information and HTTP request to output. But, if you have configured everything correctly, you should never see its output, as you should be redirected to your second domain! &#x1F609;</p><h2 id="conclusion">Conclusion</h2><p>In my case, the previous solution has helped me to have <a href="https://www.merisia.ca/linkedin">www.merisia.ca/linkedin</a> temporarily redirect to <a href="https://www.benjaminrancourt.ca/linkedin">www.benjaminrancourt.ca/linkedin</a>, which finally redirect to my personal LinkedIn profile. I also had some others links, but I&apos;m not able to disclosure yet. &#x1F605;</p><p>So, if you use <strong>Traefik</strong>, you now have a solution to <strong>temporarily redirect traffic from one domain to another</strong>. &#x1F609;</p><p>Good luck! &#x1F60E;</p>]]></content:encoded></item><item><title><![CDATA[How to Completely Uninstall Docker]]></title><description><![CDATA[In this article, you'll learn step-by-step on how to completely remove Docker from a Debian-based Linux system.]]></description><link>https://www.benjaminrancourt.ca/how-to-completely-uninstall-docker/</link><guid isPermaLink="false">64121188adebd10001b50e85</guid><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 21 Mar 2023 11:15:46 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2023/03/pexels-karolina-grabowska-4239146.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2023/03/pexels-karolina-grabowska-4239146.jpg" alt="How to Completely Uninstall Docker"><p>Last week, I was looking to recreate my <strong>personal infrastructure</strong> on a new server, but somehow, I <em>screwed</em> something in the process and was left with a Docker container that I could not delete... &#x1F648;</p><p>Something similar had already happened to me in the past, and the fix that I had found in that time was to <strong>completely remove</strong> and <strong>reinstall Docker</strong>. &#x1F92F;</p><p>As I was only at the beginning of my process and didn&apos;t want to lose any more time, I decided to <strong>choose again</strong> this solution. But, my previous notes were a little bit <em>scrambled</em>, so I decided to improve them and to <strong>share them with you</strong>. Just in case you need it! &#x1F609;</p><p>So, without further ado, here is the <strong>complete procedure</strong> with some explanations if you want to learn more! &#x1F60E;</p><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">In this procedure, my <strong>Linux distribution</strong> is <a href="https://www.debian.org/index.en.html"><strong>Debian</strong></a>. If you use another distribution, you may need to <strong>modify </strong>the commands as they could be not suitable for you distribution.<br><br>And to remove the need of <code>sudo</code> before each command, I suggest that you run these command as <code>root</code> (with <code>sudo su -</code>). &#x1F609;</div></div><h2 id="step-1-%E2%80%93-remove-docker-images-containers-and-volumes">Step 1 &#x2013; Remove Docker images, containers, and volumes</h2><p>To completely remove Docker from your system, you&apos;ll need to remove any <strong>images</strong>, <strong>containers</strong>, and <strong>volumes </strong>that were created. To do this, use the following commands:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Remove unused data
docker system prune --all --volumes

# Remove all services
docker service rm $(docker service ls -q)

# Remove all containers
docker rm -f $(docker ps -aq)

# Remove all images
docker rmi -f $(docker images -aq)

# Remove all volumes
docker volume rm $(docker volume ls -q)</code></pre><figcaption>Remove all data created and managed by Docker.</figcaption></figure><h2 id="step-2-%E2%80%93-stop-docker-service">Step 2 &#x2013; Stop Docker service</h2><p>Before removing Docker from your system, you should <strong>stop the Docker service</strong> by running the following command:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">systemctl stop docker</code></pre><figcaption>Shut down Docker.</figcaption></figure><p>This will ensure that Docker is <strong>not running</strong> in the <strong>background </strong>while you&apos;re removing it. &#x1F609;</p><h2 id="step-3-%E2%80%93-uninstall-docker">Step 3 &#x2013; Uninstall Docker</h2><p>It&apos;s now time to <strong>remove Docker</strong> from the operating system. Run the followings commands:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Uninstalls the Docker Community Edition (docker-ce) package from your Debian system
apt-get purge docker-ce -y

# Removes any remaining Docker packages and their dependencies that were not automatically removed by the previous command
apt-get autoremove --purge docker-ce -y</code></pre><figcaption>Uninstall Docker without prompts.</figcaption></figure><p>This will remove any packages that were installed as <strong>dependencies of Docker</strong> but are no longer needed.</p><h2 id="step-5-%E2%80%93-remove-docker-group-optional">Step 5 &#x2013; Remove Docker group (optional)</h2><p>If you had previously added users to the <a href="https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user"><strong>Docker users group</strong></a> to allow them to run Docker commands without <code>sudo</code>, you may want to remove the Docker group as well. Use the following command:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">groupdel docker</code></pre><figcaption>This command will also remove the <code>docker</code> group from all users.</figcaption></figure><h2 id="step-4-%E2%80%93-remove-docker-directories">Step 4 &#x2013; Remove Docker directories </h2><p>Next, you&apos;ll need to <strong>remove </strong>any <strong>directories associated</strong> with Docker, including its configuration files and its <a href="https://docs.docker.com/storage/storagedriver/">storage location</a>. Use the following commands:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Remove the Docker configuration files
rm -rf /etc/docker

# Remove the Docker Daemon&apos;s storage location
rm -rf /var/lib/docker</code></pre><figcaption>Remove all directories created and used by Docker.</figcaption></figure><h2 id="conclusion">Conclusion</h2><p>And that&apos;s it! You&apos;ve now <strong>completely removed Docker</strong> from your system, along with any associated files and directories. &#x1F60E;</p><p>If you want to <strong>reinstall</strong> Docker, you may now run the same commands as you typed when you installed it before! &#x1F609;</p>]]></content:encoded></item><item><title><![CDATA[Sustainable Acceptance Criteria 🌳]]></title><description><![CDATA[Do you write acceptance criteria for your user stories? Do you have any that relate to the environment and performance of your website?]]></description><link>https://www.benjaminrancourt.ca/sustainable-acceptance-criteria/</link><guid isPermaLink="false">6397d3490d0cf600019751cd</guid><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 13 Dec 2022 12:15:54 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/10/pexels-pixabay-45863.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2022/10/pexels-pixabay-45863.jpg" alt="Sustainable Acceptance Criteria &#x1F333;"><p>I write many <strong>user stories</strong> as part of my work and my entrepreneurial project. For each of them, I list all the <strong>acceptance criteria</strong> that will allow us to determine if the features have been <strong>successfully</strong> implemented. &#x2611;&#xFE0F;</p><p>To <strong>reduce</strong> the <strong>carbon footprint</strong> of the products that are developed, I have started to integrate more and more <strong>eco-responsible acceptance criteria</strong>. &#x1F343;</p><p>These acceptance criteria stand out from the others because they focus on the <strong>environment</strong>. Specifically, they are intended to <strong>reduce the data needed</strong> to <strong>operate</strong> the website (or web application).</p><p>By <strong>minimizing this data</strong>, all the infrastructures on which they transact use <strong>less memory</strong> and <strong>electricity</strong>. &#x26A1;</p><p>Did I pique your curiosity? &#x1F61C;</p><h2 id="examples-of-sustainable-acceptance-criteria"><strong>Examples of s</strong>ustainable<strong> acceptance criteria</strong></h2><p>If you are wondering <strong>what</strong> these famous criteria might look like, here are some <strong>real examples</strong> that I regularly put in my user stories:</p><ul><li>Only the data necessary for the application operation are <strong>transmitted</strong> between the different computer systems. &#x1F5A5;</li><li><strong>Data</strong> is <strong>cached</strong> for as <strong>long as</strong> possible. &#x1F648;</li><li>All <strong>caches</strong> are <strong>invalidated</strong> at the appropriate time, such as when updating data or computer systems.</li><li><strong><a href="https://en.wikipedia.org/wiki/SVG">SVG vector images</a></strong> are <a href="https://www.benjaminrancourt.ca/how-to-optimize-your-svgs-with-svgomg/"><strong>optimized</strong> with <strong>SVGOMG</strong></a><strong> </strong>so there is no perceptible difference to the naked eye. &#x1F441;&#xFE0F;</li><li>Images<strong> are available in more compressed</strong> formats than traditional ones, such as <a href="https://en.wikipedia.org/wiki/WebP">WebP</a> and <a href="https://en.wikipedia.org/wiki/AVIF">AVIF</a>. &#x1F5BC;</li><li><strong>Resources</strong> are <strong>compressed in <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> </strong>and <strong><a href="https://en.wikipedia.org/wiki/Brotli">Brotli</a></strong> formats. &#x1F5DC;&#xFE0F;</li><li>JavaScript, HTML and CSS <strong>files are minified</strong> and <em><strong><a href="https://www.quora.com/What-does-uglify-mean">uglified</a></strong></em>.</li></ul><p>Of course, these acceptance criteria are not <strong>always applicable</strong> to all user stories. Some of them are mainly intended for the <strong>design of the product</strong> itself. &#x1F609;</p><h2 id="conclusion"><strong>Conclusion</strong></h2><p>Now that you know that it is possible to write <strong>eco-responsible acceptance criteria</strong>, do you plan to add them to your next user stories? &#x1F917;</p>]]></content:encoded></item><item><title><![CDATA[How to Optimize Your SVGs with SVGOMG]]></title><description><![CDATA[Just like any image on the web, we need to compress and optimize our SVGs to deliver as few bytes as possible to our users. Let's see here how to optimize them with SVGOMG.]]></description><link>https://www.benjaminrancourt.ca/how-to-optimize-your-svgs-with-svgomg/</link><guid isPermaLink="false">6342d698048eb70001aca9ec</guid><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 08 Nov 2022 12:15:43 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/03/pexels-amar-preciado-9969537.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2022/03/pexels-amar-preciado-9969537.jpg" alt="How to Optimize Your SVGs with SVGOMG"><p>Are you able to determine the difference between the two images below? Is it the same picture?</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://www.benjaminrancourt.ca/content/images/2022/03/favicon-1.svg" width="48" height="48" loading="lazy" alt="How to Optimize Your SVGs with SVGOMG"></div><div class="kg-gallery-image"><img src="https://www.benjaminrancourt.ca/content/images/2022/03/favicon.min.svg" width="48" height="48" loading="lazy" alt="How to Optimize Your SVGs with SVGOMG"></div></div></div><figcaption>Take a close look at these two images. Do you see any differences? &#x1F914;</figcaption></figure><p>You probably guessed it, but it&apos;s not the same picture! The image on the right is actually an <strong>optimized version</strong> of the original image on the left.</p><p>This improvement was made manually using a tool I discovered this year in on of the latest <a href="https://frontendfoc.us/issues/534"><em>Frontend Focus</em></a> newsletter: <strong><a href="https://jakearchibald.github.io/svgomg/">SVGOMG</a></strong>.</p><p>SVGOMG is the <strong>M</strong>issing <strong>Graphical user interface </strong><em>of</em> a Node.js tool specialized in <strong>optimizing SVG</strong> files, named <strong><em><a href="https://www.npmjs.com/package/svgo">SVGO</a></em></strong>.</p><p>SVGOMG&apos;s interface is straightforward to use and can be summarized in three steps:</p><ul><li>Upload your SVG image</li><li>Play with the different controls offered to have an almost identical image, but with a lower weight than the original image</li><li>Download the resulting SVG image</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.benjaminrancourt.ca/content/images/2022/03/svgomg.jpg" class="kg-image" alt="How to Optimize Your SVGs with SVGOMG" loading="lazy" width="1916" height="949" srcset="https://www.benjaminrancourt.ca/content/images/size/w600/2022/03/svgomg.jpg 600w, https://www.benjaminrancourt.ca/content/images/size/w1000/2022/03/svgomg.jpg 1000w, https://www.benjaminrancourt.ca/content/images/size/w1600/2022/03/svgomg.jpg 1600w, https://www.benjaminrancourt.ca/content/images/2022/03/svgomg.jpg 1916w" sizes="(min-width: 720px) 720px"><figcaption><font style="border-color: var(--gray-200); border-style: solid; border-width: 0px; box-sizing: border-box; --tw-border-spacing-x:0; --tw-border-spacing-y:0; --tw-translate-x:0; --tw-translate-y:0; --tw-rotate:0; --tw-skew-x:0; --tw-skew-y:0; --tw-scale-x:1; --tw-scale-y:1; --tw-pan-x:; --tw-pan-y:; --tw-pinch-zoom:; --tw-scroll-snap-strictness:proximity; --tw-ordinal:; --tw-slashed-zero:; --tw-numeric-figure:; --tw-numeric-spacing:; --tw-numeric-fraction:; --tw-ring-inset:; --tw-ring-offset-width:0px; --tw-ring-offset-color:#fff; --tw-ring-color:rgba(59,130,246,0.5); --tw-ring-offset-shadow:0 0 #0000; --tw-ring-shadow:0 0 #0000; --tw-shadow:0 0 #0000; --tw-shadow-colored:0 0 #0000; --tw-blur:; --tw-brightness:; --tw-contrast:; --tw-grayscale:; --tw-hue-rotate:; --tw-invert:; --tw-saturate:; --tw-sepia:; --tw-drop-shadow:; --tw-backdrop-blur:; --tw-backdrop-brightness:; --tw-backdrop-contrast:; --tw-backdrop-grayscale:; --tw-backdrop-hue-rotate:; --tw-backdrop-invert:; --tw-backdrop-opacity:; --tw-backdrop-saturate:; --tw-backdrop-sepia:; vertical-align: inherit;"><font style="border-color: var(--gray-200); border-style: solid; border-width: 0px; box-sizing: border-box; --tw-border-spacing-x:0; --tw-border-spacing-y:0; --tw-translate-x:0; --tw-translate-y:0; --tw-rotate:0; --tw-skew-x:0; --tw-skew-y:0; --tw-scale-x:1; --tw-scale-y:1; --tw-pan-x:; --tw-pan-y:; --tw-pinch-zoom:; --tw-scroll-snap-strictness:proximity; --tw-ordinal:; --tw-slashed-zero:; --tw-numeric-figure:; --tw-numeric-spacing:; --tw-numeric-fraction:; --tw-ring-inset:; --tw-ring-offset-width:0px; --tw-ring-offset-color:#fff; --tw-ring-color:rgba(59,130,246,0.5); --tw-ring-offset-shadow:0 0 #0000; --tw-ring-shadow:0 0 #0000; --tw-shadow:0 0 #0000; --tw-shadow-colored:0 0 #0000; --tw-blur:; --tw-brightness:; --tw-contrast:; --tw-grayscale:; --tw-hue-rotate:; --tw-invert:; --tw-saturate:; --tw-sepia:; --tw-drop-shadow:; --tw-backdrop-blur:; --tw-backdrop-brightness:; --tw-backdrop-contrast:; --tw-backdrop-grayscale:; --tw-backdrop-hue-rotate:; --tw-backdrop-invert:; --tw-backdrop-opacity:; --tw-backdrop-saturate:; --tw-backdrop-sepia:; vertical-align: inherit;">Screenshot from SVGOMG, dated March 23, 2022.</font></font></figcaption></figure><p>One of the advantages of using a GUI and going about it with a <strong>manual process</strong> is that we can more easily ensure the <strong>quality</strong> of the resulting image.</p><p>My favicon&apos;s image size went from 73,728 bytes to 20,480 bytes! This represents a <strong>saving of approximately 72%</strong> of the original image! Impressive, right?&#x1F62E;</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Although the interface shows different sizes in the lower right corner, this is due to the fact that I left the &quot;<em>Compare gzipped</em> &quot; option activated.<br><br>Indeed, it compares the size of files compressed with <a href="https://fr.wikipedia.org/wiki/Gzip">gzip</a>! &#x1F609;</div></div><p>You will now have one more tool in your toolbox! &#x1F60B;</p>]]></content:encoded></item><item><title><![CDATA[Frequently Asked Questions About Open Data in Sherbrooke]]></title><description><![CDATA[Would you like to know more about the open data published by the Ville de Sherbrooke and its partners? This FAQ may answer all your questions! 🤗]]></description><link>https://www.benjaminrancourt.ca/open-data-in-sherbrooke/</link><guid isPermaLink="false">631a843ab652ad0001cb85a7</guid><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 04 Oct 2022 11:15:27 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/08/pexels-thisisengineering-3861969.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2022/08/pexels-thisisengineering-3861969.jpg" alt="Frequently Asked Questions About Open Data in Sherbrooke"><p>Last year, on December 6, 2021, the <em><a href="https://www.sherbrooke.ca/en">Ville de Sherbrooke</a></em> adopted its <a href="https://www.sherbrooke.ca/Fichiers/3337a882-4a53-e611-80ea-00155d09650f/Sites/333dd3d3-915d-e611-80ea-00155d09650f/Documents/Politiques/2021-politique-administrative-donnees_ouvertes.pdf"><strong>Open Data Policy</strong></a> at its <a href="https://www.sherbrooke.ca/fr/vie-municipale/seances-du-conseil/281/comite-plenier-public-et-seance-ordinaire">regular council meeting</a>.</p><p>To celebrate its first anniversary, I decided to create this reference article on open data in Sherbrooke. &#x1F973;</p><p>This article may also help the participants of the next <a href="https://hackqc.ca/">HackQC</a>, an IT competition using open data, whose theme will be &quot;<em>Data at the service of the <strong>ecological transition</strong></em>&quot;. &#x1F60E;</p><p>Are you ready to dig deeper into open data in Sherbrooke? &#x1F609;</p><h2 id="frequently-asked-questions">Frequently Asked Questions</h2><h3 id="what-is-open-data">What is Open Data?</h3><p>Although I have already mentioned the term several times in this article, I have never defined this one. But what exactly is open data? See the <a href="https://vitrinelinguistique.oqlf.gouv.qc.ca/fiche-gdt/fiche/26519745/donnees-ouvertes">definition</a> on the <a href="https://vitrinelinguistique.oqlf.gouv.qc.ca/">linguistic showcase</a> of the <a href="https://www.oqlf.gouv.qc.ca/"><em>Office qu&#xE9;b&#xE9;cois de la langue fran&#xE7;aise</em></a> (OQLF):</p><blockquote class="kg-blockquote-alt">Non-personal <strong>raw data</strong> <strong>free of rights</strong>, produced or collected by a public or private organization, and <strong>accessible to citizens</strong> via the <strong>Internet</strong>.</blockquote><p>These data are generally delivered in one or more open formats (not requiring proprietary software), to facilitate their <strong>reuse</strong>.</p><h3 id="why-does-the-ville-de-sherbrooke-publish-open-data">Why does the Ville de Sherbrooke publish open data?</h3><p>As part of its open data policy, the <em>Ville de Sherbrooke</em> has identified several advantages to publishing open data:</p><ul><li>data can be <strong>enriched</strong> and its potential increased;</li><li>the citizens can <strong>obtain</strong> the desired information <strong>themselves</strong>;</li><li><strong>useful applications</strong> for the community or research can be established;</li><li>and it encourages <strong>innovation</strong> and sustainable economic growth.<br></li></ul><h3 id="where-can-i-find-the-ville-de-sherbrookes-open-data">Where can I find the Ville de Sherbrooke&apos;s open data?</h3><p>The data can be found on the <em><a href="https://www.donneesquebec.ca/">Partenariat Donn&#xE9;es Qu&#xE9;bec</a> website</em>. As of this writing, <strong>34 datasets</strong> have been released.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://www.donneesquebec.ca/recherche/dataset?q=&amp;extras_organisation_principale=ville-de-sherbrooke"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Jeu de donn&#xE9;es - Donn&#xE9;es Qu&#xE9;bec</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://www.donneesquebec.ca/recherche/images/favicon.ico" alt="Frequently Asked Questions About Open Data in Sherbrooke"><span class="kg-bookmark-author">ville-de-sherbrooke</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://www.donneesquebec.ca/recherche/uploads/group/2016-01-22-204608.163487eee.jpg" alt="Frequently Asked Questions About Open Data in Sherbrooke"></div></a><figcaption>Datasets published by the Ville de Sherbrooke.</figcaption></figure><p>The open data produced by the geomatics team can also be consulted via a dedicated site linked to <a href="https://www.arcgis.com/index.html">ArcGIS</a>, a suite of geomatics software used by the <em>Ville de Sherbrooke</em>.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://donneesouvertes-sherbrooke.opendata.arcgis.com/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Donn&#xE9;es ouvertes de la Ville de Sherbrooke</div><div class="kg-bookmark-description">Portail de donn&#xE9;es ouvertes de la Ville de Sherbrooke</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://arcgis.com/favicon.ico" alt="Frequently Asked Questions About Open Data in Sherbrooke"></div></div><div class="kg-bookmark-thumbnail"><img src="https://cartes.ville.sherbrooke.qc.ca/Hyperliens/Logo/logoSherbrooke.png" alt="Frequently Asked Questions About Open Data in Sherbrooke"></div></a><figcaption>Map data portal.</figcaption></figure><h2 id="what-open-data-does-the-ville-de-sherbrooke-offer">What open data does the Ville de Sherbrooke offer?</h2><p>By combining the two previous sources, I was able to list the following 34 datasets:</p><ol><li>&#x1F30E; Developed areas</li><li>&#x1F30E; Boroughs</li><li>&#x1F30E; Buildings</li><li>&#x26A0;&#xFE0F;&#x1F30E; Municipal buildings and services</li><li>Real-time event calendar</li><li>&#x1F30E; Contours and Digital Elevation Model (DEM)</li><li>Dataset requests</li><li>&#x1F30E; Electoral districts</li><li>Defibrillators</li><li>Ecocentres - list and schedules</li><li>Events - <em>Soci&#xE9;TIC</em></li><li>Events in the Sherbrooke region (What to do - GVQ standardization)</li><li>&#x1F30E; Parking meters</li><li>&#x1F30E; Public Safety Incidents</li><li>Login information</li><li>Residual materials - Materials accepted</li><li>Walls dedicated to temporary and permanent graffiti</li><li>Where to eat?</li><li>&#x26A0;&#xFE0F;&#x1F30E; Road works</li><li>&#x1F30E; Parking signs</li><li>&#x26A0;&#xFE0F;&#x1F30E; Pools and beaches</li><li>Cycle paths</li><li>&#x1F30E; Street snow removal priorities</li><li>Urban perimeter</li><li>What to do?</li><li>Directory of businesses in the Sherbrooke region</li><li>&#x1F30E; Waste collection channels</li><li>&#x1F30E; Street Segments</li><li>&#x1F30E; Walking trails</li><li>&#x1F30E; Landmarks</li><li>&#x1F30E; Public car parks</li><li>Park Recreational Structures</li><li>Public transport</li><li>&#x1F30E; Work in progress</li><li>ZAP: access points</li><li>&#x1F30E; Landslide areas</li><li>&#x1F30E; Flood zones<br></li></ol><p>The datasets identified by a globe (&#x1F30E;) are the datasets from the geomatics software ArcGIS.</p><p>As for those identified by an exclamation mark (&#x26A0;&#xFE0F;), these datasets do not seem to be present on the <em>Donn&#xE9;es Qu&#xE9;bec</em> website yet.</p><h2 id="which-sherbrooke-organizations-participate-in-open-data">Which Sherbrooke organizations participate in open data?</h2><p>According to <em>Donn&#xE9;es Qu&#xE9;bec</em>, the main organizations and paramunicipal organizations are, in alphabetical order:</p><ul><li><a href="https://www.destinationsherbrooke.com/fr"><em>Destination Sherbrooke</em></a></li><li><a href="https://www.entreprendresherbrooke.com/"><em>Entreprendre Sherbrooke</em></a></li><li><a href="https://societic.ca/"><em>Soci&#xE9;TIC</em></a></li><li><a href="https://www.sts.qc.ca/"><em>Soci&#xE9;t&#xE9; de transport de Sherbrooke</em></a> (STS)</li><li><a href="https://www.sherbrooke.ca/en"><em>Ville de Sherbrooke</em></a></li><li><a href="https://www.zapsherbrooke.org/"><em>ZAP Sherbrooke</em></a></li></ul><h3 id="who-to-contact-to-request-the-release-of-new-datasets">Who to contact to request the release of new datasets?</h3><p>According to the Open Data Policy, the Communications Department is responsible for receiving requests for new datasets. This service can <a href="https://www.sherbrooke.ca/en/contact-us">be reached</a> by email at <a href="mailto:communications@sherbrooke.ca">communications@sherbrooke.ca</a>.</p><h2 id="under-what-license-is-the-ville-de-sherbrookes-open-data-published">Under what license is the Ville de Sherbrooke&apos;s open data published?</h2><p>All open data is released under the <em>Attribution 4.0 International License (CC BY 4.0)</em>.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://creativecommons.org/licenses/by/4.0/deed.en"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Creative Commons &#x2014; Attribution 4.0 International &#x2014; CC BY 4.0</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://creativecommons.org/favicon.ico" alt="Frequently Asked Questions About Open Data in Sherbrooke"><span class="kg-bookmark-author">cc.logo.white</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://creativecommons.org/images/language_icon_x2.png" alt="Frequently Asked Questions About Open Data in Sherbrooke"></div></a><figcaption>CC BY 4.0 License Summary.</figcaption></figure><p>Under the terms of the license, it is therefore permitted to:</p><ul><li><strong>Share </strong>&#x2014; copy, distribute and communicate the material in any format and by any means</li><li><strong>Adapt </strong>&#x2014; transform and create from the material for any use, including commercial</li></ul><p>under the following conditions:</p><ul><li><strong>Attribution</strong> &#x2014; The dataset must be <strong>credited</strong> and a link to its license must be embedded. In addition, all changes made to the data must be indicated. This information should appear, by all reasonable means, without suggesting that the dataset owner supports you in how you use their dataset.</li><li><strong>No Additional Restrictions</strong> &#x2014; No legal requirement or technical measure can prevent others from using the same dataset.</li></ul><h3 id="how-do-i-know-when-the-ville-de-sherbrooke-publishes-or-updates-datasets">How do I know when the Ville de Sherbrooke publishes or updates datasets?</h3><p>Those interested can subscribe to the <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom</a> feed below with their favourite feed reader, such as <a href="https://feedly.com/">Feedly</a>.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://www.donneesquebec.ca/recherche/feeds/custom.atom?q=&amp;q=&amp;extras_organisation_principale=ville-de-sherbrooke"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Donn&#xE9;es Qu&#xE9;bec - Custom query</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://www.donneesquebec.ca/favicon.ico" alt="Frequently Asked Questions About Open Data in Sherbrooke"><span class="kg-bookmark-author">Custom query</span></div></div></a><figcaption>Atom feed to follow <em>Ville de Sherbrooke</em> datasets, recently created or updated on the <em>Donn&#xE9;es Qu&#xE9;bec</em> website.</figcaption></figure><h3 id="where-can-i-find-examples-of-research-papers-using-open-data-on-sherbrooke">Where can I find examples of research papers using open data on Sherbrooke?</h3><p>I don&apos;t know if the Ville de Sherbrooke maintains such a registry, but here is a non-exhaustive list of research reports that I found while writing this article:</p><ul><li><strong>[French]</strong> <a href="https://contenu.maruche.ca/Fichiers/3337a882-4a53-e611-80ea-00155d09650f/Sites/333ceda8-915d-e611-80ea-00155d09650f/Documents/Routes%20et%20transport/Projet_rues_conviviales_Rapport%20final_2020-12-29.pdf"><em>&#xC9;valuation du potentiel des rues de la Ville de Sherbrooke &#xE0; &#xEA;tre transform&#xE9;es en rues partag&#xE9;es</em></a>, by Alexandre Cailhier (December 29, 2020)</li></ul><h2 id="references">References</h2><ul><li><strong>[French] </strong><a href="https://www.sherbrooke.ca/Fichiers/3337a882-4a53-e611-80ea-00155d09650f/Sites/333dd3d3-915d-e611-80ea-00155d09650f/Documents/Politiques/2021-politique-administrative-donnees_ouvertes.pdf">Politique de donn&#xE9;es ouvertes (ADM-2115)</a>, on the <em>Ville de Sherbrooke</em> website</li></ul>]]></content:encoded></item><item><title><![CDATA[How to Import a Custom PowerShell Module Into a Dockerfile 👨‍🍳]]></title><description><![CDATA[If you want to use a custom PowerShell module in a Docker container, the command to import it correctly is simple when you know it! 😋]]></description><link>https://www.benjaminrancourt.ca/how-to-import-a-custom-powershell-module-into-a-dockerfile/</link><guid isPermaLink="false">63167fccb652ad0001cb8150</guid><category><![CDATA[PowerShell]]></category><category><![CDATA[GitLab]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 27 Sep 2022 11:15:47 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/09/pexels-magda-ehlers-6492315.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-green"><div class="kg-callout-emoji">&#x1F970;</div><div class="kg-callout-text">This post wouldn&apos;t have been possible without <strong>Jesse Chisholm</strong>, who last month gave me the solution below. Thanks Jesse! &#x2764;</div></div><img src="https://www.benjaminrancourt.ca/content/images/2022/09/pexels-magda-ehlers-6492315.jpg" alt="How to Import a Custom PowerShell Module Into a Dockerfile &#x1F468;&#x200D;&#x1F373;"><p>In one of my previous posts on <a href="https://www.benjaminrancourt.ca/lint-powershell-scripts-with-psscriptanalyzer/"><strong>linting PowerShell scripts</strong></a>, I wasn&apos;t impressed with how <a href="https://docs.gitlab.com/ee/ci/">GitLab CI</a> displays <a href="https://github.com/PowerShell/PSScriptAnalyzer"><strong>PSScriptAnalyzer</strong></a><strong> logs</strong>. I ended my post by asking my readers if they know how we could improve the readability of logs and one reader, Jesse Chisholm, suggested I try the script below.</p><figure class="kg-card kg-code-card"><pre><code class="language-powershell">function Show-LintMessages()
{
  $output = &quot;&quot;
  $input|ForEach-Object -Process {
    $I = $_;
    $output += ($I | Select RuleName,Severity,ScriptName,Line | Format-Table | Out-String);
    $output += ($I | Select Message | Format-Table | Out-String);
    }
  $output | Out-Host;
}

# Demonstration of function
Invoke-ScriptAnalyzer -ScriptDefinition &apos;&quot;b&quot; = &quot;b&quot;; function eliminate-file (){}&apos; | Show-LintMessages
</code></pre><figcaption>The script that Jesse gave me to display the results in a prettier way.</figcaption></figure><p>After trying it, it was exactly what I was looking for! &#x1F929;</p><p>But, since I was using PSScriptAnalyzer with <a href="https://www.docker.com/"><strong>Docker</strong></a>, I didn&apos;t want to declare this function every time I called the analyzer... So I started looking for how to make it available in my Docker container. &#x1F914;</p><p>After a while, I finally managed to install the function and make it available in the terminal with a <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_modules"><strong>PowerShell module</strong></a>. By adding it to the <code>/usr/local/share/powershell/Modules</code> folder, the module is automatically imported and every user who connects to the Docker container has access to the <code>Show-LintMessages</code> function! &#x1F917;</p><figure class="kg-card kg-code-card"><pre><code class="language-docker">COPY Show-LintMessages.psm1 /usr/local/share/powershell/Modules/Show-LintMessages/Show-LintMessages.psm1</code></pre><figcaption>The Docker instruction to add a custom PowerShell module into a Docker container.</figcaption></figure><p>In GitLab CI, I also had to pipe the output of PSScriptAnalyser to the new command.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml"> - pwsh -Command &quot;Invoke-ScriptAnalyzer -EnableExit -Recurse -Path . | Show-LintMessages&quot;</code></pre><figcaption>The <code>Show-LintMessages</code> function will format the output of the previous command.</figcaption></figure><p>Unfortunately, the message was truncated in the GitLab CI job output... &#x1F613;</p><figure class="kg-card kg-code-card"><pre><code>&apos;Select&apos; is an alias of &apos;Select-Object&apos;. Alias can introduce possible problems and make scripts hard to maintain. Pleas&#x2026;
RuleName                  Severity ScriptName             Line
--------                  -------- ----------             ----
PSAvoidUsingCmdletAliases  Warning Show-LintMessages.psm1    6
Message
-------
&apos;Select&apos; is an alias of &apos;Select-Object&apos;. Alias can introduce possible problems and make scripts hard to maintain. Pleas&#x2026;
RuleName              Severity ScriptName Line
--------              -------- ---------- ----
PSAvoidUsingWriteHost  Warning test.ps1      1
Message
-------
File &apos;test.ps1&apos; uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when ther&#x2026;</code></pre><figcaption>The results in GitLab CI were unfortunately truncated and didn&apos;t contain blank lines...</figcaption></figure><p>So I tweaked the function a bit, and after a few tries, I finally ended with the script below.</p><figure class="kg-card kg-code-card"><pre><code class="language-powershell">function Show-LintMessages() {
  $input|ForEach-Object -Process {
    Write-Output &quot;--------------------------------------------------------------------------------&quot;;
    $PSItem | Select-Object RuleName,Severity,ScriptName,Line | Format-Table -Wrap | Out-String -Width 80;
    Write-Output &quot; &quot;;
    $PSItem | Select-Object Message | Format-Table -Wrap | Out-String -Width 80;
    Write-Output &quot;--------------------------------------------------------------------------------&quot;;
    Write-Output &quot; &quot;;
  }
}
</code></pre><figcaption>The final content of the <code>Show-LintMessages.psm1</code> file.</figcaption></figure><p>And the output is now much better in GitLab CI! &#x1F604;</p><figure class="kg-card kg-code-card"><pre><code>--------------------------------------------------------------------------------
RuleName              Severity ScriptName Line
--------              -------- ---------- ----
PSAvoidUsingWriteHost  Warning test.ps1      1

Message
-------
File &apos;test.ps1&apos; uses Write-Host. Avoid using Write-Host because it might not
work in all hosts, does not work when there is no host, and (prior to PS 5.0)
cannot be suppressed, captured, or redirected. Instead, use Write-Output,
Write-Verbose, or Write-Information.
--------------------------------------------------------------------------------</code></pre><figcaption>The same message, but displayed with a custom script to look better in GitLab CI.</figcaption></figure><p>It&apos;s more readable, isn&apos;t it? &#x1F609;</p>]]></content:encoded></item><item><title><![CDATA[How to Optimize Card Assets in a Ghost Theme]]></title><description><![CDATA[After thoroughly reviewing Ghost, I was able to remove 5.4 kB for the cards I used. That's a reduction of 77.14% of the original files. Want to know how I did this? 😉]]></description><link>https://www.benjaminrancourt.ca/how-to-optimize-card-assets-in-a-ghost-theme/</link><guid isPermaLink="false">62e2cda235df8d00017e6673</guid><category><![CDATA[Ghost CMS]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 23 Aug 2022 11:15:08 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/07/pexels-tima-miroshnichenko-6255288.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2022/07/pexels-tima-miroshnichenko-6255288.jpg" alt="How to Optimize Card Assets in a Ghost Theme"><p>If you&apos;re a creator using <a href="https://ghost.org/">Ghost CMS</a> (or a Ghost theme developer), you might want your website to be fast to provide a great experience for your users. There are many ways to optimize the performance of a Ghost website. Today, I want to talk about one way that seems to be under looked according to <a href="https://github.com/search?l=JSON&amp;q=%22card_assets%22%3A+true&amp;type=Code">this search</a>: by <strong>optimizing card assets</strong>.</p><p>One thing I absolutely love about Ghost is that they offer so <strong>many tools</strong> to enhance your post content: buttons, bookmarks, galleries, headers, products, audio, video, and more. </p><p>But, if you don&apos;t use these features, chances are your website is <strong>unnecessarily loading</strong> JavaScript and/or CSS related to them. How can you quickly check if this is your case?</p><p>Open your <strong>theme&apos;s package.json file</strong> and check if you have the <code>card_assets</code> property in it.</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{ 
  &quot;config&quot;: {
    &quot;card_assets&quot;: true
  }
}</code></pre><figcaption>Excerpt of a default theme&apos;s package.json file.&#xA0;</figcaption></figure><p>If you can&apos;t find this setting inside your theme&apos;s package.json file, the default value for this property has been <code>true</code> <a href="https://ghost.org/docs/changes/#ghost-50">since Ghost 5.0</a>.</p><p>So, what does this imply? These settings tell Ghost to automatically provide and merge many<strong> default CSS and JavaScript assets</strong> into a <code>cards.min.css</code> and a <code>cards.min.js</code> files.</p><p>Take a look at these files <a href="https://demo.ghost.io/public/cards.min.css">here</a> (5.0 kB) and <a href="https://demo.ghost.io/public/cards.min.js">here</a> (2.0 kB) for the <a href="https://demo.ghost.io/">demo.ghost.io</a> website. There&apos;s a lot of code, isn&apos;t it? &#x1F632;</p><p>But, did you know that you can exclude the features you don&apos;t use? I did, and my <a href="https://www.benjaminrancourt.ca/public/cards.min.css">cards.min.css file</a> is down to 1.6 kB, a reduction of 3.4 kB (68%)! And my <a href="https://www.benjaminrancourt.ca/public/cards.min.js">cards.min.js file</a> has been completely deleted!</p><p>It turns out there&apos;s an <strong><a href="https://ghost.org/docs/themes/content/#editor-cards">exclude option</a></strong> you can use to remove the default card implementations in Ghost.</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{ 
  &quot;config&quot;: {
    &quot;card_assets&quot;: {
      &quot;exclude&quot;: [
        &quot;audio&quot;,
        &quot;blockquote&quot;,
        &quot;bookmark&quot;,
        &quot;button&quot;,
        &quot;gallery&quot;,
        &quot;nft&quot;,
        &quot;product&quot;,
        &quot;toggle&quot;,
        &quot;video&quot;
      ]
    }
  }
}</code></pre><figcaption>My current exclude option, where it removes the default implementation of many features.</figcaption></figure><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">&#x1F9D0;</div><div class="kg-callout-text">If you add a card name to the <code>exclude</code> list, it doesn&apos;t mean you can&apos;t use this feature anymore! It only means that your local theme is now <strong>100% responsible</strong> for implementing the necessary code.<br><br>For example, I still have quotes, galleries and buttons in some of my posts, don&apos;t worry! &#x1F605;</div></div><p>You can also disable all cards assets by setting <code>card_assets</code> to false.</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{ 
  &quot;config&quot;: {
    &quot;card_assets&quot;: false
  }
}</code></pre><figcaption>If your theme has already implemented all cards, you can use these settings instead.</figcaption></figure><p>A simple configuration could immediately reduce your website&apos;s response time. Isn&apos;t that awesome? &#x1F917;</p><h2 id="faq">FAQ</h2><h3 id="what-files-are-automatically-included-in-the-cardsmincss-and-cardsminjs-files">What files are automatically included in the cards.min.css and cards.min.js files?</h3><p>If you look at this <a href="https://github.com/TryGhost/Ghost/tree/main/ghost/core/core/frontend/src/cards/css">CSS directory</a> and this <a href="https://github.com/TryGhost/Ghost/tree/main/ghost/core/core/frontend/src/cards/js">JavaScript directory</a>, you&apos;ll find all the files that are merged and minified by Ghost. A theme developer will probably want to exclude some of these features if they have already implemented them.</p><p>As of July 31st, 2022, the included files were:</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Name</th>
<th>CSS file</th>
<th>JavaScript file</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#audio-upload-card">audio</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/audio.css">audio.css</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/js/audio.js">audio.js</a></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#alternative-blockquote-style">blockquote</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/blockquote.css">blockquote.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#bookmark-card">bookmark</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/bookmark.css">bookmark.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#button-card">button</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/button.css">button.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#callout-card">callout</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/callout.css">callout.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#file-upload-card">file</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/file.css">file.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#gallery-card">gallery</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/gallery.css">gallery.css</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/js/gallery.js">gallery.js</a></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#header-card">header</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/header.css">header.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#nft-card">nft</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/nft.css">nft.css</a></td>
<td></td>
</tr>
<tr>
<td>product</td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/product.css">product.css</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#toggle-card">toggle</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/toggle.css">toggle.css</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/js/toggle.js">toggle.js</a></td>
</tr>
<tr>
<td><a href="https://ghost.org/docs/themes/content/#video-upload-card">video</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/css/video.css">video.css</a></td>
<td><a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/cards/js/video.js">video.js</a></td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="what-would-a-configuration-that-disables-all-default-implementations-look-like">What would a configuration that disables all default implementations look like?</h3><p>If you listed all the previous card names, it would look something like below:</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{ 
  &quot;config&quot;: {
    &quot;card_assets&quot;: {
      &quot;exclude&quot;: [
        &quot;audio&quot;,
        &quot;blockquote&quot;,
        &quot;bookmark&quot;,
        &quot;button&quot;,
        &quot;callout&quot;,
        &quot;file&quot;,
        &quot;gallery&quot;,
        &quot;header&quot;,
        &quot;nft&quot;,
        &quot;product&quot;,
        &quot;toggle&quot;,
        &quot;video&quot;
      ]
    }
  }
}</code></pre><figcaption>An exclude option that removes the default implementation of many features.</figcaption></figure><h3 id="can-i-use-an-allowlist-include-instead-of-a-disallow-list-exclude">Can I use an allowlist (include) instead of a disallow list (exclude)?</h3><p>According to the <a href="https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/services/card-assets/service.js">current CardAssetService implementation</a>, you can, but I haven&apos;t been able to get it to work yet. Let me know if you succeed! &#x1F60B;</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">// CASE: the theme has declared an include directive, we should include exactly these assets
// Include rules take precedence over exclude rules.
if (_.has(this.config, &apos;include&apos;)) {
  return {
    &apos;cards.min.css&apos;: `css/(${this.config.include.join(&apos;|&apos;)}).css`,
    &apos;cards.min.js&apos;: `js/(${this.config.include.join(&apos;|&apos;)}).js`
  };
}</code></pre><figcaption>Excerpt of the <code>CardAssetService</code> file.</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[How to Create a Dictionary from an Array in JavaScript]]></title><description><![CDATA[JavaScript arrays are great, but sometimes, having a single object (a dictionary) that contains all elements can significantly simplify the complexity of our code!]]></description><link>https://www.benjaminrancourt.ca/how-to-create-a-dictionary-from-an-array-in-javascript/</link><guid isPermaLink="false">62bcd159398df50001d554e5</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 16 Aug 2022 11:15:55 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/06/pexels-snapwire-6997.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2022/06/pexels-snapwire-6997.jpg" alt="How to Create a Dictionary from an Array in JavaScript"><p>Often, I end up with a list of objects and want to find some items on the list using a <strong>unique identifier.</strong> One example: enhancing other data that relates to an item on my list.</p><p>There are many ways to do this, but some are better than others, especially when there is a <strong>lot of data</strong>.</p><p>Let&apos;s see with a simple example:</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">const data = [{
  id: 3,
  name: &quot;New Brunswick&quot;
 }, {
  id: 8,
  name: &quot;Ontario&quot;
 }, {
  id: 10,
  name: &quot;Quebec&quot;
 },
];</code></pre><figcaption>An abbreviate list of provinces and territories of Canada. For this example, the provinces only have two attributes, but they have more in real life.</figcaption></figure><p>If we want to retrieve the name of the province identified by the id 10, how do we do it? &#x1F914;</p><pre><code class="language-javascript">const province10 = `?`;</code></pre><p>Let&apos;s see some solutions we could use! &#x1F917;</p><h2 id="solution-0using-find">Solution #0 - Using find</h2><p>A simple solution could be to use the <strong>find function</strong> to retrieve the first element that satisfies our search:</p><pre><code class="language-javascript">const province10 = data.find(element =&gt; element.id === 10);
</code></pre><p>This works perfectly, but it may <strong>not be quick</strong> if you need to find a lot of items and/or multiple times. Let&apos;s see if we can do better. &#x1F60B;</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Array.prototype.find() - JavaScript | MDN</div><div class="kg-bookmark-description">The find() method returns the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://developer.mozilla.org/favicon-48x48.cbbd161b.png" alt="How to Create a Dictionary from an Array in JavaScript"><span class="kg-bookmark-author">MDN Web Docs.logo-m{fill:var(--text-link)}</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://developer.mozilla.org/mdn-social-share.cd6c4a5a.png" alt="How to Create a Dictionary from an Array in JavaScript"></div></a></figure><h2 id="solution-1using-reduce">Solution #1 - Using reduce</h2><p>What if we had the following data in our hands:</p><pre><code class="language-javascript">{
  3: {id: 3, name: &apos;New Brunswick&apos;},
  8: {id: 8, name: &apos;Ontario&apos;},
  10: {id: 10, name: &apos;Quebec&apos;},
}</code></pre><p>Some people call this structure a <a href="https://www.w3schools.com/python/python_dictionaries.asp"><strong>dictionary</strong></a> or an <strong>index</strong>. If we had it, we could directly access the element we want using its identifier: <code>dictionary[10]</code>. Wouldn&apos;t that be easier? &#x1F914;</p><p>Let&apos;s see how we can create it using one of my favourite array functions: <strong>reduce</strong>! &#x1F970;</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Array.prototype.reduce() - JavaScript | MDN</div><div class="kg-bookmark-description">The reduce() method executes a user-supplied &#x201C;reducer&#x201D; callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element. The final result of running the reducer across all elements of the array is a single value.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://developer.mozilla.org/favicon-48x48.cbbd161b.png" alt="How to Create a Dictionary from an Array in JavaScript"><span class="kg-bookmark-author">MDN Web Docs.logo-m{fill:var(--text-link)}</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://developer.mozilla.org/mdn-social-share.cd6c4a5a.png" alt="How to Create a Dictionary from an Array in JavaScript"></div></a></figure><pre><code class="language-javascript">const dictionary = data.reduce((dictionary, province) =&gt; {
  dictionary[province.id] = province;
  return dictionary;
}, {});

const province10 = dictionary[10];
// {id: 10, name: &apos;Quebec&apos;} </code></pre><p>The biggest advantage of this structure is that you only need to iterate on your array <strong>one time </strong>(to build the dictionary). If you have a lot of data, you should be able to get the element you want faster. &#x1F911;</p><p>But, if you&apos;re not a fan of <a href="https://en.wikipedia.org/wiki/Functional_programming">functional programming</a>, I&apos;ve another solution you could use. &#x1F609;</p><h2 id="solution-2using-object-destructuring">Solution #2 - Using object destructuring</h2><p>While writing this post, I found another elegant solution that I like, but wouldn&apos;t use in a good codebase because it&apos;s more complex.</p><pre><code class="language-javascript">const dictionary = Object.assign({}, ...data.map((x) =&gt; ({[x.id]: x})));</code></pre><p>Do you understand easily this one? Let me help you if not.</p><pre><code class="language-javascript">const dictionary = Object.assign({}, ...[
    {8: {id:8,name:&quot;Ontario&quot;}},
    {10: {id:10,name:&quot;Quebec&quot;}},
])</code></pre><p>This solution use <strong>destructuring</strong> to create a dictionary with lots of smaller objects. All the generated objects are <strong>merged </strong>into a simpler one.</p><p>More complex, right? &#x1F605;</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Destructuring assignment - JavaScript | MDN</div><div class="kg-bookmark-description">The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://developer.mozilla.org/favicon-48x48.cbbd161b.png" alt="How to Create a Dictionary from an Array in JavaScript"><span class="kg-bookmark-author">MDN Web Docs.logo-m{fill:var(--text-link)}</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://developer.mozilla.org/mdn-social-share.cd6c4a5a.png" alt="How to Create a Dictionary from an Array in JavaScript"></div></a></figure><h2 id="conclusion">Conclusion</h2><p>I hope this post introduced you to the concept of a JavaScript dictionary and that it helps you in the future! &#x1F917;</p>]]></content:encoded></item><item><title><![CDATA[How to Delete a Service Worker]]></title><description><![CDATA[Surprisingly, if you want to completely remove a service worker from a website, you need more than deleting the file on your server. Explanations below.]]></description><link>https://www.benjaminrancourt.ca/how-to-remove-a-service-worker/</link><guid isPermaLink="false">62c36d8371a2c2000148aa1d</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Benjamin Rancourt]]></dc:creator><pubDate>Tue, 09 Aug 2022 11:15:31 GMT</pubDate><media:content url="https://www.benjaminrancourt.ca/content/images/2022/07/pexels-cottonbro-4488636.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://www.benjaminrancourt.ca/content/images/2022/07/pexels-cottonbro-4488636.jpg" alt="How to Delete a Service Worker"><p>When I decided to make full use <a href="https://ghost.org/">Ghost</a> as my CMS (<em>Content Management System</em>), I had to reimplement some features that I previously programmed. As I wanted to quickly ship this new version into production, I chose to skip some features that I might add again in the future. One of these features was that my website was a <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps"><strong>Progressive Web App</strong></a> (better known as a <em>PWA</em>).</p><p>PWA uses <strong><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Workers</a></strong>, a small JavaScript file that provides certain features like an offline experience to users. Unfortunately for me, the service worker is cached by the browser and if I deleted that file, the service workers installed on my visitors&apos; browsers would <strong>not be updated</strong> because the file would result in a <strong>404 error page</strong>.</p><p>Since I didn&apos;t want to ask my visitors to manually <a href="https://umaar.com/dev-tips/197-clear-site-data/">clear site data</a>, I had to find a way to delete it programmatically. &#x1F916;</p><p>Fortunately, I knew there was a way to remove it using a <strong>self-destructing service worker</strong>. &#x1F9E8;</p><p>Simply replace your <code>service-worker.js</code> (or <code>sw.js</code>) file and the next time a user visits your website, the service worker on its navigator will self-destruct! &#x1F4A5;</p><figure class="kg-card kg-code-card"><pre><code class="language-javascript">// Inspired from https://github.com/NekR/self-destroying-sw
self.addEventListener(&quot;install&quot;, (event) =&gt; {
  self.skipWaiting();
});

self.addEventListener(&quot;activate&quot;, (event) =&gt; {
  self.registration
    .unregister()
    .then(() =&gt; self.clients.matchAll())
    .then((clients) =&gt; {
      clients.forEach((client) =&gt; {
        if (client.url &amp;&amp; &quot;navigate&quot; in client) {
          client.navigate(client.url);
        }
      });
    });
});
</code></pre><figcaption>The code that will replace our service worker code.</figcaption></figure><p>A useful snippet to keep on hand if you play with service workers! &#x1F929;</p><p>And if your want to know more about removing old services workers, Chrome Developers has a great article on that:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developer.chrome.com/docs/workbox/remove-buggy-service-workers/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Removing buggy service workers - Chrome Developers</div><div class="kg-bookmark-description">How to fix a service worker that is causing problems.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://developer.chrome.com/images/meta/apple-touch-icon.png" alt="How to Delete a Service Worker"><span class="kg-bookmark-author">Chrome Developers</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://wd.imgix.net/image/QMjXarRXcMarxQddwrEdPvHVM242/ZUBXF0hK0jo9q4RvELUs.png?auto=format&amp;w=1521" alt="How to Delete a Service Worker"></div></a></figure><p>Have fun playing with service workers! &#x1F917;</p>]]></content:encoded></item></channel></rss>