<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>James King</title>
        <link>https://james.ripixel.co.uk/</link>
        <description>Feed for thoughts articles</description>
        <lastBuildDate>Sun, 12 Jul 2026 20:55:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, James King</copyright>
        <item>
            <title><![CDATA[Enabling Dark Mode]]></title>
            <link>/thoughts/2023-01-04_Enabling-dark-mode.html</link>
            <guid isPermaLink="false">/thoughts/2023-01-04_Enabling-dark-mode.html</guid>
            <pubDate>Wed, 04 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Ever thought "ewww this site is so bright?!" and then searched for the "dark mode" button? I'm one of those people too, and despite my love of bright blocks of colour I decided that implementing a dark mode switch would be a good idea.]]></description>
            <content:encoded><![CDATA[Ever thought "ewww this site is so bright?!" and then searched for the "dark mode" button? I'm one of those people too, and despite my love of bright blocks of colour I decided that implementing a dark mode switch would be a good idea.

Additionally, I hate it when things _default_ to light mode, even though my machine is set to dark mode! So in this little guide, I'll walk you through how I implemented a machine-aware dark mode for this site.

## What's the plan?

As this site is very simple and static (no server-side rendering here, just good ol' fashioned HTML and CSS), I'll need to implement a client-side-only dark mode. This means I'll need to:

 - Figure out how I want to "apply" a dark mode across the site using CSS
 - Figure out the JavaScript to enable this dark mode

## How to apply dark mode in HTML

This one's quite simple, and I didn't want to muck around too much. As my site is powered by basic CSS (no CSS-in-JS or anything of that ilk), I can just apply a class to the `<body>` element, and then select on that!

So if it's in light mode, the body will just be:

```html
<body>
  <h1>Content</h1>
</body>
```

But in dark mode, it'll be:

```html
<body class="dark">
  <h1>Content</h1>
</body>
```

That means in CSS I can then do things like:

```css
// light mode is default
body {
  background: white;
}

h1 {
  color: black;
}

// dark mode has a more specific selector, so will override the light mode
body.dark {
  background: black;
}

.dark h1 {
  color: white;
}
```

## How to define the dark CSS styles

My site has a single `styles.css` that defines the site's theme, along with a `reset.css` for resetting browser defaults to 0 values and `syntax_highlight.css` for my syntax block styling. All these live in a `styles` folder, and my build process merges them all together and minifies them.

So, my plan is to copy/paste my `styles.css` to `styles_dark.css`, and remove any properties that aren't about colours. It was a slightly tedious process, but only took me 15 minutes to isolate all the colour values in my CSS. Now during development (before I've created the JavaScript), I manually added the `dark` class to my body element, and ran the site in development mode.

Now I've got a file with just the colours, I prepend the `.dark` class selector to the beginning of each of them. Because it's a standard CSS file, I select all instances of `{` using VS Code `CTRL/CMD + D`, then hit the `Home` key to take me to the beginning of each line, and smack in `.dark` - now all my `styles_dark.css` selectors are prefixed with the correct selector - neat!

With my site running in development mode and the body being hard-coded to be `class="dark"`, I just modified all the values until I was happy with the darker theme.

If this sounds like a lot of manual steps, it is! But only because my site is very, _very_ simplistic in its implementation for maximum speed/ease of maintenance.

## How to flip between light and dark mode

### Simple functionality

Now I've decided on adding the `dark` class to my `<body>` tag, I can write some JavaScript to control this functionality. To begin with, I just added a button to the top of my home page (who cares about styling right now), and some simple JavaScript:

```html
<!-- HTML -->

<!-- At the top of my home page -->
<button onclick="switchMode()">Switch dark/light mode</button>

<!-- Just before the closing body tag -->
<script>
  var darkMode = false;
  function switchMode() {
    if (darkMode) {
      darkMode = false;
      document.body.classList.remove('dark');
    } else {
      darkMode = true;
      document.body.classList.add('dark');
    }
  }
</script>
```

Hooray! Now the site switches between light and dark mode as expected.

### Remembering preference

Now we're switching between modes, but it doesn't remember my last mode on page refresh! Let's use local storage to remember our value:

```javascript
// retrieve from local storage
var darkMode = window.localStorage.getItem('darkMode') || 'false';
if (darkMode === 'true') {
  document.body.classList.add('dark');
}

// enable switching functionality
function switchMode() {
  if (darkMode === 'true') {
    darkMode = 'false';
    document.body.classList.remove('dark');
  } else {
    darkMode = 'true';
    document.body.classList.add('dark');
  }
  window.localStorage.setItem('darkMode', darkMode);
}
```

Note that we have to use string versions of `true` and `false`, because local storage _always_ stores things as strings. Now our site remembers our preference, defaulting to light mode.

### Honouring system preference

This is great, but if the site is being viewed on a browser/machine with dark mode enabled system-wide, we should honour that too by default:

```javascript
// detect browser setup
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !window.localStorage.getItem('darkMode')) {
  window.localStorage.setItem('darkMode', 'true');
}

// retrieve from local storage
var darkMode = window.localStorage.getItem('darkMode') || 'false';
if (darkMode === 'true') {
  document.body.classList.add('dark');
}

// enable switching functionality
function switchMode() {
  if (darkMode === 'true') {
    darkMode = 'false';
    document.body.classList.remove('dark');
  } else {
    darkMode = 'true';
    document.body.classList.add('dark');
  }
  window.localStorage.setItem('darkMode', darkMode);
}
```

There are some caveats to this method (it's supported only in modern browsers), but it's good enough to cover 95% of users of the web, and I would suspect almost 100% of viewers of my site (due to the target audience). Again, it will default to light mode if it can't use `window.matchMedia`, so no harm no foul.

## Pretty SVG button

Finally, I updated the styling of my boring-standard button to be something snazzier, using a nice SVG of a filled or hollow sun:

```html
<section class="darkmode">
  <button onclick="switchMode()">
    <svg class="lightSun" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 48 48">
      <g id="Layer_2" data-name="Layer 2">
        <g id="invisible_box" data-name="invisible box">
          <rect width="48" height="48" fill="none" />
        </g>
        <g id="Q3_icons" data-name="Q3 icons">
          <g>
            <path d="M24,10a2,2,0,0,0,2-2V4a2,2,0,0,0-4,0V8A2,2,0,0,0,24,10Z" />
            <path d="M24,38a2,2,0,0,0-2,2v4a2,2,0,0,0,4,0V40A2,2,0,0,0,24,38Z" />
            <path d="M36.7,14.1l2.9-2.8a2.3,2.3,0,0,0,0-2.9,2.3,2.3,0,0,0-2.9,0l-2.8,2.9a2,2,0,1,0,2.8,2.8Z" />
            <path d="M11.3,33.9,8.4,36.7a2.3,2.3,0,0,0,0,2.9,2.3,2.3,0,0,0,2.9,0l2.8-2.9a2,2,0,1,0-2.8-2.8Z" />
            <path d="M44,22H40a2,2,0,0,0,0,4h4a2,2,0,0,0,0-4Z" />
            <path d="M10,24a2,2,0,0,0-2-2H4a2,2,0,0,0,0,4H8A2,2,0,0,0,10,24Z" />
            <path d="M36.7,33.9a2,2,0,1,0-2.8,2.8l2.8,2.9a2.1,2.1,0,1,0,2.9-2.9Z" />
            <path d="M11.3,14.1a2,2,0,0,0,2.8-2.8L11.3,8.4a2.3,2.3,0,0,0-2.9,0,2.3,2.3,0,0,0,0,2.9Z" />
            <path d="M24,14A10,10,0,1,0,34,24,10,10,0,0,0,24,14Zm0,16a6,6,0,1,1,6-6A6,6,0,0,1,24,30Z" />
          </g>
        </g>
      </g>
    </svg>
    <svg class="darkSun" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 48 48">
      <g id="Layer_2" data-name="Layer 2">
        <g id="invisible_box" data-name="invisible box">
          <rect width="48" height="48" fill="none" />
        </g>
        <g id="Q3_icons" data-name="Q3 icons">
          <g>
            <path d="M24,10a2,2,0,0,0,2-2V4a2,2,0,0,0-4,0V8A2,2,0,0,0,24,10Z" />
            <path d="M24,38a2,2,0,0,0-2,2v4a2,2,0,0,0,4,0V40A2,2,0,0,0,24,38Z" />
            <path d="M36.7,14.1l2.9-2.8a2.3,2.3,0,0,0,0-2.9,2.3,2.3,0,0,0-2.9,0l-2.8,2.9a2,2,0,1,0,2.8,2.8Z" />
            <path d="M11.3,33.9,8.4,36.7a2.3,2.3,0,0,0,0,2.9,2.3,2.3,0,0,0,2.9,0l2.8-2.9a2,2,0,1,0-2.8-2.8Z" />
            <path d="M44,22H40a2,2,0,0,0,0,4h4a2,2,0,0,0,0-4Z" />
            <path d="M10,24a2,2,0,0,0-2-2H4a2,2,0,0,0,0,4H8A2,2,0,0,0,10,24Z" />
            <path d="M36.7,33.9a2,2,0,1,0-2.8,2.8l2.8,2.9a2.1,2.1,0,1,0,2.9-2.9Z" />
            <path d="M11.3,14.1a2,2,0,0,0,2.8-2.8L11.3,8.4a2.3,2.3,0,0,0-2.9,0,2.3,2.3,0,0,0,0,2.9Z" />
            <path d="M24,14A10,10,0,1,0,34,24,10,10,0,0,0,24,14Z" />
          </g>
        </g>
      </g>
    </svg>
  </button>
</section>
```

And I use CSS to show or hide the appropriate SVG:

```css
// default to showing darkSun (ie, light mode)
.lightSun {
  display: none;
}

// show lightSun and hide darkSun in dark mode
.dark .lightSun {
  display: block;
  fill: #fff;
}
.dark .darkSun {
  display: none;
}
```

Now I just add this code to all my pages, popping the JavaScript in my `footer.html` partial, and the SVG button to my `header.html` partial - now it's on every page!

## This isn't perfect

This is by no means a perfect implementation - the biggest issue is that there's a "flash of unstyled content" when the page loads and it runs the JavaScript to detect if you have dark mode on, and _then_ applies the `dark` class to the `<body>` element. However, for this _incredibly basic site_ it happens in ~2ms, so I think it's a trade-off worth making for the utter simplicity of the implementation.

You can check out all the eventual code for this in this website's repository:
 - [darkmode.html partial](https://github.com/ripixel/ripixel-website/blob/master/partials/darkmode.html) (for the switching button, included on every page)
 - [footer.html partial](https://github.com/ripixel/ripixel-website/blob/master/partials/footer.html#L30) (for the switching JavaScript)
 - [styles_dark.css](https://github.com/ripixel/ripixel-website/blob/master/assets/styles/styles_dark.css) (the dark mode specific styles)

Go click that sun in the top-right hand corner of the site and let me know what you think!
]]></content:encoded>
            <author>ripixel+feed@gmail.com (James King)</author>
        </item>
        <item>
            <title><![CDATA[Deploying To Firebase Preview Channels Using CircleCI And GitHub]]></title>
            <link>/thoughts/2023-01-02_Deploying-to-Firebase-Preview-Channels-using-CircleCI-and-GitHub.html</link>
            <guid isPermaLink="false">/thoughts/2023-01-02_Deploying-to-Firebase-Preview-Channels-using-CircleCI-and-GitHub.html</guid>
            <pubDate>Mon, 02 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[If you use Firebase to host your website, you may have seen the new Preview Channels functionality has entered Beta and wondered - what on earth is that?]]></description>
            <content:encoded><![CDATA[If you use [Firebase](https://firebase.google.com/) to host your website, you may have seen the new [Preview Channels](https://firebase.google.com/docs/hosting/test-preview-deploy?authuser=0&hl=en#preview-channels) functionality has entered Beta and wondered - what on earth is that?

I've recently been using [Renovate](https://github.com/renovatebot/renovate) to automatically handle dependency updates, and wanted a way to preview the automatically proposed changes without having to merge into staging and do that dance. Preview channels to the rescue!

This guide combines a few ideas:
 - Setting up preview channels
 - Grabbing the relevant URL for the just-deployed preview channel
 - Regex shenanigans
 - Posting a comment to a relevant GitHub PR

## What is Firebase?

Let's start at the beginning - Firebase is Google's free-to-use (up to a point) suite of Web application tools. Most notably (along with Firestore) is its hosting capability, which means given a set of files you can deploy a static application incredibly easily (note: there are also cloud functions and edge services to make it not static, but that's outside the realms of this post).

## How do you get set up with Firebase?

As a quick start, you can't get better than the [Get started with Firebase Hosting](https://firebase.google.com/docs/hosting/quickstart?authuser=0&hl=en) page from Google which explains all the basics of getting set up with Firebase Hosting, that I won't repeat here as there's nothing I can add!

## What is CircleCI?

[CircleCI](https://circleci.com/) is deployment pipeline (aka CI/CD) platform, which is also free-to-use up to a point (with very generous allowances for Open Source projects), and is my pipeline of choice due to its usage in Enterprise spaces, which means using it in personal projects grants me experience with things I do for "proper work".

It also helps that it's fantastically configurable, fast, and y'know... free!

## How do you get set up with CircleCI?

CircleCI has a great [Getting started](https://circleci.com/docs/getting-started/) guide which should get you through the initial hoops of getting your repository building from GitHub, with some great initial templates for whatever your application needs.

This website uses it, so if you want to just see the output and you're confident with Continuous Deployment pipelines, go check out its [CircleCI config](https://github.com/ripixel/ripixel-website/blob/master/.circleci/config.yml).

## What are Firebase Preview Channels?

Firebase preview channels create a temporary version of your hosted application with some identifier available so you can "preview" any proposed changes. For example with this website, I build any PRs I raise on GitHub to a preview channel so I can check everything is working as expected and nothing has broken silently in the build process.

So, when I raise a PR CircleCI builds it, and asks Firebase to deploy it to a preview channel. I then post this URL back to GitHub so I can see it for real!

![screenshot of github comment on a pull request](/deploying_to_firebase_gh_comment.png "Look ma, a Github comment!")

## How to configure Firebase preview channels?

So now you've followed the [Get started with Firebase Hosting](https://firebase.google.com/docs/hosting/quickstart?authuser=0&hl=en) and [Getting started [with CircleCI]](https://circleci.com/docs/getting-started/) guides, and you've got a deployment pipeline for your application - well done! Now let's do something cool.

### Configure your CI/CD

Your CI/CD of choice should have a capability to ensure some steps only get run under certain conditions. For example on CircleCI for this very website, I have:

```yaml
# YAML

# Jobs not listed for brevity

workflows:
  build_and_deploy:
    jobs:
      - install_deps
      - lint:
          requires:
            - install_deps
      - build_dev:
          filters:
            branches:
              ignore: master
          requires:
            - install_deps
      - build_prod_with_version:
          filters:
            branches:
              only: master
          requires:
            - install_deps
      - deploy_to_firebase_prod:
          filters:
            branches:
              only: master
          requires:
            - lint
            - build_prod_with_version
      - deploy_to_firebase_preview:
          filters:
            branches:
              ignore:
                - master
                - staging
          requires:
            - lint
            - build_dev
      - deploy_to_firebase_stg:
          filters:
            branches:
              only: staging
          requires:
            - lint
            - build_dev
```

This config means that if I push to these different branches, then these jobs run:
  + push to `master`:
    - `install_deps`
    - `lint`
    - `build_prod_with_version`
    - `deploy_to_firebase_prod`
  + push to `staging`:
    - `install_deps`
    - `lint`
    - `build_dev`
    - `deploy_to_firebase_stg`
  + push to any other branch:
    - `install_deps`
    - `lint`
    - `build_dev`
    - `deploy_to_firebase_preview`

As an example of a non-preview deploy, the `deploy_to_firebase_prod` step is super simple:

```yaml
#YAML

jobs:
  deploy_to_firebase_prod:
    executor: node-project
    steps:
      - checkout
      - restore_cache: # special step to restore the dependency cache
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - restore_cache: # restore the /public folder and associated deploy files
          key: deploy-cache-{{ .Environment.CIRCLE_WORKFLOW_ID }}
      - run:
          name: Deploy to Firebase Production Hosting
          command: ./node_modules/.bin/firebase deploy --token "$FIREBASE_TOKEN" --only hosting:production
```

First off this step restores some caches from the `install_deps` and `build_prod_with_version` steps, before attempting to deploy to Firebase. You can see that I have to pass in `--token "$FIREBASE_TOKEN"` to authenticate (so I've added `FIREBASE_TOKEN` as an environment variable for the pipeline), as well as `--only hosting:production` to let Firebase know which platform I want to deploy to (as I have `production` and `staging` configured).

I also prefer to use the project-installed version of firebase rather than do any `npm i -g firebase-tools` shenanigans, as it keeps the firebase version locked with the rest of the project, which prevents unexpected problems if firebase were to upgrade unexpectedly.

### Deploying to Firebase Preview channel

So you're now deploying to Firebase for your "normal" deployments - production, staging, whatever. But let's get preview channels enabled! Let's see what a simple step might look like:

```yaml
jobs:
  deploy_to_firebase_preview:
    executor: node-project
    steps:
      - checkout
      - restore_cache: # special step to restore the dependency cache
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - restore_cache: # restore the /public folder and associated deploy files
          key: deploy-cache-{{ .Environment.CIRCLE_WORKFLOW_ID }}
      - run:
          name: Deploy to Firebase Preview Channel
          command: ./node_modules/.bin/firebase hosting:channel:deploy $CIRCLE_BRANCH --token "$FIREBASE_TOKEN"
```

Great, we've now got preview channels deploying - it really is that easy! As the "unique ID" we're using the `$CIRCLE_BRANCH` so that we don't create endless preview channels if the same branch gets updated. Note that you don't need the `--only hosting:x` parameter, as preview channels are _always_ only for hosting.

### Posting a comment back to GitHub

But what if we want to post a GitHub comment on the PR that accompanies the branch? Then as an extra `run` step we could add:

```yaml
      - run:
          name: Post Github PR Comment
          command: |
            sudo apt-get install jq

            channels=$(./node_modules/.bin/firebase hosting:channel:list)
            circle_branch_replaced=$(echo $CIRCLE_BRANCH | sed "s/\//-/")
            regex='(https:\/\/[a-z0-9-]*--'"${circle_branch_replaced:0:39}"'-[a-z0-9-]*.web.app)'
            [[ $channels =~ $regex ]] && url=${BASH_REMATCH[0]}

            if [ $(echo $url | jq length) -eq 0]; then
              url="Unable to get URL - check Firebase console"
            fi

            pr_response=$(curl --location --request GET "https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls?head=$CIRCLE_PROJECT_USERNAME:$CIRCLE_BRANCH&state=open" \
            -u $GH_USER:$GH_TOKEN)

            if [ $(echo $pr_response | jq length) -eq 0 ]; then
              echo "No PR found to update"
            else
              pr_comment_url=$(echo $pr_response | jq -r ".[]._links.comments.href")
            fi

            curl --location --request POST "$pr_comment_url" \
            -u $GH_USER:$GH_TOKEN \
            --header 'Content-Type: application/json' \
            --data-raw '{"body": "Successfully deployed to Firebase preview channel! Available at: '"$url"'"}'
```

This step does a few things, and begins with some set up and variable assigning.

`sudo apt-get install jq` installs the `jq` package to enable some functions we want to use in the script.

`channels=$(./node_modules/.bin/firebase hosting:channel:list)` assigns a variable called `$channels` with a value of all the different preview channels that are currently live that Firebase is tracking. Assuming this command is run after a successful `firebase hosting:channel:deploy` run, this will include the most recently-deployed channel.

`circle_branch_replaced=$(echo $CIRCLE_BRANCH | sed "s/\//-/")` is replacing any forward slashes found in `$CIRCLE_BRANCH` with dashes, just like firebase will do if you pass in anything with slashes.

`regex='(https:\/\/[a-z0-9-]*--'"${circle_branch_replaced:0:39}"'-[a-z0-9-]*.web.app)'` creates a regex that looks for the URL that looks like the channel you've just deployed. Firebase also only uses the first 40 characters of the ID (which is what `"${circle_branch_replaced:0:39}"` is doing). So our regex ends up something like:

```
(https:\/\/[a-z0-9-]*--mybranch-something-[a-z0-9-]*.web.app)
```

Don't forget the parenthesis around it so you create a regex capture group!

`[[ $channels =~ $regex ]] && url=${BASH_REMATCH[0]}` uses the `$channels` and `$regex` variables we've defined above, and runs the regex against the channel list. It then assigns a variable called `$url` with the matched URL in a capture group.

The `if [ $(echo $url | jq length) -eq 0]; then` statement checks that we've actually found a URL by asserting the length of `$url` is greater than zero - if it's not, we assign an error message to the `$url` variable.

Next up, we assign a curl target to the variable `$pr_response`. For this we need to create two environment variables for our pipeline: `GH_USER` and `GH_TOKEN`. `GH_USER` is your GitHub username, and `GH_TOKEN` is a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with read/write permissions for pull requests on the associated repository. We don't need to worry about any of those variables used prefixed with `$CIRCLE_` because they're all available to us for free!

We then check if we found a relevant PR using the `if [ $(echo $pr_response | jq length) -eq 0 ]; then` block, using `jq` to check the length as before. If we find one, we assign `$pr_comment_url` the URL we need to hit to add a comment.

Finally, we perform the `curl` request to add the comment, using:

```bash
curl --location --request POST "$pr_comment_url" \
  -u $GH_USER:$GH_TOKEN \
  --header 'Content-Type: application/json' \
  --data-raw '{"body": "Successfully deployed to Firebase preview channel! Available at: '"$url"'"}'
```

Be very careful when constructing your `--data-raw` - the way variable interpolations work in bash is confusing, so be sure to terminate your strings either side of it to not have any weirdness. You can see my doing this by doing:

```bash
# Bash will concatenate strings if side-by-side
some_var=hello
result='some "text" with double-quotes in it '"$some_var"' more text'
echo $result
# some "text" with double-quotes in it hello more text
```

## Final Step Code

Et voila! Putting it all together we have:

```yaml
jobs:
  deploy_to_firebase_preview: # deploy the project to preview channel - SHOULD ONLY BE RUN ON PR BRANCHES
    executor: node-project
    steps:
      - checkout
      - restore_cache: # special step to restore the dependency cache
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - restore_cache: # restore the /public folder and associated deploy files
          key: deploy-cache-{{ .Environment.CIRCLE_WORKFLOW_ID }}
      - run:
          name: Deploy to Firebase Preview Channel
          command: ./node_modules/.bin/firebase hosting:channel:deploy $CIRCLE_BRANCH --token "$FIREBASE_TOKEN"
      - run:
          name: Post Github PR Comment
          command: |
            sudo apt-get install jq

            channels=$(./node_modules/.bin/firebase hosting:channel:list)
            circle_branch_replaced=$(echo $CIRCLE_BRANCH | sed "s/\//-/")
            regex='(https:\/\/[a-z0-9-]*--'"${circle_branch_replaced:0:39}"'-[a-z0-9-]*.web.app)'
            [[ $channels =~ $regex ]] && url=${BASH_REMATCH[0]}

            if [ $(echo $url | jq length) -eq 0]; then
              url="Unable to get URL - check Firebase console"
            fi

            pr_response=$(curl --location --request GET "https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls?head=$CIRCLE_PROJECT_USERNAME:$CIRCLE_BRANCH&state=open" \
            -u $GH_USER:$GH_TOKEN)

            if [ $(echo $pr_response | jq length) -eq 0 ]; then
              echo "No PR found to update"
            else
              pr_comment_url=$(echo $pr_response | jq -r ".[]._links.comments.href")
            fi

            curl --location --request POST "$pr_comment_url" \
            -u $GH_USER:$GH_TOKEN \
            --header 'Content-Type: application/json' \
            --data-raw '{"body": "Successfully deployed to Firebase preview channel! Available at: '"$url"'"}'
```

And now we have PRs (well, any branches that aren't `master` or `staging`) deploying to preview channels and posting back to the relevant PR with a comment to view the changes.

To see the entire build step for this website, check out its [CircleCI config](https://github.com/ripixel/ripixel-website/blob/master/.circleci/config.yml). It's not doing anything crazy - install dependencies, lint the repo, build the site, and deploy!
]]></content:encoded>
            <author>ripixel+feed@gmail.com (James King)</author>
        </item>
        <item>
            <title><![CDATA[Webmentions Are Fun]]></title>
            <link>/thoughts/2021-03-21_Webmentions-are-Fun.html</link>
            <guid isPermaLink="false">/thoughts/2021-03-21_Webmentions-are-Fun.html</guid>
            <pubDate>Sun, 21 Mar 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Ever wondered if there's some way to let people know you think their web article/content is great, but also wanted it to be automated and snazzy?]]></description>
            <content:encoded><![CDATA[Ever wondered if there's some way to let people know you think their web article/content is great, but also wanted it to be automated and snazzy?

Enter [Webmentions](https://indieweb.org/Webmention), the cool little system to notify (and receive notifications about) people you liked/reposted/commented on their stuff! You can even hook it up to Twitter, and it's completely agnostic of whatever system you use to publish your little corner of the web.

## So how do they work?

Simply put, you have some service that accepts a request that you've been mentioned, and you yourself poke other services to let other people know that you've been mentioned.

That second bit sounds like it could be an absolute nightmare to figure out right? Wrong! The [Webmentions Spec](https://www.w3.org/TR/webmention/) has thought of this, and all you do is add some tags to your head to let people know whe to send mention requests (more on that in the [tutorial](#justshowmehowtodoitimhereforthecode) section).

So when you want to mention someone, you scrape the page you're mentioning, find the `webmention` link tag and fire your request there - easy!

But how about when you want to find out what you've been mentioned in? Well that depends on what service you use to accept your mention requests, but generally you'll just poke an API endpoint to retrieve them, et voila! You have all your webmentions, and you can do with them what you will (for example, I shove them at the end of an article).

## Just show me how to do it, I'm here for the code!

Ok, so first things first, we've got to decide how to accept webmentions. For this entire guide, I'll be effectively using the process that [Luke Bonaccorsi](https://lukeb.co.uk/) noted in his excellent article ["No comment: Adding Webmentions to my site"](https://lukeb.co.uk/blog/2021/03/15/no-comment-adding-webmentions-to-my-site/#webmentions) (and because I've mentioned his article in mine, he'll get a webmention ping - neat!). He uses Eleventy and thus a slightly different integration path (he even made a build-plugin for it to make all this process super easy for everyone using Eleventy), but I've shamelessly copy/pasted his code here. Go give him a yell on Twitter [@CodeFoodPixels](https://twitter.com/CodeFoodPixels) telling him how great he is!

Ok, with that gushing out the way, we're going to use [webmention.io](https://webmention.io/) to receive mentions for us, and [webmention.app](https://webmention.app/) to send mentions to other people.

### Accepting webmentions

To get started accepting webmentions with .io, we've got to let it know we are responsible for that site. So you need to do two things.

Firstly, ensure that your website is on the profile of one of the sign on mechanisms they support, so for me that was Twitter or GitHub. Then you need to add `rel="me"` to a link to that profile on your website, for example:

```html
<!-- HTML -->

<a href="https://github.com/your_gh_username" rel="me">GitHub</a>
<a href="https://www.twitter.com/your_twitter_handle" rel="me">Twitter</a>
```

Now when you enter your website into the sign in page of .io, it'll ask you to sign in using one of those platforms - noice.

Now you need to add some tags to your `<head>` so .io knows you want to use it:

```html
<!-- HTML -->

<link
  rel="webmention"
  href="https://webmention.io/www.your-website.com/webmention"
/>
<link rel="pingback" href="https://webmention.io/www.your-website.com/xmlrpc" />
```

Once you've done that, that's it! You're ready to accept webmentions! Any pages trying to send a webmention will see those tags, and know where to send requests.

### Rendering webmentions

So this will be different depending on how your website works, but as I'm a crazy person who wrote their own (admittedly very noddy) Static Site Generator, I had to do all this manually (code shamelessly stolen from Luke's article).

So, when I'm generating my articles, I first get all the webmentions for my site using the below script:

```typescript
// TypeScript

export const getWebmentions = async (): Promise<any[]> => {
  const url = `${WEBMENTION_BASE_URL}?domain=${DOMAIN}&token=${WEBMENTION_IO_TOKEN}&per-page=1000`;

  try {
    const res = await fetch(url);
    if (res.ok) {
      const feed = await res.json();
      return feed.children as any[];
    }
  } catch (err) {
    console.error(err);
    return [];
  }
  return [];
};
```

Ensure you define `WEBMENTION_BASE_URL`, `DOMAIN` and your `WEBMENTION_IO_TOKEN`, and you when this runs you'll now have all webmentions for your entire website!

So the next step is to filter out the webmentions you care about for each page. The following script takes all the webmentions, and filters out the ones that aren't any of the types we care about (more on that in a moment), and only for the page we care about:

```typescript
// TypeScript

export const webmentionsForPage = (
  webmentions: any[],
  page: string
): {
  likes: any[];
  reposts: any[];
  comments: any[];
} => {
  const url = new URL(
    page.replace('.html', ''),
    `https://${DOMAIN}/`
  ).toString();

  const allowedTypes = {
    likes: ['like-of'],
    reposts: ['repost-of'],
    comments: ['mention-of', 'in-reply-to'],
  };

  const clean = (entry: any) => {
    if (entry.content) {
      if (entry.content.text.length > 280) {
        entry.content.value = `${entry.content.text.substr(0, 280)}&hellip;`;
      } else {
        entry.content.value = entry.content.text;
      }
    }
    return entry;
  };

  const cleanedWebmentions = webmentions
    .filter((mention) => mention['wm-target'] === url)
    .sort(
      (a, b) =>
        new Date(b.published).getTime() - new Date(a.published).getTime()
    )
    .map(clean);

  const likes = cleanedWebmentions
    .filter((mention) => allowedTypes.likes.includes(mention['wm-property']))
    .filter((like) => like.author)
    .map((like) => like.author);

  const reposts = cleanedWebmentions
    .filter((mention) => allowedTypes.reposts.includes(mention['wm-property']))
    .filter((repost) => repost.author)
    .map((repost) => repost.author);

  const comments = cleanedWebmentions
    .filter((mention) => allowedTypes.comments.includes(mention['wm-property']))
    .filter((comment) => {
      const { author, published, content } = comment;
      return author && author.name && published && content;
    });

  return {
    likes: likes ?? [],
    reposts: reposts ?? [],
    comments: comments ?? [],
  };
};
```

You'll note that we're only caring about the `like-of`, `repost-of`, `mention-of`, and `in-reply-to` types (with those last two types lumped together as `comments`).

So for each article page I'm generating, I pass in all the webmentions and the page path, and get back an object with the `likes`, `reposts`, and `comments` keys.

Now this is where my janky SSG really makes things look complicated, but essentially my HTML files have some handlebars-esque markers where content should go and be repeated - so I take that, and replace the values with each type of mention. For example:

```html
<!-- HTML -->

<!-- START_MENTIONS -->
<main class="thoughts mentions">
  <!-- START_LIKES -->
  <div class="likes">
    <h4>{LIKES} likes</h4>
    <div class="mention-links">
      <!-- START_LIKES_REP -->
      <a
        class="item"
        href="{mention_link}"
        target="_blank"
        rel="external noopener noreferrer"
      >
        <img
          src="{mention_avatar}"
          loading="lazy"
          decoding="async"
          width="28"
          height="28"
        /><span>{mention_name}</span></a
      >
      <!-- END_LIKES_REP -->
    </div>
  </div>
  <!-- END_LIKES -->

  <!-- START_REPOSTS -->
  <div class="reposts">
    <h4>{REPOSTS} reposts</h4>
    <div class="mention-links">
      <!-- START_REPOSTS_REP -->
      <a
        class="item"
        href="{mention_link}"
        target="_blank"
        rel="external noopener noreferrer"
      >
        <img
          src="{mention_avatar}"
          loading="lazy"
          decoding="async"
          width="28"
          height="28"
        /><span>{mention_name}</span></a
      >
      <!-- END_REPOSTS_REP -->
    </div>
  </div>
  <!-- END_REPOSTS -->

  <!-- START_COMMENTS -->
  <div class="comments">
    <h4>{COMMENTS} comments</h4>
    <div class="mention-links">
      <!-- START_COMMENTS_REP -->
      <div class="comment">
        <a
          class="item"
          href="{mention_link}"
          target="_blank"
          rel="external noopener noreferrer"
        >
          <img
            src="{mention_avatar}"
            loading="lazy"
            decoding="async"
            width="28"
            height="28"
          /><span>{mention_name}</span></a
        >
        <p>{comment}</p>
        <a
          class="item comment-link"
          href="{comment_link}"
          target="_blank"
          rel="external noopener noreferrer"
          ><span>View</span></a
        >
      </div>
      <!-- END_COMMENTS_REP -->
    </div>
  </div>
  <!-- END_COMMENTS -->
</main>
<!-- END_MENTIONS -->
```

Those `<!-- START_X -->` and `<!-- END_X -->` blocks are for my SSG script to see which bits should be repeated. The script that then replaces them looks something like (omitting the SSG janky bits):

```typescript
// TypeScript

const generateWebmentionBlock = (
  tag: 'COMMENTS' | 'LIKES' | 'REPOSTS',
  content: string,
  mentions: any[]
): string => {
  const isComment = tag === 'COMMENTS';

  // I'm omitting the whole bit around grabbing the repeating blocks from the content,
  // but that happens here if I were to subject you to my horrible hacky "Oh that'll
  // do" coding for personal stuff. You can always see the source code for this site
  // on my GitHub if you really want to see how bad it is...!

  return mentions
    .map((mention) => {
      return content
        .replace(
          /{mention_link}/g,
          !isComment ? mention.url : mention.author.url
        )
        .replace(
          /{mention_avatar}/g,
          (!isComment ? mention.photo : mention.author.photo) ??
            '/default_avatar.png'
        )
        .replace(
          /{mention_name}/g,
          !isComment ? mention.name : mention.author.name
        )
        .replace(/{comment}/g, isComment ? mention.content.value : '')
        .replace(/{comment_link}/g, isComment ? mention.url : '');
    })
    .join('');
};
```

And that's it! Do that for every page, and every type of mention, and you've now got webmentions into your site!

### Wait, that just does it at build time - won't webmentions come in over time?

Why yes voice in my head, you're correct! This will only grab the state of webmentions whenever the build runs.

To remedy this, I've set my build job on CircleCi to run every hour to grab new mentions and the site it generates and deploys will then have any new mentions. There are ways to configure webmention.io to poke a URL when you receive a mention, but I didn't want that to trigger masses of builds if I were to go #viral - so every hour it is!

```yaml
# Yaml

workflows:
  version: 2
  hourly:
    triggers:
      - schedule:
          cron: '55 * * * *' # 5 mins to every hour it will run
          filters:
            branches:
              only:
                - master
```

### Sending webmentions

Ok so we're now accepting webmentions ourselves, and rendering those on our pages. But how do we partake in the community happiness and send webmentions ourselves? Well now we get to play with webmention.app, and you'll see it's as simple as doing a `POST` request to `https://webmention.app/check/?url=:url` whenever we publish a new article.

You can also hook it up to various things, all walked through on the excellent homepage (RSS feeds are particularly useful!) which I won't repeat here for the sake of your sanity.

### Ok, but what about Twitter integration?

That's actually the simplest bit! Using [Bridgy](https://brid.gy/) you can connect your Twitter account, and it will monitor your profile for any likes/retweets/replies for links that are webmention-able, and automatically do all the scraping and sending for you!

## Hey presto!

I love it when the web decides "hey, wouldn't it be cool if...?" and a bunch of smart people get together and make it happen. Even better when some of those smart people create free apps like webmention.io, webmention.app, and brid.gy to make it super simple for everyone else to get started!

Big thanks to Luke's [excellent article](https://lukeb.co.uk/blog/2021/03/15/no-comment-adding-webmentions-to-my-site/#webmentions) for being so easy to follow; I'm simply typing it up again for the sake of writing an article myself, and spreading the webmention love.

Go make awesome things, and who knows, maybe this article will even get some interactions!
]]></content:encoded>
            <author>ripixel+feed@gmail.com (James King)</author>
        </item>
        <item>
            <title><![CDATA[Stop Blaming Slack]]></title>
            <link>/thoughts/2020-08-16_Stop-Blaming-Slack.html</link>
            <guid isPermaLink="false">/thoughts/2020-08-16_Stop-Blaming-Slack.html</guid>
            <pubDate>Sun, 16 Aug 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[These unprecedented times have caused many companies to shift to or expand their usage of communication technologies, such as Microsoft Teams and Slack. With this expansion have come teething issues - as would be expected.]]></description>
            <content:encoded><![CDATA[These unprecedented times have caused many companies to shift to or expand their usage of communication technologies, such as Microsoft Teams and Slack. With this expansion have come teething issues - as would be expected.

What I didn't expect though is the number of articles decrying Slack specifically as mostly a terrible thing, and that it ["has made work more taxing"](https://www.ft.com/content/e036016c-39f0-4ce7-a64b-845f764d3254). To those writing these articles: stop blaming Slack.

## Slack is not a real-time communication app

Slack (and other workplace communication tools) are primarily intended to replace (or supplement) _email_. You know, that incredibly slow, old-fashioned, and unreliable mechanism. The one where you have to trawl through endless replies/forwards/"I don't think they're CC'd into this email chain" messages to find the actual information you want. Or even more accurately, to often find that most of the emails you receive you don't even need to read.

Slack steps in here, allowing companies who use it to create channels, stick all the people related to that topic (ie #team-marketing), so that anything the marketing team needs gets posted there.

Just because you've posted it however, doesn't mean that someone will respond immediately. Slack is an **asynchronous communication tool**, just like email. It just-so-happens that because it's so much easier to use, that people tend to respond more quickly. With that said though...

## Do not expect instant replies

The main complaint is that "there's a lot of interruptions", because Slack users are sending and receiving between 800 and 2,500 messages a month.

As I've noted above, just because a message has been sent does not guarantee immediate response - nor should you expect that as a user or company. Users can be busy in a meeting, or concentrating on getting some work done. Slack has an excellent "Do Not Disturb" function which when turned on, will inform other people trying to message you directly that you have notifications paused, and give you (the sender) the opportunity to override this if it's urgent.

It's important that you respect the Do Not Disturb flag, and that your employees can feel free to turn it on without fear of being hounded by unnecessary messages. I'm sure if you could wait 2 days for a reply by email, you can wait a few hours for a reply by Slack.

## Stop having loads of communication tools

Yet another issue is that "now I have to monitor email and Slack" - again, not Slack's fault - it's your company.

While it's true Slack is not a complete replacement for email (you still need to email people outside of your organisation, unless you have brought them into your Slack org via a shared channel), Slack _should_ replace **all internal email**. I should only be checking it for external communications, which I should receive a lot fewer of in the first place. If people are still sending things via email that should be via Slack, employees should feel able to say "Hey, for future communications can you pop into our Slack channel?" without fear of reprimand.

If you find yourself having to check multiple communication apps, then please, for the love of all that is holy, as an organization **pick one and ditch the others**. Be that Slack, Teams, Facebook Workplace, or whatever. You may say "Oh but the marketing team prefer X and the business ops team prefer Y", but at the end of the day it's much-of-a-muchness. You will reduce hate, workload, and stress by having a single internal communications platform.

## Why should I bother?

Anecdotally, at a previous company I was receiving ~200 emails a day - maybe 30 of which needed addressing by me. Another 50 or so were "FYI"-type CCs. The rest were garbage. Now, I'm in the Slack channels I need to be in, and I can immediately see which channels have new messages and if I've been directly mentioned, all without actually opening said messages. I can choose to address these at my own pace (if I see #super-critical-project blinking like a Christmas tree, I may answer immediately for example), and via good company practice and usage of the Do Not Disturb function, get some decent blocks of time to concentrate on tasks.

Email is slow, clunky, insecure, and out-dated. Yes it has its use as being a universal electronic communication form, but for the same reason I don't fax people anymore, you shouldn't be sending emails when much better alternatives are available.

Pick a single communication tool, champion its use, and don't expect more of your employees just because you've got a shiny new app. Stop blaming Slack; it's your processes at fault.
]]></content:encoded>
            <author>ripixel+feed@gmail.com (James King)</author>
        </item>
        <item>
            <title><![CDATA[Talk To Your Engineers]]></title>
            <link>/thoughts/2020-07-04_Talk-To-Your-Engineers.html</link>
            <guid isPermaLink="false">/thoughts/2020-07-04_Talk-To-Your-Engineers.html</guid>
            <pubDate>Sat, 04 Jul 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[We've all been there - the sales person says "Hey everyone, I've just got us a new project!". At this point all the engineers look at each other, and realise that none of them have been spoken to regarding the project.]]></description>
            <content:encoded><![CDATA[We've all been there - the sales person says "Hey everyone, I've just got us a new project!". At this point all the engineers look at each other, and realise that none of them have been spoken to regarding the project.

As one, they exclaim the hallowed word, echoed throughout time - "Fuck.".

They know what's about to happen - they're about to be asked to deliver a project that has been scoped out by the sales team, and will inevitably have bizarre requirements, unachievable deadlines, or both! "Why oh why did no one ask us about this? We would've helped!".

## "That's not their job! It's the Sales Team's job!"

Sales teams are incredible. Their ability to take client problems and figure out how their specific company's tech can help them solve it is nothing short of miraculous. The documents and proposal they create are worthy of literacy awards, and the RoI promised is nothing short of Bill-Gates-money amazing.

The problem is that unless they've spoken to someone in the engineering team, any dates or cost for that integration are **almost certainly wrong**, along with assumptions about what the underlying technologies may be, and how they fit together.

## "We don't have any sales engineers though!"

Just because you don't have any specific sales engineers (or whatever you want to call them), this doesn't mean your current engineers cannot help with the sales process. If you don't feel that they'd be good in front of clients, then fine - just take down the client requirements in as much detail as you can, and then take that to them. We engineers love figuring out how we can solve a problem, and we'll be delighted you included us in the process. You can then take your discoveries back to the client, and have a more informed conversation about what you can offer.

If you are lucky enough to have dedicated sales engineers, then amazing; get them involved as soon as you can! Put them in front of a client, and let that conversation begin.

Even if you're in the _incredibly_ rare boat of having fully-fledged engineers within your sales team, then I still urge you to get the delivery team engineers involved. Their being on the frontline of developing solutions will _always_ give them more context than those who are not.

The point is, engineers provide valuable insight into accurately estimating and understanding a project. There will be nuances no one has thought of, intricacies that escape both parties, and similarities (including problems faced) to previous solutions that are only obvious to them.

## "Engineers are there to build the solution, not sell or recommend one!"

If this is your mindset, that needs to change _right now_. If your engineers are simply code-monkeys who do what they're told, you're losing out on **so much value**. Give them the chance to input into the solution, and I guarantee the end result will be better for it.

## "What do you know? You're just some random guy!"

I'm [lucky enough to work somewhere](https://dvelp.co.uk) that takes its engineers seriously, and our engineering-first mindset is a core tenant of our value to clients. We get engineers involved as early as possible, start talking shop, and make wonderful things happen. As a happy coincidence, clients love seeing that we get engineers involved early and often - it shows dedication and commitment that we're taking their problem seriously.

So yeah - trust your engineers, and get them involved. You'll love what happens.
]]></content:encoded>
            <author>ripixel+feed@gmail.com (James King)</author>
        </item>
        <item>
            <title><![CDATA[The Importance Of Monitoring And Alerting]]></title>
            <link>/thoughts/2019-09-09_The-Importance-of-Monitoring-and-Alerting.html</link>
            <guid isPermaLink="false">/thoughts/2019-09-09_The-Importance-of-Monitoring-and-Alerting.html</guid>
            <pubDate>Mon, 09 Sep 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[We all know that monitoring and alerting are important, right? But just _how_ important do we really think that? How much do we show that in what we do?]]></description>
            <content:encoded><![CDATA[We all know that monitoring and alerting are important, right? But just _how_ important do we really think that? How much do we show that in what we do?

This is a story about an experience I had at a previous company, where I had the pleasure of being at least partially responsible for the system going down, and what we learned.

## What happened?

As some scene-setting - we had to implement a service that would be accessed anytime a user visited the site. I did the work, got it reviewed, tested, and hey-presto, got it live. More testing, ensure nothing went bang, and went home.

The next morning, I return to work, and all is well. About 3 hours in the site goes down. _Panic!_ Time to follow the steps...

1. Who deployed last? - _Not me!_
2. What big changes have we made recently? - _I did something yesterday, but everything's been fine for 24 hours, it can't be me!_
3. Check the logs - _"Cannot access the service that you implemented"_
4. _Gulp._

## How did we fix it?

For the purpose of this story, this is actually not important (although obviously the initial fix was we just rolled back to pre-my-change). What is important is _how did we get here_.

## So, how did we get here?

Oh, so many questions, so few graphs...

### The Service

First thing's first - let's have a look at the monitoring that was in place on the service I was calling:

![alt text](https://dvelp-production.s3.amazonaws.com/uploads/image/file/176/x_large_1568038815-tol_service_initial.PNG "Service - Initial")

Everything looks fine! Let's check the whole view:

![alt text](https://dvelp-production.s3.amazonaws.com/uploads/image/file/177/x_large_1568038850-tol_service_final.PNG "Service - Final")

Oh dear. So, the service was getting warmer and warmer, until it reached 100% usage and any subsequent requests stopped being served, failed, and the site calling it fell over.

### The Site

Ok, so the service got overloaded. Let's have a look at the number of calls we were making... _oh. There is no monitoring on the number of calls we were making._

Ok, let's assume we did have that monitoring in place - what is the expected number of calls?

![alt text](https://dvelp-production.s3.amazonaws.com/uploads/image/file/178/x_large_1568038873-tol_site_expected.PNG "Site Expected")

Great - now let's have a look at the actual:

![alt text](https://dvelp-production.s3.amazonaws.com/uploads/image/file/179/x_large_1568038897-tol_site_actual.PNG "Site Actual")

I think that would do it, yes. Overloading the service with around 50x the expected calls (that's already taking into account the headroom planned).

## Time to assign blame

There's a common misunderstanding with coding, in that the coder themself is to blame when things go wrong. That's not _incorrect_, but it's certainly not the whole story. Let's have a look at who else was involved in that deployment:

- The developer who created the change
- The developers who reviewed and approved the change
- The testers who missed the issue
- The engineering manager who approved the approach
- The team responsible for the service being called

So it's not so simple. Software development is a team effort, so there's never a single person to blame.

## What did we learn?

To quote the product owner:

> Why the hell didn't we know about this earlier, before it became an issue?

It's the only question that really matters in this instance. We had monitoring for the service, but no alerting set up for when it was getting too warm. We had no monitoring in place at all for the number of calls being made, and therefore no alerting.

So, some takeaways from my (traumatic) trip down memory lane about how I took down a site:

- Data is only important if you use it
- Graphs and alerts are really boring until _they save you_
- Give developers time to make "of no immediate value" things like monitoring and alerting, and make it part of any Acceptance into Service checklists

_This thought was first published on the [DVELP blog](https://dvelp.co.uk/articles/monitoring-and-alerting) - but it was still written by me!_
]]></content:encoded>
            <author>ripixel+feed@gmail.com (James King)</author>
        </item>
    </channel>
</rss>