<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike Street</title>
    <description>The latest articles on DEV Community by Mike Street (@mikestreety).</description>
    <link>https://dev.to/mikestreety</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F29250%2Fd5a19082-4c49-40bd-96fc-d352dea4dd34.jpg</url>
      <title>DEV Community: Mike Street</title>
      <link>https://dev.to/mikestreety</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikestreety"/>
    <language>en</language>
    <item>
      <title>Running Playwright in Gitlab CI</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Fri, 09 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/running-playwright-in-gitlab-ci-1mg3</link>
      <guid>https://dev.to/mikestreety/running-playwright-in-gitlab-ci-1mg3</guid>
      <description>&lt;p&gt;After setting up my &lt;a href="https://www.mikestreety.co.uk/blog/use-playwright-to-smoke-test-your-deployments/" rel="noopener noreferrer"&gt;smoke tests&lt;/a&gt; with Playwright, I wanted to run them in Gitlab CI.&lt;/p&gt;

&lt;p&gt;Following the &lt;a href="https://playwright.dev/docs/ci#gitlab-ci" rel="noopener noreferrer"&gt;Playwright documentation&lt;/a&gt;, I set up my YAML file to run the tests after deployment&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test:playwright:smoke&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;
    &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull&lt;/span&gt;
    &lt;span class="na"&gt;untracked&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitor&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/playwright:v1.52.0-noble&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PLAYWRIGHT_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run test -- app/*/*/smoke.spec.ts&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH == "main" &amp;amp;&amp;amp; $CI_DEPLOY_FREEZE == &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="s"&gt; &amp;amp;&amp;amp; $CI_PIPELINE_SOURCE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;span&gt;Note&lt;/span&gt; there are some extra bits in the YAML above which I won't repeat but are always good to see&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cache:&lt;/code&gt; In a previous step, I've run an &lt;code&gt;npm install&lt;/code&gt; and cached the &lt;code&gt;node_modules&lt;/code&gt; folder to speed up the job&lt;/li&gt;
&lt;li&gt;My &lt;code&gt;npm run test&lt;/code&gt; looks like &lt;code&gt;playwright test --grep-invert @vr&lt;/code&gt; to exclude visual regression tests by default (although we are then specifying to run only the smoke tests)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;PLAYWRIGHT_ENV&lt;/code&gt; is specified as we are using our custom &lt;a href="https://liquidlight.github.io/playwright-framework/" rel="noopener noreferrer"&gt;Playwright framework&lt;/a&gt; for dynamic hosts&lt;/li&gt;
&lt;li&gt;The rules prevent the smoke test from running on anything except main when there isn't a deployment freeze&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue we ran into was with a version mis-match. The image version we were using was different to that in the &lt;code&gt;package-lock.json&lt;/code&gt;. We were presented with this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ╔══════════════════════════════════════════════════════════════════════╗
    ║ Looks like Playwright Test or Playwright was just updated to 1.50.1. ║
    ║ Please update docker image as well.                                  ║
    ║ -  current: mcr.microsoft.com/playwright:v1.52.0-noble               ║
    ║ - required: mcr.microsoft.com/playwright:v1.50.1-noble               ║
    ║                                                                      ║
    ║ &amp;lt;3 Playwright Team                                                   ║
    ╚══════════════════════════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; update the image to match the &lt;code&gt;package-lock&lt;/code&gt; version, however this CI config is shared between projects and it would be a pain to keep them all in sync - especially as the CI fails if this error crops up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;Why does this happen? Playwright installs a specific version of the browsers to match the code version. The issue happens when you have a different Playwright version installed to the browsers set up.&lt;/p&gt;

&lt;p&gt;The Playwright &lt;a href="https://playwright.dev/docs/docker" rel="noopener noreferrer"&gt;Docker image&lt;/a&gt; has the browsers set up, so running the local Playwright code with the global browsers throws the version mis-match.&lt;/p&gt;

&lt;p&gt;We needed to run the local &lt;code&gt;npm run test&lt;/code&gt; instead of a global &lt;code&gt;npx playwright test&lt;/code&gt; as we needed the Playwright framework to be loaded to parse the custom code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;In the end, the solution was a one-line change (always the way) - adding &lt;code&gt;npx playwright install&lt;/code&gt; before running the tests (this is actually done in the &lt;a href="https://playwright.dev/docs/ci#github-actions" rel="noopener noreferrer"&gt;Github action docs&lt;/a&gt; for Playwright)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test:playwright:smoke&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx playwright install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run test -- app/*/*/smoke.spec.ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By running &lt;code&gt;npx playwright install&lt;/code&gt; before the test, Node syncs the browser versions with the code to allow them to run. We still use the Playwright docker images as they have all the right tech set-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other ways of solving it
&lt;/h2&gt;

&lt;p&gt;The other ways I thought of solving the issue&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep everything in sync
&lt;/h3&gt;

&lt;p&gt;The first option I considered was keeping everything in sync. Pin the playwright version in the &lt;code&gt;package.json&lt;/code&gt; file to prevent anything updating and then using the corresponding Docker image. This would be an absolute pain with maintenance as the Gitlab CI YAML is shared. We would have to have a release day where we bump all the projects and change the YAML to prevent it blocking a deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  No custom framework
&lt;/h3&gt;

&lt;p&gt;If we weren't using the custom Playwright framework, we could have run &lt;code&gt;npx playwright test&lt;/code&gt; &lt;em&gt;without&lt;/em&gt; the local &lt;code&gt;node_modules&lt;/code&gt; folder. This would have used the globally installed Playwright which matched the cached browsers.&lt;/p&gt;

&lt;p&gt;The danger with this, though, is if your test is (for some reason) incompatible with the version you are using (say, if you were using a new version locally than your Docker image) it could cause the tests to fail&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic image usage
&lt;/h3&gt;

&lt;p&gt;I considered extracting the current version during the &lt;code&gt;npm install&lt;/code&gt; job and caching it as a variable to use later. I didn't get this far as I found the solution above, but for reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/variables/#pass-an-environment-variable-to-another-job" rel="noopener noreferrer"&gt;Pass an environment variable to another job&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/1448#note_704838967" rel="noopener noreferrer"&gt;Use docker images built in previous stages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would have run &lt;code&gt;npm run test -- --version&lt;/code&gt; and stored the output to use later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 3 mins&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>ci</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>Run unit tests in Playwright</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Wed, 08 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/run-unit-tests-in-playwright-554b</link>
      <guid>https://dev.to/mikestreety/run-unit-tests-in-playwright-554b</guid>
      <description>&lt;p&gt;I'm a big believer in using the right tool for the job - but I'm a bigger believer in the best tool for the job is the one you've got.&lt;/p&gt;

&lt;p&gt;While you might not install Playwright to &lt;em&gt;just&lt;/em&gt; do unit tests, if you already have it installed, there is no need to install an additional dependency (such as Jest) to check your functions &amp;amp; methods.&lt;/p&gt;

&lt;p&gt;In simple terms, unit tests are the idea of testing a specific function - the classic example always given is passing two parameters into a function that adds them together and produces a single integer of which you can test the result.&lt;/p&gt;

&lt;p&gt;For some of our projects we already have &lt;a href="https://dev.to/category/playwright/"&gt;Playwright&lt;/a&gt; installed and wanted a way to unit test some utility classes without using a separate framework (and thus, loading a whole new set of dependencies &amp;amp; files).&lt;/p&gt;

&lt;p&gt;This post assumes you already have Playwright installed and configured&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;For our examples, we are going to be using a function which limits an array to the first X number of items&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lives in a file called &lt;code&gt;limit.js&lt;/code&gt; - we'll be making our test next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test
&lt;/h2&gt;

&lt;p&gt;Our conventions state we would make a file next to the JS file as &lt;code&gt;limit.spec.ts&lt;/code&gt; - however if you have Playwright setup, you probably have standards/conventions of your own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Import the playwright functions&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import our JS file&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./limit.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create a new test&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a dummy array&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Check our array length matches our expected length&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this test code, we import the JS file and fire the function - we then check the output. We don't use the &lt;code&gt;page&lt;/code&gt; or &lt;code&gt;request&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out&lt;/strong&gt;: If you have multiple projects pointing to the same directories (e.g. a Desktop and Mobile) this test will run multiple times.&lt;/p&gt;

&lt;p&gt;You can still use &lt;code&gt;test.describe&lt;/code&gt; to group tests &amp;amp; share data - for example, if I wished to test these Mean, Median and Mode &lt;a href="https://gitlab.com/mikestreety-sites/ale-house-rock/-/blob/f9484119ca75a5accd7e09ba835f7d236a714009/app/filters/meanMedianMode.js" rel="noopener noreferrer"&gt;functions from Ale House Rock&lt;/a&gt;, I could do something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;meanMedianMode&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./meanMedianMode.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Verified with&lt;/span&gt;
&lt;span class="c1"&gt;// https://www.calculatorsoup.com/calculators/statistics/mean-median-mode.php&lt;/span&gt;
&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Calculations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;meanMedianMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Mean - average&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mean&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toStrictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;16.75&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;// Median - middle value&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Median&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;median&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toStrictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;15.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;// Mode - most frequent value&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mode&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toStrictEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;13&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View the original post: &lt;a href="https://www.mikestreety.co.uk/blog/run-unit-tests-in-playwright/" rel="noopener noreferrer"&gt;https://www.mikestreety.co.uk/blog/run-unit-tests-in-playwright/&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Login with Puppeteer and re-use cookies for another window</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Thu, 23 Nov 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/login-with-puppeteer-and-re-use-cookies-for-another-window-5fbm</link>
      <guid>https://dev.to/mikestreety/login-with-puppeteer-and-re-use-cookies-for-another-window-5fbm</guid>
      <description>&lt;p&gt;FFor a recent project I needed to automate something which was only available in the CMS via a login. To help speed to process up, I created a script which can login with supplied credentials and store the cookies in a local file. The main process can then use these cookies to carry out the task rather than needing to login each time.&lt;/p&gt;

&lt;p&gt;A working example of this code can be found in this &lt;a href="https://github.com/liquidlight/puppeteer-typo3-translations"&gt;git repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Puppeteer?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pptr.dev/"&gt;Puppeteer&lt;/a&gt; is a Node/NPM package which allows you to create &amp;amp; control a headless Chrome instance, allowing you to do front-end/UI based tasks programmatically. It is hugely powerful and worth investigating if that is your thing. One of the most common examples is opening a page and taking a screenshot or submitting a form for testing.&lt;/p&gt;

&lt;p&gt;In this instance, we are going to login to the CMS and then store the cookies in a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login to your site
&lt;/h2&gt;

&lt;p&gt;Below is the code to login to the site and store the cookies - there is some explanation text afterwards with some more details.&lt;/p&gt;

&lt;p&gt;To make this work, you need to install both &lt;code&gt;puppeteer&lt;/code&gt; to carry out the work and &lt;code&gt;fs&lt;/code&gt; to write the file. If you are using the cookies later in the same file, this bit isn't required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i puppeteer fs &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating a &lt;code&gt;login.js&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Save this code in a file (e.g. &lt;code&gt;login.js&lt;/code&gt;) and then run it via command line (e.g. &lt;code&gt;node login.js&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I would recommend changing the &lt;code&gt;headless&lt;/code&gt; value to &lt;code&gt;false&lt;/code&gt; while you are testing, as this opens the browser and allows you to watch the code execute and spot any issues&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Require packages&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Login credentials&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create a login function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a new puppeteer browser&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="c1"&gt;// Change to `false` if you want to open the window&lt;/span&gt;
        &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Create a new browser page&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Go to the URL&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Input username (selector may need updating)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type=text]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Input password (selector may need updating)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type=password]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Click the submit button&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[type=submit]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Wait for a selector to be loaded on the page -&lt;/span&gt;
    &lt;span class="c1"&gt;// this helps make sure the page is fully loaded so you capture all the cookies&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./cookies.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Optional - sessions &amp;amp; local storage&lt;/span&gt;
    &lt;span class="c1"&gt;// const sessionStorage = await page.evaluate(() =&amp;gt; JSON.stringify(sessionStorage));&lt;/span&gt;
    &lt;span class="c1"&gt;// await fs.writeFileSync('./sessionStorage.json', cookies);&lt;/span&gt;

    &lt;span class="c1"&gt;// const localStorage = await page.evaluate(() =&amp;gt; JSON.stringify(localStorage));&lt;/span&gt;
    &lt;span class="c1"&gt;// await fs.writeFileSync('./localStorage.json', cookies);&lt;/span&gt;

    &lt;span class="c1"&gt;// Close the browser once you have finished&lt;/span&gt;
    &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Fire the function&lt;/span&gt;
&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read through the comments as they should help guide you where things may need altering - the main thing to watch out for this the field selectors when entering a username &amp;amp; password and the selector for when the page has loaded.&lt;/p&gt;

&lt;p&gt;The other thing to watch out for (that this does not cater for) is 2FA. It may be you need to open the browser window and enter it yourself before proceeding.&lt;/p&gt;

&lt;p&gt;You can also choose to store the session and local storage, should your application use this for authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the cookies
&lt;/h2&gt;

&lt;p&gt;Once the above script as run, you should have a &lt;code&gt;cookies.json&lt;/code&gt; file sitting alongside your login script. If you opted to also collect the &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt; then these files will also exist.&lt;/p&gt;

&lt;p&gt;Once again you will need &lt;code&gt;puppeteer&lt;/code&gt; and &lt;code&gt;fs&lt;/code&gt; as dependencies so you can load the cookie file.&lt;/p&gt;

&lt;p&gt;Create your secondary script which will utilise the cookies with the following code as a base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Load dependencies&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Load the cookies into the page passed in&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadCookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Load the cookie JSON file&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./cookies.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Parse the text file as JSON&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookieJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Set the cookies on the page&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCookie&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Our main function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a new puppeteer browser&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="c1"&gt;// Change to `false` if you want to open the window&lt;/span&gt;
        &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Create a new page in the browser&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Load the cookies&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Load your super secure URL&lt;/span&gt;
    &lt;span class="c1"&gt;// await page.goto(https://super.secure/url);&lt;/span&gt;
    &lt;span class="c1"&gt;// Do more work&lt;/span&gt;
    &lt;span class="c1"&gt;// Profit&lt;/span&gt;

    &lt;span class="c1"&gt;// Close the browser once you have finished&lt;/span&gt;
    &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Run it all&lt;/span&gt;
&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there you can navigate through your system as if you were logged in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 5 mins&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Node, NPM&lt;/p&gt;

</description>
      <category>npm</category>
      <category>node</category>
      <category>puppeteer</category>
    </item>
    <item>
      <title>Adding Tina CMS to 11ty</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Sat, 14 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/adding-tina-cms-to-11ty-3ml7</link>
      <guid>https://dev.to/mikestreety/adding-tina-cms-to-11ty-3ml7</guid>
      <description>&lt;p&gt;This website is powered by 11ty. I've written about 11ty before and will talk the ear off anyone who even mentions Static Site Generators (or just websites) about 11ty. I love it, it's great.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;only&lt;/em&gt; disadvantage with using Markdown and a static site is editing content. It's great having your content with the code as you can leverage the power of your IDE and do programmatic changes if required, however, sometimes you just want to fix a typo or write a quick note - normally from your mobile.&lt;/p&gt;

&lt;p&gt;What I wanted was a CMS which would work the existing ecosystem. Something which will commit to the repository so I can still clone down my site and edit it, but also so I can make small changes on the go without battling the Gitlab/Github IDEs on mobile.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://www.netlifycms.org/" rel="noopener noreferrer"&gt;Netlify CMS&lt;/a&gt; and &lt;a href="https://www.mikestreety.co.uk/blog/add-netlify-cms-to-an-existing-11ty-site/" rel="noopener noreferrer"&gt;something I've written about before&lt;/a&gt;. I tried to add it to this site but got in a muddle and the mobile experience isn't the best, so I went on the hunt for something else.&lt;/p&gt;

&lt;p&gt;I eventually came across &lt;a href="https://forestry.io/" rel="noopener noreferrer"&gt;Forestry CMS&lt;/a&gt;. My banner blindness kicked in and it was only once I had signed up, configured and added it to my site did I notice the red banner at the top mentioned they were closing down in March - not good for longevity.&lt;/p&gt;

&lt;p&gt;However, it did mention it was being replaced with &lt;a href="https://tina.io/" rel="noopener noreferrer"&gt;TinaCMS&lt;/a&gt; - they have a nice little &lt;a href="https://tina.io/pricing/" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; and seemed to do what I wanted, so I thought I would give it ago.&lt;/p&gt;

&lt;p&gt;After signing up I noticed my first hurdle - they &lt;strong&gt;only support Github&lt;/strong&gt;. It was a bit of a shame coming from Forestry, which support Gitlab but I migrated my repository to trial the CMS.&lt;/p&gt;

&lt;p&gt;As I had set up with Forestry already, &lt;a href="https://tina.io/docs/introduction/tina-init/" rel="noopener noreferrer"&gt;the TinaCMS setup&lt;/a&gt; was able to migrate some of the config to the &lt;code&gt;.tina&lt;/code&gt; folder. On further inspection, the &lt;code&gt;config.js&lt;/code&gt; file it generates contains a JSON config object, which is able to be manipulated for the fields I wanted - including changing the order and setting some defaults.&lt;/p&gt;

&lt;p&gt;The noteworthy changes I did were for the &lt;code&gt;Notes&lt;/code&gt; section of the config. I wanted the least amount of friction between me wanting to post and pressing the save button, so I have the filename generated from the title and the date to be &lt;code&gt;now&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The full config can be found in the repo, however, the snippet below should help identify the solution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default defineConfig({
    schema: {
        collections: [
            {
                label: "Notes",
                name: "notes",
                path: "app/content/notes",
                format: "md",
                ui: {
                    filename: {
                        slugify: (values) =&amp;gt; {
                            return slugIt(values?.title);
                        },
                    }
                },
                defaultItem: () =&amp;gt; {
                    return {
                        date: new Date().toISOString(),
                    }
                },
                fields: [
                    {
                        type: "string",
                        name: "title",
                        label: "title",
                    },
                    {
                        type: "string",
                        name: "link",
                        label: "link",
                    },
                    {
                        type: "datetime",
                        name: "date",
                        label: "date",
                        ui: {
                            dateFormat: 'YYYY-MM-DD',
                            timeFormat: 'HH:MM:SS'
                        }
                    },
                    {
                        type: "rich-text",
                        name: "body",
                        label: "Body of Document",
                        description: "This is the markdown body",
                        isBody: true,
                    },
                ],
            },
        ],
    },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;To set a default date and time in TinaCMS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;date&lt;/code&gt; field has the &lt;code&gt;dateFormat&lt;/code&gt; and &lt;code&gt;timeFormat&lt;/code&gt; specified, but has a default set in the &lt;code&gt;defaultItem&lt;/code&gt; function at the top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To have an auto-generated filename&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is done in the &lt;code&gt;ui&lt;/code&gt; section of the config. I don't quite understand how it works, but a copy and paste from StackOverflow and a tweak using my own slugify function solved it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const slugIt = function (str) {
    if (str) {
        str = str.replace(/^\s+|\s+$/g, ''); // trim
        str = str.toLowerCase();

        // remove accents, swap ñ for n, etc
        var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;";
        var to = "aaaaeeeeiiiioooouuuunc------";
        for (var i = 0, l = from.length; i &amp;lt; l; i++) {
            str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
        }

        str = str.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
            .replace(/\s+/g, '-') // collapse whitespace and replace by -
            .replace(/-+/g, '-'); // collapse dashes
    }

    return str;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I now have TinaCMS running on this site, with notes auto-posting to Mastodon &lt;a href="https://montemagno.com/automate-posting-to-mastodon-via-web-requests/" rel="noopener noreferrer"&gt;thanks to this article from James Montemagno&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>11ty</category>
      <category>tinacms</category>
      <category>cms</category>
    </item>
    <item>
      <title>Be Code Agnostic</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Thu, 05 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/be-code-agnostic-1d2k</link>
      <guid>https://dev.to/mikestreety/be-code-agnostic-1d2k</guid>
      <description>&lt;p&gt;I wrote a blog about being &lt;a href="https://www.liquidlight.co.uk/blog/how-to-become-device-agnostic-and-what-it-means-for-your-data/" rel="noopener noreferrer"&gt;device agnostic&lt;/a&gt; which is an idea about ensuring your data is not tied to one kind of device, or one manufacturer. I believe you should be able to export your data regularly (in a generic format) or it should exist in a more device-fluid state. Services and devices shouldn't try to lock users in by holding their data hostage. The idea is you can move between devices (or lose a phone) and not be ground to a halt (or be worried about where your data has gone).&lt;/p&gt;

&lt;p&gt;While converting my website from Craft CMS to 11ty, a similar concept approached me: when creating websites, we should try and make our content as &lt;strong&gt;code agnostic&lt;/strong&gt; as possible. This not only applies to our personal websites, but should apply to our clients sites as well.&lt;/p&gt;

&lt;p&gt;Code should just be the transportation for the content and should not influence how it is crafted. If the package/library/CMS you are using or the framework that powers your website disappeared, can you honestly say that you could easily access your content to migrate to a new platform?&lt;/p&gt;

&lt;p&gt;With the move to 11ty, a lot of my content was already written in Markdown in the Craft CMS database. Despite them falling out of fashion with the cool kids, database powered websites are often a great measure of code agnosticism. These frameworks and content management systems rarely stuff too much opinionated code in there. Although, saying that, we use TYPO3 at work which has &lt;code&gt;&amp;lt;a href="t3://page?uid=713"&amp;gt;&amp;lt;/a&amp;gt;&lt;/code&gt; style links. These would be difficult to parse/transpose into complete URLs if you were migrating, but I digress.&lt;/p&gt;

&lt;p&gt;Craft CMS is great example of keeping content, content. I was able to download all my posts as a JSON and didn't have to do any processing on the content itself. With the export, I was able to write a small PHP script that turned the big array into individual Markdown files. As a side-note, JSON, like Markdown, is a great framework-free file format. They seem to have become a pretty solid and reliable standard that the web industry has embraced.&lt;/p&gt;

&lt;p&gt;Sure, 11ty requires some &lt;a href="https://www.11ty.dev/docs/data-frontmatter/" rel="noopener noreferrer"&gt;front matter&lt;/a&gt;, but that seems to be moving into a standard when writing content with Markdown. It does make me feel slightly "off" that framework specific content is in my files, but I feel confident that, if 11ty disappeared tomorrow and my computer died, I could extract, process and get my content extracted again - it's about finding a compromise between framework standards and keeping it clean.&lt;/p&gt;

&lt;p&gt;Throughout my site, I have &lt;code&gt;&amp;lt;div class="info"&amp;gt;&lt;/code&gt; boxes and such. I considered turning these into &lt;a href="https://www.11ty.dev/docs/shortcodes/" rel="noopener noreferrer"&gt;11ty shortcodes&lt;/a&gt;, but this tied me deeper into the 11ty ecosystem. I love 11ty, but if the shortcode syntax changed, or I needed/wanted to migrate my content elsewhere, I would need to do a lot of find and replacing. I figured it best to stick to plain ol' HTML.&lt;/p&gt;

&lt;p&gt;I suppose that is the crux of it really - it's not necessarily about whether your content does or does not have framework specific code in it, it's whether you feel confident in getting it out. Can the framework specific code be extracted and parsed? Are you tied in or can you get your words out without too much trouble?&lt;/p&gt;

&lt;p&gt;Perhaps you need a headless CMS? A headless CMS is a content management system without the front-end. You use APIs and to access the data for a website which means you can use any technology you want to build your website or app.&lt;/p&gt;

&lt;p&gt;This blog seems to be turning into a massive advocacy into custom APIs serving up data. The nature of a good API ensures the content stays code agnostic so it can be used by many apps. I've never used GraphQL, but I can see the appeal as the &lt;em&gt;data&lt;/em&gt; is just data, and the framework determines how it gets that content and how it uses it.&lt;/p&gt;

&lt;p&gt;So be more code agnostic, let your content be content and your code be code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 3 mins&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Ramblings&lt;/p&gt;

</description>
      <category>api</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How to delete a Git branch</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Sat, 19 Nov 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/how-to-delete-a-git-branch-ng4</link>
      <guid>https://dev.to/mikestreety/how-to-delete-a-git-branch-ng4</guid>
      <description>&lt;p&gt;Deleting branches is important to ensure old code doesn't hang around or the wrong thing doesn't get merged&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In the examples below &lt;code&gt;pow&lt;/code&gt; will be used for the branch name&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete a local branch
&lt;/h2&gt;

&lt;p&gt;If you have a local branch you want to delete you can run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git branch -d pow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your branch &lt;strong&gt;has not been merged&lt;/strong&gt; into your &lt;strong&gt;current branch&lt;/strong&gt; you need to change the &lt;code&gt;-d&lt;/code&gt; to a capital:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git branch -D pow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Delete a remote branch
&lt;/h2&gt;

&lt;p&gt;If you wish to delete the branch via command line, you have to "push" it with a colon (&lt;code&gt;:&lt;/code&gt;) preceding the name&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push origin :pow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Updating your local branch data
&lt;/h2&gt;

&lt;p&gt;If you deleted your remote branch from another computer (or via the website if on Github/Gitlab), you might find it is still listed when running a &lt;code&gt;git branch -a&lt;/code&gt;. This means your local Git repository thinks the branch still exits and could cause conflicts if you try to create a branch of the same name.&lt;/p&gt;

&lt;p&gt;To remove these, you can fetch with an additional &lt;code&gt;--prune&lt;/code&gt; parameter&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git fetch origin --prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you want it to always prune when you do a &lt;code&gt;git fetch origin&lt;/code&gt;, you can set this as a global setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git config --global fetch.prune true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 1 mins&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Git&lt;/p&gt;

</description>
      <category>git</category>
    </item>
    <item>
      <title>How I improved the speed of Docker builds in Gitlab CI</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Wed, 08 Jun 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/how-i-improved-the-speed-of-docker-builds-in-gitlab-ci-59d9</link>
      <guid>https://dev.to/mikestreety/how-i-improved-the-speed-of-docker-builds-in-gitlab-ci-59d9</guid>
      <description>&lt;p&gt;Our Gitlab CI powered Docker build is now 500% faster (I think?) - taking it more than 15 minutes to around 3 minutes to build and push to a registry.&lt;/p&gt;

&lt;p&gt;Unfortunately, there isn't one thing to copy and paste - this blog post is more a collection of concepts and steps I took to drop the build time. 15 minutes wasn't the end of the world, but I wanted to see how far I could take it.&lt;/p&gt;

&lt;p&gt;This blog post features some pseudo dockerfile &amp;amp; Gitlab CI yaml code - it might not be able to be copied but should give you some idea as to what was happening&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Background
&lt;/h2&gt;

&lt;p&gt;The application is a website which runs on TYPO3 - a composer-based open-source CMS and the client provided a Docker environment to host the website. I'm a big fan of Gitlab CI, so took the challenge to completely compile and build a Docker image via Gitlab, rather than building assets and installing dependencies locally (which I had done in the past).&lt;/p&gt;

&lt;p&gt;The site also features NPM dependencies - nothing over the top, but our SCSS and Webpack JS is compiled using Gulp, along with optimising images and &lt;a href="https://www.liquidlight.co.uk/blog/creating-svg-sprites-using-gulp-and-sass/"&gt;building a sprite&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I'll be using "website" and "application" interchangeably - they ultimately mean the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;The build needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone the repository&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer install&lt;/code&gt; - to get the backend dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm ci&lt;/code&gt; - this is "clean install"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gulp compile&lt;/code&gt; - this generates all the assets&lt;/li&gt;
&lt;li&gt;Copy the compiled assets into an apache-ready Docker container&lt;/li&gt;
&lt;li&gt;Push the container to the registry&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Docker multi-stage build
&lt;/h2&gt;

&lt;p&gt;The first attempt at building the application was to use a &lt;a href="https://dev.to/blog/creating-a-multi-stage-docker-build-to-make-your-images-smaller/"&gt;multi-stage Docker build&lt;/a&gt; (this is actually the project that was written for). This used a custom image which had NPM and Composer preloaded to install and compile the dependencies and the final assets we passed to the Web image for pushing.&lt;/p&gt;

&lt;p&gt;The Docker file looked something like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;custom-node-and-composer-image&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;

&lt;span class="c"&gt;# COPY package.json, composer.json &amp;amp; app files&lt;/span&gt;
composer i
npm ci
gulp compile

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; custom-web-app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist /var/www/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the Gitlab CI file having a script block like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t registry:my-image .&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push registry:my-image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This meant the Gitlab file was extremely lightweight, which was great as it meant you could compile the full Docker image locally, if required. It left Docker to do most of the heavy lifting which seemed to make the most sense.&lt;/p&gt;

&lt;p&gt;This built the Docker image as expected, but took a whopping &lt;strong&gt;15 minutes&lt;/strong&gt; from start to finish. Granted, it is all done on Gitlab so you can push and carry on with some other work if required, but it still needed you to go back 15 minutes later to test everything worked as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing with Gitlab CI
&lt;/h2&gt;

&lt;p&gt;In a bid to try and speed up the process I took a blind punt and seeing what would happen if I took the &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;composer&lt;/code&gt; install steps and moved them &lt;em&gt;outside&lt;/em&gt; of the Docker build. Let Gitlab do the installing and then copy the resulting files to Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; custom-web-app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; /app/dist /var/www/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gitlab CI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custom-node-and-composer-image-with-docker&lt;/span&gt;
&lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;composer i&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gulp compile&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t registry:my-image .&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push registry:my-image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The premise behind this is to run all our build steps outside the Docker file and then copy the resulting files in.&lt;/p&gt;

&lt;p&gt;We use the base Docker image used in the original multi-stage build to run the Gitlab CI step - the only difference being that Docker is installed inside the Docker image so it can build the final… image (Docker and image were mentioned far too many times).&lt;/p&gt;

&lt;p&gt;This obviously comes with the big disadvantage of suddenly not being able to build the Docker image locally - however we use ddev for local development so this wasn't an issue for us.&lt;/p&gt;

&lt;p&gt;This dropped the build times from 15 to 7 minutes - halving the time it takes just by moving the build step out of Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;With the time down to 7 minutes for a fresh build, it was time to start looking at caching. Using the next few steps, I was able to get the build down to around 3 minutes. However, if all the caches got invalidated then there is likelihood the next build would go back up to 7 minutes again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Caching
&lt;/h2&gt;

&lt;p&gt;Docker has caching build in to the process - that is why a fresh build always takes longer. Each step is made into a mini image that then gets "loaded" into the main build. More steps means more HDD space but a speedier build. Less steps means smaller space, but a bigger chance of one of the steps' cache being invalidated.&lt;/p&gt;

&lt;p&gt;During this exercise I learnt a lot about Dockerfiles and optimising them to get the best performance. The big thing I learnt was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If anything changes in a step, it invalidates the cache for &lt;em&gt;every&lt;/em&gt; step after that&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Knowing that really helps order the copy steps - make sure you have the files which change the least being copied first (base config files etc.). The last &lt;code&gt;COPY&lt;/code&gt; (or other step) should be the files that change the most - for example if you have FE assets with cache-busting file names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gitlab CI Caching
&lt;/h2&gt;

&lt;p&gt;The final piece to the puzzle was to use Gitlab CI caching. You can use the cache in the runner itself, however this isn't very reliable - especially if you have several runners.&lt;/p&gt;

&lt;p&gt;Gitlab runners don't retain for projects or even jobs in a pipeline, so you can have a different runner picking up different stages - if you use the runner cache there is a high likelihood it would be out-of-date or (dangerously) wrong.&lt;/p&gt;

&lt;p&gt;The most efficient (and safest) method is to use an external service like an Amazon S3 bucket or a self hosted solution, such as &lt;a href="https://min.io/"&gt;Minio&lt;/a&gt; - this has S3 compatible APIs so can be used as drop in replacement.&lt;/p&gt;

&lt;p&gt;Our Gitlab CI file features a global cache of the &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;vendor&lt;/code&gt; folders, which is invalidated if the respective lockfiles change&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;composer.lock&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vendor/&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is plenty of documentation about &lt;a href="https://docs.gitlab.com/ee/ci/caching/"&gt;Gitlab Caching on their website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The last step was to change my &lt;code&gt;npm ci&lt;/code&gt; command to just &lt;code&gt;npm i&lt;/code&gt;. The &lt;code&gt;ci&lt;/code&gt; command deletes the exsiting &lt;code&gt;node_modules&lt;/code&gt; folder, which defeats the point in caching it!&lt;/p&gt;

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

&lt;p&gt;Removing the build from Docker, re-ordering my Dockerfile and leaning on caching are the main steps I took in dramatically dropping the build time of my Docker image using Gitlab CI.&lt;/p&gt;

&lt;p&gt;At time of writing, the last deployment (including linting) took 3 minutes and 21 seconds - the build of that taking 2 minutes and 9 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 5 mins&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; Docker, Gitlab CI&lt;/p&gt;

</description>
      <category>docker</category>
      <category>gitlabci</category>
    </item>
    <item>
      <title>Get your Eleventy Site into the real world (Netlify, Cloudflare pages or any host)</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Mon, 19 Apr 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/get-your-eleventy-site-into-the-real-world-netlify-cloudflare-pages-or-any-host-2953</link>
      <guid>https://dev.to/mikestreety/get-your-eleventy-site-into-the-real-world-netlify-cloudflare-pages-or-any-host-2953</guid>
      <description>&lt;p&gt;Eleventy (11ty) is a great entry point for getting a markdown or JSON content from files to a website. There are plenty of tutorials out in the world to get 11ty up and running in a development environment.&lt;/p&gt;

&lt;p&gt;But once you are ready to get your website out into the public domain, what are the options and how do you go about it? There are a couple of free options (with paid extras), as well as self-hosting if that is your JAM (geddit? Because 11ty is JAMstack... No? I'll move on...)&lt;/p&gt;

&lt;p&gt;There were 3 main contenders when I planned this post a few weeks ago, however a new arrival (Cloudflare pages) gives an extra bit of variance. For ease of access with low barriers of entry, I would recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-hosted&lt;/li&gt;
&lt;li&gt;Netlify&lt;/li&gt;
&lt;li&gt;Github/Gitlab pages&lt;/li&gt;
&lt;li&gt;Cloudflare Pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-hosted
&lt;/h2&gt;

&lt;p&gt;This is the most manual of the options, but gives you the most control over your website. It is also the only one that requires a domain name of sorts to access the final result (the other options give you a placeholder/branded domain name).&lt;/p&gt;

&lt;p&gt;With 11ty, you can either generate the site in a one-off, or run a watcher, which recompiles on save.&lt;/p&gt;

&lt;p&gt;When self-hosting, you should run the "compile" step of Eleventy and manually move the source files to your designated hosting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx @11ty/eleventy --serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will compile your site into your &lt;code&gt;output&lt;/code&gt; dir as specified in your &lt;code&gt;.eleventy.js&lt;/code&gt;. You can transfer this directory to your hosting environment, be it via FTP, SCP/RSYNC or you can zip it up to transfer it.&lt;/p&gt;

&lt;p&gt;The other option is to &lt;code&gt;git clone&lt;/code&gt; or similar your source files and run the compile step on the server you are hosting it on - some shared hosting environments, however, might not let you install npm/node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netlify
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt; is a fantastic hosting service that provides a fairly straightforward deployment pipeline, as long as you are comfortable with git (although if you're not, they offer alternatives).&lt;/p&gt;

&lt;p&gt;To get your 11ty site onto Netlify with git:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push your git repository to Github, Gitlab or Bitbucket&lt;/li&gt;
&lt;li&gt;Sign up for a Netlify account&lt;/li&gt;
&lt;li&gt;Click "New site from Git"&lt;/li&gt;
&lt;li&gt;Connect to your git repository host &amp;amp; select the repository&lt;/li&gt;
&lt;li&gt;Choose the branch you want to use for deployment&lt;/li&gt;
&lt;li&gt;Enter the build command and publish directory

&lt;ul&gt;
&lt;li&gt;Build command is &lt;code&gt;npx @11ty/eleventy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Publish directory is your &lt;code&gt;output&lt;/code&gt; directory specified in your &lt;code&gt;.eleventy.js&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click "deploy site"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Netlify will take a few minutes to deploy your website but, once complete you are provided with a random &lt;code&gt;.netlify.app&lt;/code&gt; URL to view your website. You can rename this or you can link a custom domain (even &lt;a href="https://www.mikestreety.co.uk/blog/setting-up-a-custom-domain-with-netlify-with-cloudflare-ssl"&gt;using Cloudflare&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If you don't want to/can't connect your git provider (or your 11ty site isn't in a git repo), Netlify offer a drag and drop interface (at the bottom of the "Sites" page). Run the 11ty compilation steps locally and upload the output directory.&lt;/p&gt;

&lt;p&gt;Side note: This site, &lt;a href="https://www.behindthesource.co.uk/"&gt;Behind the Source&lt;/a&gt; and &lt;a href="https://hovelo.co.uk/"&gt;Hovélo&lt;/a&gt; are all hosted on Netlify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Github/Gitlab pages
&lt;/h2&gt;

&lt;p&gt;Gitlab and Github pages are another great way of hosting your website for free. It takes a bit more configuration than Netlify, but if your website data is hosted on one of these services, there might an argument to reduce the number of services required.&lt;/p&gt;

&lt;p&gt;I'm not a Github user really, and haven't really investigated using Github pages so I won't attempt to muddle my way through a tutorial. &lt;a href="https://www.linkedin.com/pulse/eleventy-github-pages-lea-tortay/"&gt;Lea Tortay&lt;/a&gt; has written a comprehensive tutorial on this topic.&lt;/p&gt;

&lt;p&gt;For Git &lt;strong&gt;lab&lt;/strong&gt; however, I have just experimented and been able to spin my site up in the space of 15 minutes (pray the lords for 11ty!).&lt;/p&gt;

&lt;p&gt;To get this working, the output directory &lt;em&gt;had&lt;/em&gt; to be &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure your git repository is on &lt;a href="https://gitlab.com/"&gt;gitlab.com&lt;/a&gt; (this can be done with self-hosted instances too, but the following steps follow the provided Gitlab instance)&lt;/li&gt;
&lt;li&gt;Add a new file to the root directory called &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add the following contents
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: node:latest# This folder is cached between builds# http://docs.gitlab.com/ee/ci/yaml/README.html#cachecache: paths: - node_modules/pages: script: - npm install - ./node_modules/.bin/eleventy artifacts: paths: - public only: - master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Enable CI/CD and Pages on your repository&lt;/li&gt;
&lt;li&gt;Push your changes to Gitlab&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CI/CD should fire and compile your pages, you website should then be available at &lt;code&gt;&amp;lt;username&amp;gt;.gitlab.io/path/to/repo&lt;/code&gt;. You can add a custom domain in the Gitlab pages config if that is the route you choose to take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pages.cloudflare.com/"&gt;Cloudflare pages&lt;/a&gt; are the new kids on the block and (at time of writing) in beta. Looking like a direct competitor to Netlify, it appeals to me as an avid Cloudflare user to keep everything in one place.&lt;/p&gt;

&lt;p&gt;Currently they only support Github (but &lt;a href="https://developers.cloudflare.com/pages/platform/known-issues"&gt;Gitlab and other things&lt;/a&gt; are planned), however the workflow should be very familiar.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up to Cloudflare and ensure your git repository is on Github&lt;/li&gt;
&lt;li&gt;Connect your Github account and select your repository&lt;/li&gt;
&lt;li&gt;Give it a name and select the branch&lt;/li&gt;
&lt;li&gt;Enter the build command (e.g. &lt;code&gt;npx @11ty/eleventy&lt;/code&gt;) and your output directory&lt;/li&gt;
&lt;li&gt;Click save and deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll get a &lt;code&gt;pages.dev&lt;/code&gt; domain, but this can be replaced with a custom domain name if desired.&lt;/p&gt;




&lt;p&gt;And there we have it. 4 methods for getting your Eleventy site out into the wild. You have no excuse now!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read time:&lt;/strong&gt; 4 mins&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; DevOps, 11ty, Netlify, Github, Gitlab, Cloudflare&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Using Cloudflare workers to publish your scheduled 11ty posts</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Mon, 05 Apr 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/using-cloudflare-workers-to-publish-your-scheduled-11ty-posts-1phc</link>
      <guid>https://dev.to/mikestreety/using-cloudflare-workers-to-publish-your-scheduled-11ty-posts-1phc</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;With my switch to 11ty away from a PHP based CMS I was struggling to see how I could schedule posts. When 11ty builds, it builds static HTML files - with no way of "show this post when this date has passed" functionality.&lt;/p&gt;

&lt;p&gt;I like to write my posts and schedule them - I find it gives me more motivation to write, but I digress. I wanted a way to rebuild my site when a post was due to go live.&lt;/p&gt;

&lt;p&gt;The straightforward approach is to trigger a re-build manually in Netlify on or after the day the post was scheduled. My "if" blocks would pick up the post in the "past" and populate the listing, RSS and sitemap pages with the new URL.&lt;/p&gt;

&lt;p&gt;However, this required me to remember and be near a device that can do it (their interface is surprisingly mobile friendly).&lt;/p&gt;

&lt;p&gt;The next approach was to use the Netlify &lt;a href="https://docs.netlify.com/configure-builds/build-hooks/"&gt;build hook&lt;/a&gt; to trigger a rebuild every night. This would tick the automation box, but would quickly eat up my free Netlify minutes, along with doing several unnecessary builds (I publish every other week, generally).&lt;/p&gt;

&lt;p&gt;I could schedule a build every fortnight, but for those times I have a &lt;a href="https://dev.to/category/notes"&gt;Notes post&lt;/a&gt; scheduled or similar, it would potentially be 2 weeks before it appears (unless I publish there and then).&lt;/p&gt;

&lt;p&gt;What I wanted is for a system to know when my next scheduled post is and regularly check for that date. It can then trigger a build if required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Intro&lt;/li&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Objectives&lt;/li&gt;
&lt;li&gt;TL:DR;&lt;/li&gt;
&lt;li&gt;
Steps

&lt;ul&gt;
&lt;li&gt;Create an "upcoming" endpoint&lt;/li&gt;
&lt;li&gt;Get the build hook&lt;/li&gt;
&lt;li&gt;Make the Worker&lt;/li&gt;
&lt;li&gt;Using the UI&lt;/li&gt;
&lt;li&gt;Worker JavaScript&lt;/li&gt;
&lt;li&gt;Cloudflare Wrangler CLI&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Objectives
&lt;/h2&gt;

&lt;p&gt;With the preamble out the way we are going to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a page with 11ty which lists the next scheduled post&lt;/li&gt;
&lt;li&gt;Check this date regularly, if it matches today (or is in the past) trigger a rebuild&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Create an &lt;code&gt;upcoming.json&lt;/code&gt; or similar file to list your next post (&lt;a href="https://gitlab.com/mikestreety/mikestreety/-/blob/master/app/content/upcoming.json.njk"&gt;Example on Gitlab&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Host your site with Netlify and get the build hook&lt;/li&gt;
&lt;li&gt;Set up a scheduled Cloudflare Worker (&lt;a href="https://gitlab.com/mikestreety-components/netlify-scheduled-build"&gt;Gitlab repo&lt;/a&gt;) to check the file regularly&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create an "upcoming" endpoint
&lt;/h3&gt;

&lt;p&gt;The first step is to create an endpoint (page) somewhere with the date of your next post in it. For me, I found it the least path of resistance for this to be a &lt;code&gt;json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://remysharp.com/2019/06/26/scheduled-and-draft-11ty-posts"&gt;Remy Sharp's Post&lt;/a&gt; I created a &lt;code&gt;blog&lt;/code&gt; collection, which is posts that are not a draft neither in the future&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const now = new Date(); const livePosts = p =&amp;gt; p.date &amp;lt;= now &amp;amp;&amp;amp; !p.data.draft;  config.addCollection('blog', collection =&amp;gt; { return collection .getFilteredByGlob('./app/content/blog/*.md') .filter(livePosts) .reverse(); });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the same technique, I have also made a &lt;code&gt;drafts&lt;/code&gt; collection in my &lt;code&gt;.eleventy.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.addCollection('drafts', collection =&amp;gt; { return collection .getFilteredByGlob('./app/content/{blog,drafts}/*.md') .filter(p =&amp;gt; !livePosts(p)) .reverse(); });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The drafts collection contains both posts that are due to go live or are marked with &lt;code&gt;draft: true&lt;/code&gt; in the front matter.&lt;/p&gt;

&lt;p&gt;One method here would be to make a &lt;code&gt;upcoming&lt;/code&gt;/&lt;code&gt;scheduled&lt;/code&gt;/&lt;code&gt;pending&lt;/code&gt; collection which doesn't include &lt;code&gt;draft: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As I only needed this function in one place, I decided to use JavaScript front matter to create a "collection" on the fly.&lt;/p&gt;

&lt;p&gt;Create a file called "upcoming.json.njk" with the following front matter&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--------js{ permalink: "upcoming.json", post: function() { // Get the drafts collection let pending = this.ctx.collections['drafts'] .reverse() // Remove everything that is marked as draft .filter(p =&amp;gt; !p.data.draft); // Return the title and date of the next post return { title: pending[0].data.title, date: pending[0].date }; }}---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line sets a nice permalink, while the following lines load the drafts collection, reverse and filter out the posts marked as a draft.&lt;/p&gt;

&lt;p&gt;Lastly, we return just the first blog post - there is no need to include all of them as we only care about the next one.&lt;/p&gt;

&lt;p&gt;As our function returns an object, we can use Nunjucks built in functions to output as valid JSON&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ post() | dump | safe }}

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

&lt;/div&gt;



&lt;p&gt;This results in what you see on &lt;a href="///upcoming.json"&gt;upcoming.json&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get the build hook
&lt;/h3&gt;

&lt;p&gt;Netlify have some &lt;a href="https://docs.netlify.com/configure-builds/build-hooks/"&gt;fantastic documentation&lt;/a&gt; on how to find your build hook.&lt;/p&gt;

&lt;p&gt;Make a new hook (I called mine "worker" but you could be more specific) and make a note of the URL it gives you.&lt;/p&gt;

&lt;p&gt;We'll store that as an environment variable in our Cloudflare Worker settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the Worker
&lt;/h3&gt;

&lt;p&gt;This uses Cloudflare Workers, but anywhere that can run JavaScript on a cron/scheduled task should work&lt;/p&gt;

&lt;p&gt;The next step is to make a Cloudflare Worker to run as often as you wish to check the date of your "pending" post.&lt;/p&gt;

&lt;p&gt;Note: For this step, I am assuming you have a Cloudflare account and a basic understanding of &lt;a href="https://dev.to/blog/what-are-cloud-functions-cloudflare-workers-and-serverless"&gt;Cloudflare Workers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can either make a new worker through the user interface on the CLoudflare website or you can use the &lt;a href="https://developers.cloudflare.com/workers/cli-wrangler"&gt;Cloudflare Wrangler&lt;/a&gt; CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the UI
&lt;/h3&gt;

&lt;p&gt;You can make a Cloudflare worker using their website with the "Quick Edit" button.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the Workers page and click "Create a Worker"&lt;/li&gt;
&lt;li&gt;Paste in the Javascript below (the same file is &lt;a href="https://gitlab.com/mikestreety-components/netlify-scheduled-build/-/blob/master/index.js"&gt;on Gitlab&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Navigate to the setting page and under the "Triggers" tab click "Add Cron Trigger" and input the pattern for how often you want it to trigger&lt;/li&gt;
&lt;li&gt;Navigate to the "Settings" tab and create 2 environment variables&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;BUILD_HOOK - This is your netlify build hook URL&lt;/li&gt;
&lt;li&gt;UPCOMING_JSON - This is the full path to your &lt;code&gt;/upcoming.json&lt;/code&gt; made in step one (e.g. &lt;code&gt;https://www.mikestreety.co.uk/upcoming.json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Worker JavaScript
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*** Fire the check on both the schedule and when accessed via the URL */ addEventListener('scheduled', event =&amp;gt; {  event.waitUntil(checkBuildRequirement())});addEventListener('fetch', async event =&amp;gt; {   event.respondWith(checkBuildRequirement())});/*** Shall we build it?*/async function checkBuildRequirement() { // Get the pending posts and parse as JSON   let pending = await fetch(UPCOMING_JSON) .then(data =&amp;gt; data.json()), // Should the build be triggered? rebuild = 'No build needed'; // If we have an item and the date is in the "past" if( pending &amp;amp;&amp;amp; pending.date &amp;amp;&amp;amp; (new Date(pending.date) &amp;lt; new Date())    ) { rebuild = `Building ${pending.title}`; await fetch(BUILD_HOOK, { method: 'POST', });    }   // Return some text return new Response(rebuild, { headers: { 'content-type': 'text/plain' }    });}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cloudflare Wrangler CLI
&lt;/h3&gt;

&lt;p&gt;If you chose to use the CLI, you can use my &lt;a href="https://gitlab.com/mikestreety-components/netlify-scheduled-build"&gt;git repo&lt;/a&gt; as a base. Rename &lt;code&gt;wrangler.toml.example&lt;/code&gt; to &lt;code&gt;wrangler.toml&lt;/code&gt; and fill in the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; - lowercase, hyphenated&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;account_id&lt;/code&gt; - found on the workers overview page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zone_id&lt;/code&gt; - found on the right hand side of your main Cloudflare dashboard (as this worker is scheduled, it doesn't need to be the domain of your 11ty site - but it helps)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPCOMING_JSON&lt;/code&gt; - This is the full path to your &lt;code&gt;/upcoming.json&lt;/code&gt; made in step one (e.g. &lt;code&gt;https://www.mikestreety.co.uk/upcoming.json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lastly, review and adjust, if required, the schedule for your worker. The notation is that of a &lt;a href="https://crontab.guru/"&gt;standard crontab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The last thing you need to do is set you build hook path. From your worker project directory, run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wrangler secret put BUILD_HOOK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will prompt for your URL and store it on Cloudflare.&lt;/p&gt;

&lt;p&gt;The last thing is to run the following to get your worker live.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wrangler publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should look something like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✨ JavaScript project found. Skipping unnecessary build!
✨ Successfully published your script to
 https://your-worker-name.account.workers.dev
with this schedule
 0 9,15 * * *

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

&lt;/div&gt;



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

&lt;p&gt;You should now have your 11ty site rebuilding on Netlify &lt;em&gt;only&lt;/em&gt; when you have a new post scheduled to go live.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/mikestreety"&gt;Let me know&lt;/a&gt; if you have an questions or feedback.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>11ty</category>
      <category>cloudflareworkers</category>
    </item>
    <item>
      <title>Side Project Imposter Syndrome</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Fri, 02 Apr 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/side-project-imposter-syndrome-2d6o</link>
      <guid>https://dev.to/mikestreety/side-project-imposter-syndrome-2d6o</guid>
      <description>&lt;p&gt;Imposter syndrome is prevalent in every industry, not least the web world. Many people who I respect and admire greatly have blogged about their own feelings of not belonging or not deserving of their place.&lt;/p&gt;

&lt;p&gt;I have been fortunate enough to not have experienced imposter syndrome that much in my professional career. I appreciate my privilege has no doubt played a huge role in that, but I've kept my head down and felt like I've worked hard to get where I am today.&lt;/p&gt;

&lt;p&gt;More recently, however, I have experienced imposter syndrome more and more in a working day, especially since working my way up to director level. Having meetings with clients and trying to appear as this well-travelled, experienced and seasoned professional really kicks you in the feels when you know you're not.&lt;/p&gt;

&lt;p&gt;I kind of understand feeling imposter syndrome at work as you are paid are being paid to have "knowledge" and if you feel like you don't have that knowledge then you feel like a fake.&lt;/p&gt;

&lt;p&gt;What I don't get though, is why I experience impostor syndrome with my side &amp;amp; personal projects. It was a recent topic of conversation that came up on &lt;a href="https://makelifeworkpodcast.com/what-is-a-side-project-if-not-a-safe-place-to-get-imposter-syndrome/"&gt;on a podcast&lt;/a&gt; I was recording and, since then, I've not been able to stop thinking about it.&lt;/p&gt;

&lt;p&gt;I find it hard to self-promote and, even when I do, I don't have a lot of confidence in what I've written and it shows. So far, in 2021, I've managed to publish a blog post every other week. Despite this, I've only tweeted links to my personal site twice this year. This isn't me forgetting, I just don't trust the words i've written - especially when it comes to technical posts. I've been a "professional" developer (well, I've been paid to develop websites) for 12 years - but I still don't believe in my abilities. Even as I'm writing this, I'm questioning about whether I should even post it.&lt;/p&gt;

&lt;p&gt;Throughout February and March, I recorded 6 episodes of a podcast series &lt;a href="https://makelifeworkpodcast.com/"&gt;Make Life Work - On The Side Takeover&lt;/a&gt;. I didn't blog about it, I didn't tweet about it, I didn't even tell my family. Every week that podcast went out I felt like a fraud - who am I to have the authority to speak on a &lt;em&gt;podcast&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;When a project completes at work, you get the reassuring emails from clients, the congratulations from co-workers and the potential new biz won off the back of it. With personal projects, though, there is no validation that what you are doing is good or even correct. But then, every now and then, a little trickle of priase comes in. Be it a tweet or someone &lt;a href="https://www.buymeacoffee.com/mikestreety"&gt;buying me a coffee&lt;/a&gt;. They are rare but they are reminders that people read (and learn from) things I write.&lt;/p&gt;

&lt;p&gt;So please excuse me if my posts feel guarded, if they seem simplistic or they're not "pushing the boundaries". Despite blogging for 11 years I still have trust issues with my words.&lt;/p&gt;

</description>
      <category>impostersyndrome</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Why should you consider Cloudflare for your website?</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Mon, 20 Apr 2020 08:47:00 +0000</pubDate>
      <link>https://dev.to/liquidlight/why-should-you-consider-cloudflare-for-your-website-5b2n</link>
      <guid>https://dev.to/liquidlight/why-should-you-consider-cloudflare-for-your-website-5b2n</guid>
      <description>&lt;p&gt;Cloudflare is a service which, among other things, provides a CDN (Content Delivery Network), firewall, and performance layer for your website. It has plenty of paid upgrades and features and is a developer’s dream, but what advantage does it have for you to put your website “behind” Cloudflare and how does it work?&lt;/p&gt;

&lt;h2&gt;
  
  
  Asset caching
&lt;/h2&gt;

&lt;p&gt;Cloudflare has some mega servers which cache your static assets for you, once they have been requested once. This has a huge benefit of saving your server load and bandwidth. With a non-cloudflare’d server, if a user requests a web page which features 10 images, the browser has to make 10 additional requests to your server. If each image is 1mb, that would be an extra 10mb your server would have to find, process and serve. This, multiplied by each user on your website, is more load and takes up valuable resources which could impact performance for other users.&lt;/p&gt;

&lt;p&gt;With Cloudflare, they do Asset Caching. Once each image has been served once, they store a copy on their servers. When the second user requests the same image, Cloudflare serves it up from their servers, meaning your server has more time to do the important things - like processing that transaction or sending that email. It also helps your website performance, as many hands make light work.&lt;/p&gt;

&lt;p&gt;Below is a screenshot from one of our client’s Cloudflare accounts, showing statistics from the past week - that’s a 90% caching rate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---jieklxW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.liquidlight.co.uk/fileadmin/_processed_/f/1/csm_image1_aeb02ebf0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---jieklxW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.liquidlight.co.uk/fileadmin/_processed_/f/1/csm_image1_aeb02ebf0a.png" alt="alt text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to take it one step further, you can cache static pages as well. Pages without contact forms or dynamic content are ideal candidates for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worldwide CDN
&lt;/h2&gt;

&lt;p&gt;Along with the asset caching, Cloudflare has a worldwide Content Delivery Network. With this, your cached asset gets replicated across servers in over 200 cities worldwide. When your user requests an asset, rather than serve it from your server which is in one location, Cloudflare will serve up the asset from the nearest server, meaning your website loads fast all over the world, not just near the country your server is in.&lt;/p&gt;

&lt;p&gt;When we put Liquid Light behind Cloudflare our worldwide average total time for loading the homepage went from 1.121 seconds to 0.357 seconds. This was a combination of the CDN and the caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firewall &amp;amp; DDOS Protection
&lt;/h2&gt;

&lt;p&gt;Along with the caching and CDN, Cloudflare helps protect your site against brute-force attacks and threats against your website. Cloudflare has the advantage of serving over 12 million websites and so can identify malicious bots and users more easily than any operating system firewall. If Cloudflare thinks you are suspicious, it can show a challenge which allows “normal” users to still access your site. These firewall and security measures are ever evolving and being developed, meaning every day your website is becoming more secure and protected.&lt;/p&gt;

&lt;p&gt;Security - we don’t get logins to your domains or Cloudflare&lt;br&gt;
On the topic of security, when using Cloudflare you can share your account with us without sharing your login details. More and more companies are heading in this direction recently and Cloudflare is no exception. You set up the account in your name with your email address and then you can allow us access to your account. This means you don’t have to share your password and can revoke access at any time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nice DNS management
&lt;/h2&gt;

&lt;p&gt;This is a slightly nerdy one, but the DNS management within Cloudflare is excellent, and can be controlled via an API. DNS records are like driving directions for your browser. When you enter your domain name, the DNS records tell your browser where to go to get to their destination. This doesn’t normally affect day-to-day operation, but being able to update your records easily saves a lot of time should you need to change your server or add a verification record (usually Google Search Console requires this).&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced features
&lt;/h2&gt;

&lt;p&gt;All of the features above come with the free (yes, free) Cloudflare account. If you decide to upgrade to Pro (or even Business), this unlocks a whole host of features including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image optimisation&lt;/strong&gt; - serving images up in a next-gen image format if the user’s browser supports it (smaller, faster-loading images)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next-gen delivery&lt;/strong&gt; - You can enable HTTP/2, which optimises the speed and order the webpage loads (more about this on the Cloudflare blog)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More page rules&lt;/strong&gt; - Page rules allow you to cache, redirect or do a multitude of other things to pages and urls before the user even reaches your server. With the free account you get 3, as you upgrade you unlock more of these&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better security&lt;/strong&gt; - From the Pro package up, you get a more enhanced firewall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a side-note, if you look at their pricing page, you will also notice that at least Pro is recommended for "mission-critical" projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare workers
&lt;/h2&gt;

&lt;p&gt;Cloudflare workers are not directly part of the standard Cloudflare service per se, they are a separate offering, but by using Cloudflare for your DNS allows you to utilise them to their full potential. They allow you to run your own apps on the Cloudflare servers or modify intercept requests for a page or image to add functionality. &lt;/p&gt;

&lt;p&gt;For one of our clients, we have used Cloudflare workers to prevent an extra request to their server. At the time of writing we have saved their server from having to deal with over 3.5 million requests in the last week.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>performance</category>
    </item>
    <item>
      <title>What are the different SSL modes on Cloudflare?</title>
      <dc:creator>Mike Street</dc:creator>
      <pubDate>Sun, 19 Apr 2020 06:30:00 +0000</pubDate>
      <link>https://dev.to/mikestreety/what-are-the-different-ssl-modes-on-cloudflare-5163</link>
      <guid>https://dev.to/mikestreety/what-are-the-different-ssl-modes-on-cloudflare-5163</guid>
      <description>&lt;p&gt;Cloudflare offers free SSL certificates as part of its service, even on the free tier. There are 4 options when in the SSL/TLS section of Cloudflare and, depending on the selected option, will determine which port your server will be required to serve traffic. By default on any web server (unless specifically changed otherwise):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unencrypted traffic via &lt;code&gt;http&lt;/code&gt; is served over port &lt;strong&gt;80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Encrypted traffic via &lt;code&gt;https&lt;/code&gt; is served over port &lt;strong&gt;443&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cloudflare SSL options
&lt;/h2&gt;

&lt;p&gt;Cloudflare provides an SSL to allow you to provide a &lt;code&gt;https&lt;/code&gt; URL, without needing to provide an SSL certificate yourself or serve your site up over port 443.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Off (not secure)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This uses port 80 on your server to serve the files and uses the &lt;code&gt;http&lt;/code&gt; protocol in your browser URL bar - something which Chrome (and other browsers) will flag as insecure&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flexible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the outside, this uses a &lt;code&gt;https&lt;/code&gt; URL, however the SSL is only between the user and Cloudflare. From Cloudflare, the traffic is unencrypted and served over port 80 (This one took me a while to realise - I've lost many hours wondering why, when serving over port 443, my content wasn't appearing). Some CMS's require the server to be running on port 443 if the URL starts with HTTPS - that's where the next one comes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full uses an SSL between both the user &amp;amp; Cloudflare and Cloudlfare &amp;amp; the server. Your server serves files over port 443 and must have an SSL certificate installed to operate. However, this SSL certificate can be self-signed or invalid. Cloudflare does not verify the certificate but trusts it is ok. Cloudflare themselves offer a self-signed "Origin" certificate for you to download and install on your server, which allows you to use Full without having to go through the hassle of generating a certificate yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full (strict)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full (strict) mode in Cloudflare is the same as Full, but the certificate needs to be valid and signed by a proper authority, for example, Let's Encrypt. This is the most secure but requires a real SSL certificate to operate.&lt;/p&gt;

&lt;p&gt;In most cases Full will suffice, however, I wanted to run through generating a wildcard SSL certificate with a DNS provider which provides an API.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
