<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://www.jimmybonney.com/">
  <id>https://www.jimmybonney.com/</id>
  <title>Jimmy Bonney's Blog</title>
  <updated>2026-02-21T00:00:00Z</updated>
  <link rel="alternate" href="https://www.jimmybonney.com/" type="text/html"/>
  <link rel="self" href="https://www.jimmybonney.com/articles/feed.xml" type="application/atom+xml"/>
  <author>
    <name>Jimmy Bonney</name>
    <uri>https://www.jimmybonney.com</uri>
  </author>
  <entry>
    <id>tag:www.jimmybonney.com,2026-02-21:/articles/small_design_updates/</id>
    <title type="html">Small Design Updates</title>
    <published>2026-02-21T00:00:00Z</published>
    <updated>2026-02-21T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/small_design_updates/" type="text/html"/>
    <content type="html">&lt;p&gt;It has been a long time since anything was done on this blog and with the raise of AI it would have been a shame not to use this capability to do anything about this. I am not talking about generating editorial content here, there is plenty enough of those elsewhere on the web. I am talking about doing something about the look and feel which has never really been my forte. 
&lt;!--MORE--&gt;
So, with the help of &lt;a href="https://claude.ai"&gt;Claude&lt;/a&gt;, I’ve gone through the old design and made a few updates:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;replace mini.css by &lt;a href="https://picocss.com/"&gt;pico.css&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;move to a complete dark theme&lt;/li&gt;
  &lt;li&gt;simplify the layout so that articles are in focus&lt;/li&gt;
  &lt;li&gt;simplify the footer&lt;/li&gt;
  &lt;li&gt;simplify about page and provide recent experience highlights&lt;/li&gt;
  &lt;li&gt;a few accessibility fixes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am also trying to reduce the time it takes for me to actually publish content. Today, I need to write the content, find an image for the header, edit the image to the right size, etc. I am trying to automate some of those tasks so that I can purely focus on the content and what I have to say. Let’s see how this goes…&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20260221_new_design_overview.jpg" alt="New home page" title="New home page" /&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-09-07:/articles/scheduling_post_publication_static_website_using_bitbucket_netlify_gcp/</id>
    <title type="html">Scheduling Post Publication with a Static Website (Using Bitbucket, Netlify and GCP)</title>
    <published>2021-09-07T00:00:00Z</published>
    <updated>2021-09-07T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/scheduling_post_publication_static_website_using_bitbucket_netlify_gcp/" type="text/html"/>
    <content type="html">&lt;p&gt;As I mentioned in different articles and in the &lt;a href="/about"&gt;About&lt;/a&gt; page, this website is being built using a static site generator, namely &lt;a href="https://nanoc.app/"&gt;nanoc&lt;/a&gt;. Every time I push changes to the code repo – be it configuration changes, new or updated articles, etc –, a new build is triggered on Netlify and the new version of the site is deployed. This is all well and good, except that on some occasions, it would be nice to be able to schedule new articles to come up on the site on a specific day, without having to manually push the content only then. In the last few days, I spent some time to see how this could be done and this post will look into &lt;a href="https://bitbucket.org/jbonney/faas-netlify-build/src/master/"&gt;the solution&lt;/a&gt; that I put in place using Google Cloud Platform (GCP).&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;h2 id="update-the-logic-in-nanoc-when-building-the-website"&gt;Update the logic in nanoc when building the website&lt;/h2&gt;

&lt;p&gt;The first thing to do is to make sure that nanoc does not build articles that are scheduled to be published later. This way, we can commit the changes to git and push them to the repository, but the new article will be excluded from the build on Netlify.&lt;/p&gt;

&lt;p&gt;Initially, the &lt;code&gt;Rules&lt;/code&gt; file &lt;code&gt;preprocess&lt;/code&gt; block was containing the following rule:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;&lt;span class="c1"&gt;# Delete unpublished articles&lt;/span&gt;
&lt;span class="vi"&gt;@items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:publish&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This rule allowed me to exclude some articles that are still in draft from being processed and built into the site. In order to schedule articles, it is necessary to update the logic a bit so that we also look at the article &lt;code&gt;created_at&lt;/code&gt; attribute: if it is in the future, we do not include it in the build. The updated rules are as follow:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
3
4
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;&lt;span class="c1"&gt;# Delete unpublished articles&lt;/span&gt;
&lt;span class="vi"&gt;@items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_if&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:publish&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;

&lt;p&gt;Before diving into the details, and as with all projects, there are different ways of achieving the same things. I looked a bit around to see if other people faced the same issue and what their solutions were and there are actually quite a few options out there. Most of them are related to code being hosted on Github and using GitHub native tools though, so those were not necessarily an option for me since my own repository is hosted on Bitbucket. Bitbucket comes with &lt;a href="https://bitbucket.org/product/features/pipelines"&gt;Pipelines&lt;/a&gt; that allows automating CI/CD and maybe it could have been an option to look into but the truth is I wanted to get my hands dirty and try to create a Cloud Function in Ruby running on GCP. So here comes the overall architecture:&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_architecture_bitbucket_netlify_gcp.jpg" alt="Architecture" title="Architecture" /&gt;&lt;/p&gt;

&lt;p&gt;The basic idea is to use Cloud Scheduler to trigger a Cloud Function on a daily basis. This function will clone the code from the (private so far) Bitbucket repository, it will fetch the last date at which Netlify made a deployment of the site and it will assess if there are any articles that are due to be published. If this is the case, then the function will call a Build Hook from Netlify for the website so that a new build is triggered and the website gets updated. That’s it, pretty simple.&lt;/p&gt;

&lt;h2 id="set-up"&gt;Set up&lt;/h2&gt;

&lt;p&gt;Since we are using GCP, the first step was to create a new project and enable the necessary APIs (Cloud Functions API, Secret Manager API and Cloud Build API). At this time, a number of steps have been executed manually on the GCP console. I believe that most of them could have been done through scripting, using the glcoud SDK but this will be an exercise for later.&lt;/p&gt;

&lt;p&gt;Interacting with Bitbucket and Netlify means that there are a few secret variables that the function will need to connect to them. For this purpose, we use Google Secret Manager. Once again, the variables here were created from the GCP Console but this could have been done using the SDK or code. This is outside of the scope of this quick project.&lt;/p&gt;

&lt;h3 id="create-an-app-password-in-bitbucket"&gt;Create an App password in Bitbucket&lt;/h3&gt;

&lt;p&gt;This can be done from the &lt;a href="https://bitbucket.org/account/settings/app-passwords/"&gt;global settings&lt;/a&gt; and is one option to conveniently have read access to the repository that we are interested in. Note that this is a global setting, meaning that if we give it read access to repositories, this grants read access to all repositories. There are probably options that are repo specific (using SSH keys for instance) but for convenience and ease of setup I went with the app password.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_bitbucket_app_password.png" alt="Bitbucket App password" title="Bitbucket App password" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_bitbucket_add_app_password.png" alt="Bitbucket Add App password" title="Bitbucket Add App password" /&gt;&lt;/p&gt;

&lt;h3 id="create-an-access-token-in-netlify"&gt;Create an access token in Netlify&lt;/h3&gt;

&lt;p&gt;It is also possible to create a personal access token to connect to Netlify. This is all explained in their &lt;a href="https://docs.netlify.com/api/get-started/#make-a-request"&gt;doc&lt;/a&gt; and can be done from the global settings &amp;gt; Applications.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_netlify_access_token.png" alt="Netlify access token" title="Netlify access token" /&gt;&lt;/p&gt;

&lt;h3 id="fetch-the-site-id-in-netlify"&gt;Fetch the site ID in Netlify&lt;/h3&gt;

&lt;p&gt;In order to get the list of deployments for a given site, we need the site ID from Netlify. This is accessible either through the API or from the UI, in the Site settings section.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_netlify_site_id.png" alt="Netlify site ID from the UI" title="Netlify site ID from the UI" /&gt;&lt;/p&gt;

&lt;h3 id="generate-a-build-hook-in-netlify"&gt;Generate a build hook in Netlify&lt;/h3&gt;

&lt;p&gt;Finally, in order to generate a new build if needed, there are different possible approaches: one is of course to use the API (in the same way we fetch the deployment date) and another is to use a webhook. In this specific case, I went with the webhook since the netlify gem available does not include the build api calls.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_netlify_build_hook.png" alt="Netlify build hook information" title="Netlify build hook information" /&gt;&lt;/p&gt;

&lt;h3 id="save-secret-in-secret-manager"&gt;Save secret in Secret Manager&lt;/h3&gt;

&lt;p&gt;Once all this data is gathered, it can be safely stored away in GCP Secret Manager.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_GCP_Secret_manager.png" alt="GCP Secret Manager" title="GCP Secret Manager" /&gt;&lt;/p&gt;

&lt;p&gt;Do make sure to grant the service account that will run the cloud function the rights to access secrets in Secret Manager. If using the default App Engine service account (not recommended for production as per Google’s guidelines), this is how it is done.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_gcp_iam_secret_accessor.png" alt="GCP IAM - Secret Manager Accessor role" title="GCP IAM - Secret Manager Accessor role" /&gt;&lt;/p&gt;

&lt;p&gt;You can read more about it on &lt;a href="https://cloud.google.com/functions/docs/configuring/secrets#gcloud"&gt;Google documentation&lt;/a&gt; and forgetting to do this is a pretty common issue that generated a number of articles (for instance &lt;a href="https://newbedev.com/secret-manager-access-denied-despite-correct-roles-for-service-account"&gt;here&lt;/a&gt;) or question on StackOverflow (for instance &lt;a href="https://stackoverflow.com/questions/63000973/cant-access-secret-stored-in-secrets-manager-from-google-cloud-function"&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;h2 id="cloud-function"&gt;Cloud Function&lt;/h2&gt;

&lt;p&gt;Once all this is in place, we can go ahead and code the logic. To keep things together, I created a simple class &lt;code&gt;Reviewer&lt;/code&gt; that has the necessary logic to connect to those services and identify if a new build needs to be triggered. This class is in turn called by the Cloud Function. To consult the whole code, head to the &lt;a href="https://bitbucket.org/jbonney/faas-netlify-build/src/master/"&gt;public repo&lt;/a&gt;.&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'netlify'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'git'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'front_matter_parser'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'faraday'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'fileutils'&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FaasNetlifyBuild&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Reviewer&lt;/span&gt;

    &lt;span class="c1"&gt;# Constructor&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="c1"&gt;# @params args [Hash] the secrets from Bitbucket and Netlify (:bitbucket_token, :netlify_token, :netlify_site_id, :netlify_build_hook)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@bitbucket_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:bitbucket_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="vi"&gt;@netlify_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:netlify_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="vi"&gt;@netlify_site_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:netlify_site_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="vi"&gt;@netlify_build_hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:netlify_build_hook&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="vi"&gt;@tmp_location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:config&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"TMP_FOLDER"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Logic to be executed to validate if a new build is required&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
      &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"No new build was needed..."&lt;/span&gt;
      &lt;span class="n"&gt;checkout_code&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_build_needed?&lt;/span&gt;
        &lt;span class="n"&gt;trigger_build&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"New build was triggered"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="n"&gt;output&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Clone the git repository in a local folder&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;checkout_code&lt;/span&gt;
      &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Cloning repository..."&lt;/span&gt;
      &lt;span class="c1"&gt;# Delete the directory if it already exists&lt;/span&gt;
      &lt;span class="no"&gt;FileUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rm_rf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@tmp_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# Clone the repo in the tmp folder&lt;/span&gt;
      &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Git&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://jbonney:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@bitbucket_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@bitbucket.org/jbonney/jimmybonney.com.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@tmp_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if a new build is needed. A new build is needed if:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. There is at least one article that has a publication date set to today or in the past and&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. The last deployment date from Netlify is older than the article publication date&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="c1"&gt;# @return [Boolean] true if a new build is needed, false otherwise&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_build_needed?&lt;/span&gt;
      &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Validating if a new build is needed..."&lt;/span&gt;
      &lt;span class="n"&gt;last_deployment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_deployment_date&lt;/span&gt;
      &lt;span class="n"&gt;current_year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;year&lt;/span&gt;
      &lt;span class="n"&gt;new_build_needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="c1"&gt;# Initiate the loader for front_matter_parser to allow dates&lt;/span&gt;
      &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FrontMatterParser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;allowlist_classes: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="c1"&gt;# Articles are sorted per year, in the folder 'content/articles/[YEAR]'&lt;/span&gt;
      &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@tmp_location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/content/articles/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;current_year&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/*.md"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FrontMatterParser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;loader: &lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Analyzing article &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="c1"&gt;# If article explicitly set the publish flag to false, then don't do anything, move on to the next file&lt;/span&gt;
        &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
        &lt;span class="c1"&gt;# New build is needed if:&lt;/span&gt;
        &lt;span class="c1"&gt;# 1. the article created_at date is in the past AND&lt;/span&gt;
        &lt;span class="c1"&gt;# 2. the last deployment took place before the article created_at date&lt;/span&gt;
        &lt;span class="n"&gt;new_build_needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;last_deployment&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"New build needed: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;new_build_needed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="n"&gt;new_build_needed&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Trigger the webhook from Netlify to start a new build&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trigger_build&lt;/span&gt;
      &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Triggering the build..."&lt;/span&gt;
      &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Faraday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@netlify_build_hook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'trigger_title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Triggered automatically from Ruby script using webhook"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="c1"&gt;# TODO: handle errors in the response&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Fetch the last date at which a deployment took place&lt;/span&gt;
    &lt;span class="c1"&gt;#&lt;/span&gt;
    &lt;span class="c1"&gt;# @return [Date] last deployment date&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;last_deployment_date&lt;/span&gt;
      &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Checking last deployment date..."&lt;/span&gt;
      &lt;span class="c1"&gt;# Connect to Netlify&lt;/span&gt;
      &lt;span class="n"&gt;netlify_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Netlify&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:access_token&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="vi"&gt;@netlify_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# Get the correct site (i.e. jimmybonney.com)&lt;/span&gt;
      &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;netlify_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sites&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@netlify_site_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# The Netlify API sort deploys from most recent to oldest =&amp;gt; first element is the most recent deployment&lt;/span&gt;
      &lt;span class="n"&gt;last_deploy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
      &lt;span class="n"&gt;last_deploy_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_deploy_timestamp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_date&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"functions_framework"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"google/cloud/secret_manager"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"yaml"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./reviewer"&lt;/span&gt;

&lt;span class="c1"&gt;# Use Google Cloud Secret Manager client to decode a secret&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# @param client [Google::Cloud::SecretManager] the Google Cloud Secret Manager client&lt;/span&gt;
&lt;span class="c1"&gt;# @param secret_name [String] full path of secret to decode]&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;access_secret_version&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;secret_name&lt;/span&gt;
  &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Fetch the secret, either from the file system if the secret is mounted as a file,&lt;/span&gt;
&lt;span class="c1"&gt;# or through a secret manager client otherwise&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# @param secret_file [String] the path to the secret when it is mounted&lt;/span&gt;
&lt;span class="c1"&gt;# @param secret_manager_client [Google::Cloud::SecretManager] secret manager client&lt;/span&gt;
&lt;span class="c1"&gt;# @param secret_arn [String] the full secret identifier, including project number, name and version&lt;/span&gt;
&lt;span class="c1"&gt;# @return [String] the secret&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_manager_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;extract_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_manager_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret_arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Load configuration file .env.yaml&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;YAML&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;load_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.env.yml'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Entry point for the Cloud Function&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Read the relevant secrets and then initiate and run a Reviewer instance&lt;/span&gt;
&lt;span class="no"&gt;FunctionsFramework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;http&lt;/span&gt; &lt;span class="s2"&gt;"build_scheduler"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;

  &lt;span class="c1"&gt;# In the local environment, the json key should be present to allow us to read secrets from Secret Manager&lt;/span&gt;
  &lt;span class="c1"&gt;# This file is not commited (part of .gitignore, and therefore not sent to GCP either)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.credentials/secret_manager_secret_accessor.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Read the key only if it is not set from the environment already.&lt;/span&gt;
    &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GOOGLE_APPLICATION_CREDENTIALS"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;".credentials/secret_manager_secret_accessor.json"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# Initiate the Secret Manager client, either by using the credentials defined above when this is&lt;/span&gt;
  &lt;span class="c1"&gt;# run locally, or the proper credentials will be looked up internally by GCP when this codes run on GCP&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Google&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cloud&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SecretManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;secret_manager_service&lt;/span&gt;

  &lt;span class="c1"&gt;# Project ID is either read from the environment variable (usually when running inside GCP),&lt;/span&gt;
  &lt;span class="c1"&gt;# or through the config file&lt;/span&gt;
  &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GCP_PROJECT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Fetch the different secrets needed&lt;/span&gt;
  &lt;span class="n"&gt;bitbucket_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/secrets/BITBUCKET_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"projects/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/secrets/BITBUCKET_TOKEN/versions/latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;netlify_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/secrets/NETLIFY_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"projects/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/secrets/NETLIFY_TOKEN/versions/latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;netlify_site_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/secrets/NETLIFY_SITE_ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"projects/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/secrets/NETLIFY_SITE_ID/versions/latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;netlify_build_hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/secrets/NETLIFY_BUILD_HOOK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"projects/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/secrets/NETLIFY_BUILD_HOOK/versions/latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;reviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FaasNetlifyBuild&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;bitbucket_token: &lt;/span&gt;&lt;span class="n"&gt;bitbucket_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;netlify_token: &lt;/span&gt;&lt;span class="n"&gt;netlify_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;netlify_site_id: &lt;/span&gt;&lt;span class="n"&gt;netlify_site_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;netlify_build_hook: &lt;/span&gt;&lt;span class="n"&gt;netlify_build_hook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;config: &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There are a couple of aspects to consider here:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;GCP is making some changes to make it easier to access secrets from within a Cloud Function. However, those changes are in beta currently and therefore I didn’t get it to work 100% as I would like. In the code above, I have included some logic to either read from a secret that is mounted as file or accessed through a Secret Manager client. The client itself is either instantiated using a json key belonging to a service account having the secret accessor role (when this is done locally) or internally within GCP (GCP then discovers the credentials behind the scenes).&lt;/li&gt;
  &lt;li&gt;There are also some other variables that have been added to a config file, such as GCP Project number. Since I am not sure how secret the project number is, the repo contains an example file to be copied locally instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id="test-and-deploy"&gt;Test and Deploy&lt;/h2&gt;

&lt;h3 id="test-cloud-function-locally"&gt;Test Cloud Function locally&lt;/h3&gt;

&lt;p&gt;GCP Cloud Functions comes with a framework allowing to test locally without having to deploy to GCP.&lt;/p&gt;

&lt;div class="language-bash highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;functions-framework-ruby &lt;span class="nt"&gt;--target&lt;/span&gt; build_scheduler
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Simply open a web browser and connect to localhost:8080 by default or from the console &lt;code&gt;curl localhost:8080&lt;/code&gt;. If all goes well, then the output should be “No new build was needed…” or “New build was triggered”.&lt;/p&gt;

&lt;p&gt;Do note that the script does need internet to run, even for local testing, since it gets the different credentials from Secret Manager, clones the git repo from Bitbucket and fetches deployment data from Netlify.&lt;/p&gt;

&lt;h3 id="deploy-to-gcp"&gt;Deploy to GCP&lt;/h3&gt;

&lt;p&gt;Once all is well, this can be deployed to GCP&lt;/p&gt;

&lt;div class="language-bash highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;gcloud functions deploy build_scheduler &lt;span class="nt"&gt;--runtime&lt;/span&gt; ruby27 &lt;span class="nt"&gt;--trigger-http&lt;/span&gt; &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As mentioned above, GCP is adding support for secrets so that they can be mounted as files or environment variables and therefore not require a full Secret Manager client. I tried to deploy using the beta version below but didn’t get it to work properly. The command is working when I include a single secret, but not with multiples.&lt;/p&gt;

&lt;div class="language-bash highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;gcloud beta functions deploy build_scheduler &lt;span class="nt"&gt;--runtime&lt;/span&gt; ruby27 &lt;span class="nt"&gt;--trigger-http&lt;/span&gt; &lt;span class="nt"&gt;--set-secrets&lt;/span&gt; /secrets/BITBUCKET_TOKEN&lt;span class="o"&gt;=&lt;/span&gt;BITBUCKET_TOKEN:latest,/secrets/NETLIFY_TOKEN&lt;span class="o"&gt;=&lt;/span&gt;NETLIFY_TOKEN:latest,/secrets/NETLIFY_SITE_ID&lt;span class="o"&gt;=&lt;/span&gt;NETLIFY_SITE_ID:latest,/secrets/NETLIFY_BUILD_HOOK&lt;span class="o"&gt;=&lt;/span&gt;NETLIFY_BUILD_HOOK:latest
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="set-up-the-scheduler"&gt;Set up the scheduler&lt;/h2&gt;

&lt;p&gt;GCP offers Cloud Scheduler that allows to simply set up cron jobs. In this specific case, we can use the scheduler to trigger a http request to the function that we have just deployed.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210905_cloud_scheduler.png" alt="GCP Cloud Scheduler" title="GCP Cloud Scheduler" /&gt;&lt;/p&gt;

&lt;p&gt;Note that when we deployed the function in the previous section, we set the flag &lt;code&gt;--allow_unauthenticated&lt;/code&gt; which means that the function can be triggered by anyone having the link. We did this to validate that everything works as expected once the function is deployed. This is however probably not needed long term since the scheduler is invoking the function from within GCP: this should allow us to include some authentication easily and restrict access to who can trigger the function.&lt;/p&gt;

&lt;h2 id="next-steps"&gt;Next steps&lt;/h2&gt;

&lt;p&gt;Here are some things that come to mind in terms of improvements:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Auto-deploy the function when the code is pushed into the repository, inspired by &lt;a href="https://blog.jakoblind.no/aws-lambda-github-actions/"&gt;this article&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Authenticate Cloud Scheduler so that we can remove the public access to the function (see resources below)&lt;/li&gt;
  &lt;li&gt;Make this solution more generic so that it can be used by others with minimal changes&lt;/li&gt;
  &lt;li&gt;Reflect about how to interact with secrets&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;As we have seen, adding the possibility to schedule posts / articles is actually quite easy to put in place. I am conscious that my set-up is probably not the most common: nanoc is not the most used static-site generator, Bitbucket is not the most used git repository, and GCP doesn’t have the same footprint as AWS. All this is probably the reason why I had to deploy my own solution in the first place but hopefully it can help or inspire others having their own exotic set-up. One aspect that I didn’t mention is that this overall setup is probably going to run on the free tier offered by Google, meaning that we are adding this functionality for free. This is a nice bonus.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-08-22:/articles/everyday-carry/</id>
    <title type="html">Everyday Carry (EDC)</title>
    <published>2021-08-22T00:00:00Z</published>
    <updated>2021-08-22T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/everyday-carry/" type="text/html"/>
    <content type="html">&lt;p&gt;While I have always been interested in office material – I remember going through catalogues of pens and paper that my parents brought back home when I was a child / teenager – my interest in organization and “being ready quickly” just went off the charts in the last few years. As usual, turning to the Internet to look into my disease, I learned that there was a name for what I was interested in: Everyday Carry, or EDC in short.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;h2 id="overview"&gt;Overview&lt;/h2&gt;

&lt;p&gt;Looking in more details into this, everyday-carry includes quite a lot, actually a lot more than I will probably ever be interested in. When browsing through websites in the USA, some include guns, knives and different kinds of tactical gear. I am a bit more focused on office EDC, meaning things that I would / might need in an office environment (or on the way to the office). Here are some examples to give you an idea:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Bags&lt;/li&gt;
  &lt;li&gt;Organizers&lt;/li&gt;
  &lt;li&gt;Notebooks&lt;/li&gt;
  &lt;li&gt;Wallets&lt;/li&gt;
  &lt;li&gt;Key rings&lt;/li&gt;
  &lt;li&gt;Pens&lt;/li&gt;
  &lt;li&gt;Watches&lt;/li&gt;
  &lt;li&gt;Flashlights&lt;/li&gt;
  &lt;li&gt;Multi tools&lt;/li&gt;
  &lt;li&gt;Glasses&lt;/li&gt;
  &lt;li&gt;Electronics
    &lt;ul&gt;
      &lt;li&gt;Phone&lt;/li&gt;
      &lt;li&gt;Computer&lt;/li&gt;
      &lt;li&gt;Charger(s)&lt;/li&gt;
      &lt;li&gt;Cables&lt;/li&gt;
      &lt;li&gt;Battery pack&lt;/li&gt;
      &lt;li&gt;Headset&lt;/li&gt;
      &lt;li&gt;USB key&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of my specific needs evolve around electronics and how to carry and organize all this in the best way. Let me go through a few items!&lt;/p&gt;

&lt;h2 id="bag--backpack--rucksack"&gt;Bag / Backpack / Rucksack&lt;/h2&gt;

&lt;p&gt;First things first, when carrying a large number of items, everything would need to get into a bag. Choices are numerous and I have had quite a few bags across the years. From traditional top-loading backpack, to messenger bag, to portfolio, to “tech” backpack and finally recently clam-shell opening backpack. A couple of years back, I got a bag from Oakley (Motion Tech 2.0 Backpack) that I really enjoyed: slim design, dedicated sleeve for the laptop and tablet / notebook, a few pockets for daily essentials. The main problems that I had with it was that&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;in occasions, it felt a bit too small as it was hard to stash in rain clothes or my over-the-ear headset;&lt;/li&gt;
  &lt;li&gt;the top handle of the bag quickly started to fall apart (less than a year after I bought the bag)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reaching to Oakley to get it repaired only resulted in a discount to buy another bag as the one I had was not produced any longer and they didn’t have a “repair” program. This experience led me to look after goods that were having a better warranty program, if possible a “buy for life” type of warranty, meaning that if something happens I know that I can go back to the manufacturer to get it fixed. As one can guess, there are not too many companies out there that are offering this kind of warranty, but hopefully more and more will do so as consumers start to realize the ecological impact of simply throwing away stuff as soon as something breaks. See the external links section below to get some inspiration.&lt;/p&gt;

&lt;p&gt;With all this considered and after spending way too much time looking for the &lt;em&gt;perfect bag&lt;/em&gt;™, I ended up ordering a slick version of GORUCK GR1 from &lt;a href="https://huckberry.com/store/goruck/category/p/62216-gr1-1000d-slick-21l"&gt;Huckberry&lt;/a&gt; as I was not too much into the tactical look of the &lt;a href="https://www.goruck.com/products/gr1"&gt;original version&lt;/a&gt; with the MOLLE webbing. It comes with a lifetime warranty that I hope I will never need considering the tough construction of the bag. At 21l, it also feels much larger than the Oakley so there is no problem to pack a rain jacket and pants or my headset. While I haven’t been in the office for a while due to covid-19, I used it during the holiday to carry around “my office” and this has been working well. Let’s see how it goes later when commuting again.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_gr1.JPG" alt="GR1 Slick" title="GR1 Slick" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_gr1_open_content_hidden.JPG" alt="GR1 Slick with content hidden" title="GR1 Slick - loaded" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_gr1_open_content_visible.JPG" alt="GR1 Slick with content visible" title="GR1 Slick - load details" /&gt;&lt;/p&gt;

&lt;h2 id="organizers"&gt;Organizers&lt;/h2&gt;

&lt;p&gt;Previously, when choosing a bag, I always looked at all the internal organization that it was offering. This might be the reason why I never managed to stick to any bag, because as soon as something that I am carrying around would change, then the bag would be less ideal. It would have either too much organization or not enough. Having everything spread out in the bag in the different internal pockets also meant that it was taking time to switch to another bag and not forget anything. This is how I learned that it would be much better to keep the internal organization of the bag separate from the bag layout. Ironically, this separation of concerns concept is a basic concept in software engineering (my daily job)… I guess it is sometimes hard to make the parallel between different domains :-).&lt;/p&gt;

&lt;p&gt;One of the main features of the GR1 is the minimal organization that it offers. This means that if you want things to stick together, you’d better have some organization for it. From packing cubes to tech / admin pouches or wash bags, there are plenty of options out there and pretty much for all budgets.&lt;/p&gt;

&lt;p&gt;I started with a &lt;a href="https://www.hp.com/us-en/shop/pdp/hp-spectre-folio-pouch#!"&gt;HP Spectre Folio Pouch&lt;/a&gt; that I grabbed on sale. It opens flat for easy access and comes with some webbing and a large mesh pocket inside. I have been pretty satisfied with it but have a bit too much nowadays so I also bought a couple of GORUCK &lt;a href="https://www.goruck.com/collections/accessories/products/wire-dopp"&gt;Wire Dopp&lt;/a&gt; and I have split the content in between:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mouse and adapters go in the Folio Pouch;&lt;/li&gt;
  &lt;li&gt;Cables and charger go into the Wire Dopp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_organizers_closed_flat.JPG" alt="HP Spectre Folio Pouch and GORUCK Wire Dopp" title="HP Spectre Folio Pouch and GORUCK Wire Dopp" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_organizers_open_content_hidden.JPG" alt="HP Spectre Folio Pouch and GORUCK Wire Dopp with content hidden" title="HP Spectre Folio Pouch and GORUCK Wire Dopp with content hidden" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_organizers_open_content_visible.JPG" alt="HP Spectre Folio Pouch and GORUCK Wire Dopp with content visible" title="HP Spectre Folio Pouch and GORUCK Wire Dopp with content visible" /&gt;&lt;/p&gt;

&lt;h2 id="notebooks"&gt;Notebooks&lt;/h2&gt;

&lt;p&gt;I used to buy a lot of paper notebooks, but a few years ago I decided to try out a &lt;a href="https://remarkable.com/"&gt;reMarkable&lt;/a&gt; paper tablet and since then, I do not go anywhere without it. I have the first version and I really enjoy it a lot. I know that they have made a second version, but this felt like an expensive upgrade for something that was still working perfectly so I am sticking with the one I have.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210822_remarkable.JPG" alt="Remarkable v1" title="Remarkable v1" /&gt;&lt;/p&gt;

&lt;p&gt;As a person, I usually like to have things in order and paper and notebooks usually become messy and out of order quickly. The reMarkable allows me to insert pages where I need them and easily sort things how I want. But of course, having this tablet means losing the benefit of pen and paper that does not require any charging. As I am writing this, I am considering buying back a few ruled cahier journals from Moleskine that I used to carry around earlier. They don’t take much space after all.&lt;/p&gt;

&lt;h2 id="wallet"&gt;Wallet&lt;/h2&gt;

&lt;p&gt;I’ll keep this section short. Sweden is pretty much a cash-free country nowadays so the main things that I need to carry around are plastic cards and the occasional receipts. France had some very peculiar format for their ID card up until the beginning of the month. They decided recently to use a credit card format (like many other countries are already doing) so this means that I might even be able to go one size smaller on my wallet / card holder… except that I can’t because I still have a driving license that is a piece of paper folded in three :-(. So I’ll stick to what I have for now.&lt;/p&gt;

&lt;h2 id="electronics"&gt;Electronics&lt;/h2&gt;

&lt;p&gt;This is the bulk of what I am actually carrying around, and for which I need all this organization. There is not much to say about this, but here is the list:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Work phone: I like to separate my private and working life, making it easier to disconnect when needed.&lt;/li&gt;
  &lt;li&gt;Work computer: Again, my private computer stays home and I carry around the work one. Recently got an upgrade to a Macbook Pro 16’’. I usually prefer a more compact format (13 or 14 inches) but like the additional real estate when working from home without an external screen.&lt;/li&gt;
  &lt;li&gt;Charger: I bought a &lt;a href="https://www.hypershop.com/products/hyperjuice-100w-usb-c-gan-charger"&gt;GaN 100W charger&lt;/a&gt; from Hyper on Kickstarter a while ago and this is really convenient to be able to charge up to 4 devices at once (laptop included).&lt;/li&gt;
  &lt;li&gt;Cables: different mini-USB and USB-C cables in different sizes for the devices mentioned above as well as a short network cable when one needs to connect directly to an outlet / router.&lt;/li&gt;
  &lt;li&gt;Adapters:
    &lt;ul&gt;
      &lt;li&gt;I have a number of USB-A devices (headset and mouse connectors, USB-keys) and the MBP only comes with USB-C ports, so here we are with small adapters allowing me to connect all those things.&lt;/li&gt;
      &lt;li&gt;USB-C to RJ45 Network adapter&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Headset: an over-the-ear headset from Jabra with Active Noise Cancellation – particularly helpful when working from home and your neighbor decides it is time to redo the kitchen.&lt;/li&gt;
  &lt;li&gt;USB keys: a few keys, mainly holding different Linux distro that I tried out or few larger files that need to be shared&lt;/li&gt;
  &lt;li&gt;Mouse: a Logitech MX Master 3&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id="misc"&gt;Misc&lt;/h2&gt;

&lt;p&gt;As I mentioned above, I usually pack some rain gear – both rain jacket and pants – inside a packing cube from IKEA. The jacket is a simple light-weight one, and the &lt;a href="https://tierra.com/product/back-hybrid-pant-gen-3-m/"&gt;pants&lt;/a&gt; have a side opening pretty much all the way up to the waist so that they are easy to take on while keeping my shoes on.&lt;/p&gt;

&lt;h2 id="last-word"&gt;Last Word&lt;/h2&gt;

&lt;p&gt;EDC encompasses a lot of gear and there is no right or wrong in there, only whatever works for you. The good thing is that it gets you thinking about what are the things that you need (or might need) so that you are ready for situations that life throws at you.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-08-17:/articles/forms_for_static_sites/</id>
    <title type="html">Forms for Static Sites</title>
    <published>2021-08-17T00:00:00Z</published>
    <updated>2021-08-17T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/forms_for_static_sites/" type="text/html"/>
    <content type="html">&lt;p&gt;I am getting more and more interested in static websites as I am following the developments in the &lt;a href="https://jamstack.org/"&gt;JAMSTACK&lt;/a&gt; space. I have covered in previous articles some of the benefits of having a static site, such as better performance, easier scaling, cheap hosting to name a few. In this article, I wanted to share a few thoughts based on my latest experimentation and implementation.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;p&gt;I recently helped a family friend to update his company website, a simple site with 5 pages, one of them being a contact page with a form. Since I have been using Netlify for a few years now and the previous version of his site was already hosted there, I decided to use &lt;a href="https://www.netlify.com/products/forms/"&gt;Netlify Forms&lt;/a&gt; to manage the contact form. Implementation is pretty straightforward as it only requires adding an attribute (&lt;code&gt;netlify&lt;/code&gt;) to the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; tag to get started. One can then play with some additional options as well to customize it a bit more, redirecting to a specific page when the form is successfully submitted or adding a honeypot to limit spam further. Notifications can be sent to different email addresses when a form is received and entries are saved in Netlify as well.&lt;/p&gt;

&lt;p&gt;One limitation that I found there, and that I was not really found of, was the fact that the notification email includes &lt;code&gt;[Netlify]&lt;/code&gt; in the subject of the email. This is even added when &lt;a href="https://docs.netlify.com/forms/notifications/"&gt;customizing the notification subject&lt;/a&gt;. This is not a deal breaker and there are options to bypass this, such as using &lt;a href="https://zapier.com/"&gt;zapier&lt;/a&gt; to customize notification further and avoid the confusion for the website owner receiving the email. But this experience got me curious and I started to search for “form as a service” options that were available out there.&lt;/p&gt;

&lt;p&gt;I was not disappointed! There are quite a few solutions available, here is the top of the iceberg (in no particular order) as I am writing this post:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://formspree.io"&gt;Formspree&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://www.99inbound.com"&gt;99 inbound&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://formkeep.com"&gt;FormKeep&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://www.staticforms.xyz/"&gt;Static Forms&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://www.freecodecamp.org/news/building-serverless-contact-form-for-static-websites/"&gt;Build your own&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://formforsite.com/"&gt;Form for site&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The features and pricing models are quite different across all those applications: free tier might or might not be available, base pricing is sometimes based on the number of forms, or number of submissions, or a combination of both. In short, I would need to look into more details based on my specific need to see what would be most appropriate and for the specific use case that I was relating above, this is not necessary at this time. But I am quite sure that this will be relevant in a near future so I am keeping this list handy as a starting point.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-08-05:/articles/favicon_converter_generator/</id>
    <title type="html">Favicon Converter and Generator</title>
    <published>2021-08-05T00:00:00Z</published>
    <updated>2021-08-05T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/favicon_converter_generator/" type="text/html"/>
    <content type="html">&lt;p&gt;I took some time recently to help out and redesign a website using &lt;a href="https://middlemanapp.com/"&gt;Middleman&lt;/a&gt;. Considering that this was a simple website with only a few pages, it was a good opportunity to use a tool that I never used before. Once the layout was defined and the content was imported, it was time to create a favicon. I do not have much experience with that so I was browsing / googling around to see how to proceed when I stumbled upon &lt;a href="https://favicon.io/"&gt;favicon.io&lt;/a&gt;.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;p&gt;Favicon.io has been developed by &lt;a href="https://twitter.com/johnsorrentino"&gt;John Sorrentino&lt;/a&gt; and is really easy to use. It provides a clean interface to either convert an existing image to a favicon or to generate one from your text or emoji. You then just need to download the resulting assets and update the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; section to include them properly.&lt;/p&gt;

&lt;p&gt;It was so easy to use that I decided that it was time to move away from the generic favicon that my own website was using. As you can see now in your browser, there is a simple one in place using my initials.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/android-chrome-192x192.png" alt="Favicon JB" title="Favicon JB" /&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-08-03:/articles/load_disqus_comments_on_click/</id>
    <title type="html">Load Disqus Comments on Click</title>
    <published>2021-08-03T00:00:00Z</published>
    <updated>2021-08-03T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/load_disqus_comments_on_click/" type="text/html"/>
    <content type="html">&lt;p&gt;Comments on this website are managed by &lt;a href="https://disqus.com/"&gt;Disqus&lt;/a&gt;. I installed and configured this a long time ago when initially starting the transition to having a static website but still wanting to offer readers the possibility to comments on the articles I published.&lt;/p&gt;

&lt;p&gt;While this solution might be convenient for me as a site owner (one click integration with a free tier available), it does come with a number of drawbacks, whether around &lt;a href="https://techcrunch.com/2021/05/05/disqus-facing-3m-fine-in-norway-for-tracking-users-without-consent/"&gt;privacy concerns&lt;/a&gt; or &lt;a href="https://victorzhou.com/blog/replacing-disqus/"&gt;page load times&lt;/a&gt;. I have the ambition of removing it altogether in the (hopefully near) future but before I do so, I need to find a way to import the existing comments to whatever new system I put in place. Some articles include interesting and valuable input or links in the comments, so this would be a shame to lose them altogether.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;p&gt;I have started to look into the alternatives, and there are plenty of them. Until I find a solution that works well for this simple blog, one thing that I can do to help readers that are not interested in being tracked and / or experience longer load time, is at least make loading comments an explicit activity. Starting from today, Disqus comments will therefore not be loaded automatically on the article pages. Instead, I followed &lt;a href="https://w3bits.com/load-disqus-on-click/"&gt;Rahul Arora&lt;/a&gt; and require the reader to click on a button to actually load the comments. On top of this, I added some background information linking back to this article if readers would like to understand more.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210803_comments_disclaimer.png" alt="Load comments - Disclaimer" title="Load comments disclaimer" /&gt;&lt;/p&gt;

&lt;div class="language-html highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
3
4
5
6
7
8
9
10
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"disqus_thread"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"disqus-disclaimer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
      For the time being, comments are managed by Disqus, a third-party library. I will eventually replace it with another solution, but the timeline is unclear. Considering the amount of data being loaded, if you would like to view comments or post a comment, click on the button below. For more information about why you see this button, take a look at the following &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/articles/load_disqus_comments_on_click/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;article&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"disqus-trigger"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"load_disqus('jimmybonney')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Load Comments
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-javascript highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;table class="rouge-table"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="rouge-gutter gl"&gt;&lt;pre class="lineno"&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
&lt;/pre&gt;&lt;/td&gt;&lt;td class="rouge-code"&gt;&lt;pre&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load_disqus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;disqus_shortname&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Prepare the trigger and target&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;disqus_trigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disqus-trigger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;disqus_target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disqus_thread&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;disqus_disclaimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disqus-disclaimer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;disqus_embed&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;disqus_hook&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;head&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// Load script asynchronously only when the trigger and target exist&lt;/span&gt;
  &lt;span class="nf"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;disqus_target&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;disqus_trigger&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;disqus_embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/javascript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;disqus_embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;disqus_embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;//&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;disqus_shortname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.disqus.com/embed.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;disqus_hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disqus_embed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;disqus_disclaimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Disqus loaded.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="a-word-about-performance"&gt;A Word About Performance&lt;/h2&gt;

&lt;p&gt;As mentioned above, Disqus is known for impacting the load time of a page, adding between 500kB and 1MB of elements to the page. To see what the impact of the change mentioned above would be, I ran a test on &lt;a href="https://developers.google.com/speed/pagespeed/insights/"&gt;PageSpeed Insights&lt;/a&gt; before and after introducing this button. Here are the results:&lt;/p&gt;

&lt;h3 id="before"&gt;Before&lt;/h3&gt;

&lt;p&gt;When Disqus is loaded automatically, the total score of my &lt;a href="https://www.jimmybonney.com/articles/another_start/"&gt;previous article page&lt;/a&gt; was 76. It might not be as bad as it looks considering that the first contentful paint was only taking 0.8s but still, a lot of time was spent loading those JS scripts and assets from Disqus making the page not fully interactive before 8.7s.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210803_psi_score_before.png" alt="PageSpeed Insights with Disqus loading automatically" title="PageSpeed Insights with Disqus" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210803_psi_opportunities_before.png" alt="PageSpeed Insights - Opportunities before removing Disqus" title="PageSpeed Insights Opportunities with Disqus" /&gt;&lt;/p&gt;

&lt;h3 id="after"&gt;After&lt;/h3&gt;

&lt;p&gt;When Disqus is not loaded automatically anymore, the total score jumped to 99 and all metrics are now green. Most of the diagnostics and opportunities have now a medium or low severity.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210803_psi_score_after.png" alt="PageSpeed Insights when Disqus is not loaded" title="PageSpeed Insights without Disqus" /&gt;&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20210803_psi_opportunities_after.png" alt="PageSpeed Insights - Opportunities when Disqus is not loaded" title="PageSpeed Insights Opportunities without Disqus" /&gt;&lt;/p&gt;

&lt;p&gt;This of course is just the result of one run of test (so not fully scientific :-)) but the page size is divided by nearly 3 and time to be fully loaded is divided by at least as much. Of course, if you are still interesting in going through the comments or leaving a comment, you’ll need to load of those assets, but this is at least a conscious decision and will not impact your experience until you decide to do so.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-08-01:/articles/another_start/</id>
    <title type="html">Another Start</title>
    <published>2021-08-01T00:00:00Z</published>
    <updated>2021-08-01T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/another_start/" type="text/html"/>
    <content type="html">&lt;p&gt;As I wrote a few months back already, I haven’t been very active around here for a while. With the exception of the article earlier this year, it has been a pause of around 3 years in my writing and publishing. Obviously a lot happened during the last 3 years – both on the personal and professional fronts –, and this is probably the reason why my priorities got shifted a bit but I would like to give this blog a new start and see if I can publish at least once a month in the coming year. Ambitious enough for someone who hasn’t written for a while BUT not too ambitious so that it becomes overwhelming.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;p&gt;I have an old list of topics for which I wanted to write articles about but everything is probably out of date so this really feel like starting from scratch again. In the past years, I manage to read quite a few books on diverse subjects (sleep, devops practices, product management, leadership, entrepreneurship, personal finance, …) and moving forward, I think I’ll share some thoughts on those subjects. This means that the blog might get a bit less technical but let’s see how it turns out.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2021-04-07:/articles/removing_google_analytics/</id>
    <title type="html">Removing Google Analytics</title>
    <published>2021-04-07T00:00:00Z</published>
    <updated>2021-04-07T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/removing_google_analytics/" type="text/html"/>
    <content type="html">&lt;p&gt;It has been a long time since I posted something and it was time for some clean-up, so bye-bye Google Analytics. It was long pending on my to-do list and really quick to remove but life happened in between and it therefore took a long time to get done.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;p&gt;Google Analytics (GA) has long been a default analytics solutions to get information about the number of users who visited a website, how long they interacted, where they are coming from (both geographically and other website referral page), etc. It is a really complete solution and integrates of course really well with the Google ecosystem. The downside of it all is the hidden cost for the audience. While completely free for the website owner, having Google Analytics in place means that users are getting tracked across the web. GA uses cookies to track users and with GDPR in place in EU, this also means that the audience should be informed about such tracking. In addition, considering all the things that GA offers, for such a simple website as mine, this is actually overkill in most cases since the main thing I might be interested in is the page views (i.e. how popular is a given article). For such a limited use of their tool, users visiting the website still needs to load the GA script which is quite large compared to alternative solutions.&lt;/p&gt;

&lt;p&gt;At this point in time, I am not installing any other analytics solution. I have identified a couple of alternatives, &lt;a href="https://plausible.io/"&gt;Plausible&lt;/a&gt; and &lt;a href="https://usefathom.com"&gt;Fathom&lt;/a&gt;, that look attractive but I would need to dig more into this. I’ll communicate more about this if / when I put something in place.&lt;/p&gt;

&lt;p&gt;In the meantime, enjoy not being tracked when browsing this website :-).&lt;/p&gt;

</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2018-03-18:/articles/firefox_warning_your_connection_is_not_secure/</id>
    <title type="html">Firefox Warning "Your connection is not secure"</title>
    <published>2018-03-18T00:00:00Z</published>
    <updated>2018-03-18T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/firefox_warning_your_connection_is_not_secure/" type="text/html"/>
    <content type="html">&lt;p&gt;Firefox is my browser of choice currently, mostly due to its philosophy centered around a web accessible to all. I therefore use it as the default browser on all computer, and that includes my working computer. However, at work, I started encountering a rather peculiar issue where Chrome and Edge could access the web without any issue but Firefox would keep on throwing me warning on almost all pages being visited to tell me “ Your connection is not secure”. Solving the issue proved to be quite simple but comes with a risk.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;p&gt;I am not sure why the problem occurred all of a sudden but it might be related to a specific version of Firefox that started to enforce something that was not done previously (as suggested by &lt;a href="https://support.mozilla.org/en-US/questions/1175296#answer-1006282"&gt;elliotstarks&lt;/a&gt;) or maybe it is simply my employer who updated something on their end. Either way, a lot of pages started to display a message “Your connection is not secure” as illustrated by the screenshot below.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180318_firefox_connection_not_secure.png" alt="Firefox warning - Your connection is not secure" title="Firefox warning - Your connection is not secure" /&gt;&lt;/p&gt;

&lt;p&gt;Considering that Edge and Chrome did not have the same problem, it seemed to be related to the way Firefox handles its certificates. Indeed digging a little bit deeper, both Chrome and Edge were looking for certificates information in the same place. However, Firefox had its own certificates store and was not looking for the ones from my employer.  Fortunately, Mozilla (the organization between Firefox) is aware of this kind of issue and there is therefore a way to &lt;a href="https://wiki.mozilla.org/CA:AddRootToFirefox"&gt;look for certificates&lt;/a&gt; in the OS certificate store.&lt;/p&gt;

&lt;p&gt;This is done by editing Firefox settings:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;In the address bar, type: &lt;code&gt;about:config&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Read the warning on the screen and click on “I accept the risk”&lt;/li&gt;
  &lt;li&gt;Search for &lt;code&gt;security.enterprise_roots.enabled&lt;/code&gt; and set the value to true (by double clicking on it)&lt;/li&gt;
&lt;/ol&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180318_firefox_security_entreprise_roots_settings.png" alt="Firefox settings - enterprise root certificate" title="Firefox settings - enterprise root certificate" /&gt;&lt;/p&gt;

&lt;p&gt;You should not even need to restart your browser and Firefox is now able to access again the websites as before. There is however one drawback with this method: you are now allowing your web browser to use self-signed certificates of your employer, allowing the company to act like a man in the middle. At the same time, in most countries I believe that employers are allowed to monitor and inspect the traffic going in and out of your computer so as usual, while at work, make sure to do work related activities when navigating the Internet.&lt;/p&gt;

&lt;p&gt;Stay safe!&lt;/p&gt;

</content>
  </entry>
  <entry>
    <id>tag:www.jimmybonney.com,2018-03-11:/articles/website_performance_review/</id>
    <title type="html">Website Performance Review</title>
    <published>2018-03-11T00:00:00Z</published>
    <updated>2018-03-11T00:00:00Z</updated>
    <link rel="alternate" href="https://www.jimmybonney.com/articles/website_performance_review/" type="text/html"/>
    <content type="html">&lt;p&gt;As I mentioned in a &lt;a href="/articles/hosting_static_websites/"&gt;previous article&lt;/a&gt;, this website is purely static and has been so for a while now. However, the site structure and components have not been revisited for a number of years and while it was performing quite well (with a page size well under the current web page average size), a number of things were a bit overkill and unnecessary. As an example, the site was relying on a full fledge front-end component library containing both Javascript and CSS files which were barely used. It was time to perform some clean up activities to identify what was actually used or not to try and see how this could improve the overall website performance.&lt;/p&gt;

&lt;!--MORE--&gt;

&lt;h2 id="starting-point"&gt;1. Starting Point&lt;/h2&gt;

&lt;p&gt;The website is using an old version of &lt;a href="http://ink.sapo.pt/"&gt;INK&lt;/a&gt; having both Javascript and CSS components. Using &lt;a href="https://tools.pingdom.com/"&gt;Pingdom&lt;/a&gt;, it is quite easy to run an analysis on some pages of the site to get an overview of:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The load time of the page&lt;/li&gt;
  &lt;li&gt;The page size&lt;/li&gt;
  &lt;li&gt;The number of requests&lt;/li&gt;
  &lt;li&gt;Splits of requests by content type and size&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s have a look at were we stand on the start page. The page size is only 310kB which is already way under the &lt;a href="https://www.keycdn.com/support/the-growth-of-web-page-size/"&gt;average page size&lt;/a&gt; of the last few years and bulk of the weight is coming from Javascript (41%), fonts (37.3%) and images (15.8%).&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180311_performance_start_root.png" alt="Pingdom performance analysis - start page before performance optimization" title="Pingdom performance analysis - start page before performance optimization" /&gt;&lt;/p&gt;

&lt;p&gt;When looking at an article page in particular, we can see that the page size is more that doubled (753.9kB) compared to the start page and the number of requests is doubled as well. One interesting aspect which I believe is due to caching on Pingdom servers is that the load time was actually lower this time. Bulk of the content is still coming from Javascript and fonts but this time, CSS represents a bigger share than images.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180311_performance_start_article.png" alt="Pingdom performance analysis - article page before performance optimization" title="Pingdom performance analysis - article page before performance optimization" /&gt;&lt;/p&gt;

&lt;p&gt;Following this analysis on two pages, there are already a couple of takeaways:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Reduce the Javascript footprint&lt;/li&gt;
  &lt;li&gt;Use standard fonts rather than exotic ones&lt;/li&gt;
  &lt;li&gt;Reduce the CSS footprint&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google Chrome now also comes with an performance auditing tool that can provide some inputs as to what is most critical to address to improve the performance of the site.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180311_chrome_audit_start.jpg" alt="Chrome performance audit - start page before performance optimization" title="Chrome performance audit - start page before performance optimization" /&gt;&lt;/p&gt;

&lt;h2 id="performance-improvements"&gt;2. Performance Improvements&lt;/h2&gt;

&lt;p&gt;Based on the performance analysis performed above, and as I mentioned in the introduction of this article, it was clear that a review of the front-end component library was due. In my case, I took a rather radical approach to completely remove the framework (INK) in place and decided to go with a small footprint CSS framework instead. The choice of the framework will be discussed in a later article, but for information I went with &lt;a href="https://minicss.org/index"&gt;mini.CSS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Replacing the framework obviously required some rework on the website but the benefit of having a generated static website is that pages are based on templates and those templates are the only files to be updated. The articles themselves being written in markdown are not impacted by this. In this case as well, I took the liberty to do some cleanup and remove a bunch of unused Javascript files.&lt;/p&gt;

&lt;p&gt;The results of these changes are speaking for themselves. As you can see below, the start page has a size that has been divided by 3, weighting less than 100kB currently. The number of requests has been reduced to 12 (from 20) and the biggest content by size is now images (which is more acceptable than having scripts in my case).&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180311_performance_end_root.png" alt="Pingdom performance analysis - start page after performance optimization" title="Pingdom performance analysis - start page after performance optimization" /&gt;&lt;/p&gt;

&lt;p&gt;The article page also benefits from massive improvements. The page size is being reduced from 753.9kB to 541.1kB, and the number of requests is reduced to 32. What is worrying from this analysis is actually the part that the comment management system (Disqus) represents. After the page is loaded, it is nearly 400kB of data (almost 75% of the overall page). Looks like something might be needed there if we want to improve even further.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180311_performance_end_article.png" alt="Pingdom performance analysis - article page after performance optimization" title="Pingdom performance analysis - article page after performance optimization" /&gt;&lt;/p&gt;

&lt;p&gt;The performance audit from Chrome also shows some nice figures. The most important things to fix now seems to be related to images and non-blocking rendering of CSS.&lt;/p&gt;

&lt;p class="center"&gt;&lt;img src="/img/articles/20180311_chrome_audit_end.jpg" alt="Chrome performance audit - start page after performance optimization" title="Chrome performance audit - start page after performance optimization" /&gt;&lt;/p&gt;

&lt;h2 id="conclusion-and-next-steps"&gt;3. Conclusion and Next Steps&lt;/h2&gt;

&lt;p&gt;Conducting a performance analysis on a website can be done at multiple levels. In this article, we focus on the page size and number of requests which ultimately has an impact on the load time. A number of tools are available to help out in conducting such analysis and while we chose to illustrate our case with &lt;a href="https://tools.pingdom.com/"&gt;Pingdom&lt;/a&gt; it might be interesting to cite a couple of other alternatives such as &lt;a href="https://gtmetrix.com"&gt;GTMetrix&lt;/a&gt;, &lt;a href="https://www.webpagetest.org"&gt;WebPageTest&lt;/a&gt;, &lt;a href="https://developers.google.com/speed/pagespeed/insights"&gt;PageSpeed&lt;/a&gt; or &lt;a href="https://www.dareboost.com"&gt;dareboost&lt;/a&gt;. Some of the recommendations coming from those tools are of course similar but it is nonetheless interesting to get the reports from all of them because they also offer different focus areas.&lt;/p&gt;

&lt;p&gt;As far as this site is concerned, the focus on the CSS framework allowed to drastically reduce the page size, number of requests and load time of the page. There are however still a number of things to accomplish if we would like to improve even more the performances of the site. The next activity will most likely be around grouping together small icons into a sprite so that the number of requests to the server can be reduced. As I highlighted above, I am not particularly impressed either by the footprint of Disqus which means that I will most likely look into another commenting system when time allows. Finally, one of the recommendations from Chrome was to use WebP images and there is some room for improvement as well in the overall CSS file that is being generated. It currently relies on the complete mini.CSS framework but a large number of classes and elements are actually not used on the site and could therefore be filtered out. Focusing on performance is a never ending story but we are now in a much better place than we have been for years.&lt;/p&gt;
</content>
  </entry>
</feed>
