<?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: Luc Shelton</title>
    <description>The latest articles on DEV Community by Luc Shelton (@theloveduckie).</description>
    <link>https://dev.to/theloveduckie</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%2F4440%2F38a40c81-4c51-45b1-ba38-8f3b2789a9e7.jpeg</url>
      <title>DEV Community: Luc Shelton</title>
      <link>https://dev.to/theloveduckie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/theloveduckie"/>
    <language>en</language>
    <item>
      <title>Homebrew and Anaconda: Configuring for Apple M1 and Intel Silicon</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Tue, 17 May 2022 10:24:26 +0000</pubDate>
      <link>https://dev.to/theloveduckie/homebrew-and-anaconda-configuring-for-apple-m1-and-intel-silicon-2ej5</link>
      <guid>https://dev.to/theloveduckie/homebrew-and-anaconda-configuring-for-apple-m1-and-intel-silicon-2ej5</guid>
      <description>&lt;p&gt;This is a quick article that will serve as a brain dump for how I've recently configured my &lt;code&gt;/bin/bash&lt;/code&gt; profile for using &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew Package Manager&lt;/a&gt; with both M1 and Intel Silicon (with x86 emulated using Rosetta).&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation Locations
&lt;/h2&gt;

&lt;p&gt;Depending on which CPU architecture you are installing &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew Package Manager&lt;/a&gt; with will determine where brew is installed. By default, &lt;code&gt;brew&lt;/code&gt; will be installed to the following locations.&lt;/p&gt;

&lt;h3&gt;
  
  
  M1 Silicon Installation Path
&lt;/h3&gt;

&lt;p&gt;If you are using brew on Apple M1 silicon, then the default installation script will install &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew Package Manager&lt;/a&gt; to the following paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
/opt/homebrew/bin
/opt/homebrew/Caskroom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Intel Silicon Installation Path
&lt;/h3&gt;

&lt;p&gt;If you are using &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew Package Manager&lt;/a&gt; on Intel silicon, or through Rosetta emulation, then you will notice that &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt; is installed to the following paths when you use the default installation script that is available from &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;the official homepage&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
/usr/local/bin/brew
/usr/local/Caskroom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuring Bash
&lt;/h3&gt;

&lt;p&gt;Now that we know where &lt;code&gt;brew&lt;/code&gt; is installed, we can configure bash to automatically load the relevant shell environment depending on which architecture is being used. This is useful if you have multiple copies of iTerm2 (for example), whereby one copy may be configured to launch with &lt;a href="https://support.apple.com/en-gb/HT211861" rel="noopener noreferrer"&gt;Rosetta emulation&lt;/a&gt;. If your Terminal instance (including iTerm2) is launched with Rosetta emulation, it will automatically attempt to use &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt; at the listed installation path above (under "Intel Silicon"). However, because both installations are at different paths, we need some logic to detect which architecture (emulated or not) is being actively used, so we know which shell environment to load.&lt;/p&gt;

&lt;p&gt;Refer to the logic below. It uses a command called "&lt;code&gt;uname&lt;/code&gt;" to detect which CPU architecture is actively in use. Based on the resulting value, it will then load the relevant shell environment. &lt;code&gt;x86_64&lt;/code&gt; is considered to be Intel Silicon, and &lt;code&gt;arm64&lt;/code&gt; is considered to be Apple M1.&lt;/p&gt;

&lt;h4&gt;
  
  
  Homebrew Shell Environment
&lt;/h4&gt;

&lt;p&gt;The snippet below describes the logic for automatically loading Homebrew shell environment depending on the architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
if [["$(uname -m)" == "x86_64"]]; then
    echo "Loading: Homebrew (x86)"
    eval "$(/usr/local/bin/brew shellenv)"
    CONDA_BREW_PATH=/usr/local/Caskroom/miniconda
else
    echo "Loading: Homebrew (ARM)"
    eval "$(/opt/homebrew/bin/brew shellenv)"
    CONDA_BREW_PATH=/opt/homebrew/Caskroom/miniconda
fi

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

&lt;/div&gt;



&lt;h4&gt;
  
  
  Anaconda Shell Environment
&lt;/h4&gt;

&lt;p&gt;The snippet below describes the logic (typically generated by Anaconda) for loading the relevant shell environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
if [["$(uname -m)" == "x86_64"]]; then
    # &amp;gt;&amp;gt;&amp;gt; conda initialize &amp;gt;&amp;gt;&amp;gt;
    # !! Contents within this block are managed by 'conda init' !!
    __conda_setup="$('/usr/local/Caskroom/miniforge/base/bin/conda' 'shell.bash' 'hook' 2&amp;gt; /dev/null)"
    if [$? -eq 0]; then
        eval "$__conda_setup"
    else
        if [-f "/usr/local/Caskroom/miniforge/base/etc/profile.d/conda.sh"]; then
            . "/usr/local/Caskroom/miniforge/base/etc/profile.d/conda.sh" # commented out by conda initialize
        else
            export PATH="/usr/local/Caskroom/miniforge/base/bin:$PATH" # commented out by conda initialize
        fi
    fi
    unset __conda_setup
    # &amp;lt;&amp;lt;&amp;lt; conda initialize &amp;lt;&amp;lt;&amp;lt;
elif [["$(uname -m)" == "arm64"]]; then
    # &amp;gt;&amp;gt;&amp;gt; conda initialize &amp;gt;&amp;gt;&amp;gt;
    # !! Contents within this block are managed by 'conda init' !!
    __conda_setup="$('/opt/homebrew/Caskroom/miniforge/base/bin/conda' 'shell.bash' 'hook' 2&amp;gt; /dev/null)"
    if [$? -eq 0]; then
        eval "$__conda_setup"
    else
        if [-f "/opt/homebrew/Caskroom/miniforge/base/etc/profile.d/conda.sh"]; then
            . "/opt/homebrew/Caskroom/miniforge/base/etc/profile.d/conda.sh" # commented out by conda initialize
        else
            export PATH="/opt/homebrew/Caskroom/miniforge/base/bin:$PATH" # commented out by conda initialize
        fi
    fi
    unset __conda_setup
    # &amp;lt;&amp;lt;&amp;lt; conda initialize &amp;lt;&amp;lt;&amp;lt;
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuring iTerm to Launch with Rosetta Emulation
&lt;/h3&gt;

&lt;p&gt;If you are running on Apple M1 silicon, you can still run the same applications with x86 emulation by using Rosetta. You can install Rosetta from the command-line by running the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
/usr/sbin/softwareupdate --install-rosetta --agree-to-license
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now you can create a copy of your iTerm installation, and modify the copy to launch with Rosetta emulation. Refer to the screenshots below for greater reference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FScreenshot-at-May-17-11-52-02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FScreenshot-at-May-17-11-52-02.png" title="Launching iTerm with Rosetta Emulation" alt="A picture of Applications in macOS finder, with a duplicate or copy of iTerm." width="524" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A picture of "Applications" in macOS finder, with a duplicate or copy of iTerm.&lt;/p&gt;

&lt;p&gt;With your duplicate instance of iTerm, you can then modify the properties so that it launches with Rosetta emulation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FScreenshot-at-May-17-11-50-05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FScreenshot-at-May-17-11-50-05.png" title="Modifying the properties of your iTerm duplicate" alt="After you've duplicated your iTerm installation and renamed it appropriately, you will then have to modify the properties so that it launches with Rosetta." width="538" height="1338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After you've duplicated your iTerm installation and renamed it appropriately, you will then have to modify the properties so that it launches with Rosetta.&lt;/p&gt;

&lt;p&gt;Additionally, if you want to be able to interchangeably use either Homebrew installations while using one kind of architecture over the other, then you can make use of the following aliases that can be added to your &lt;code&gt;~/.profile&lt;/code&gt; file. It should be noted that this snippet includes support for &lt;code&gt;pyenv&lt;/code&gt;, a tool that is used for managing multiple installations of Python on the same machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
alias ibrew='arch -x86_64 /usr/local/bin/brew'
alias mbrew='arch -arm64e /opt/homebrew/bin/brew'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There you have it. You should be able to use Homebrew with both architectures from the same Apple M1 machine.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Perforce: Installing and Configuring on Ubuntu</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sun, 08 May 2022 15:05:41 +0000</pubDate>
      <link>https://dev.to/theloveduckie/perforce-installing-and-configuring-on-ubuntu-4ec4</link>
      <guid>https://dev.to/theloveduckie/perforce-installing-and-configuring-on-ubuntu-4ec4</guid>
      <description>&lt;p&gt;In this article I will outline in clear simple steps what is required for installing command-line tooling for &lt;a href="https://www.perforce.com/" rel="noopener noreferrer"&gt;Perforce&lt;/a&gt; on Ubuntu. These instructions are for those who are wish to install it using &lt;a href="https://wiki.debian.org/Apt" rel="noopener noreferrer"&gt;APT&lt;/a&gt; (&lt;code&gt;apt install&lt;/code&gt; etc.), instead of manually installing it themselves by downloading and copying the binaries from the official website to your system's &lt;code&gt;/usr/bin/local&lt;/code&gt; directory. This method guarantees that you will always receive updates for as long as you have your &lt;code&gt;apt&lt;/code&gt; configured to source the &lt;a href="https://www.perforce.com/" rel="noopener noreferrer"&gt;Perforce&lt;/a&gt; repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
apt update &amp;amp;&amp;amp; apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;This section outlines some essential steps that must be done before installing Perforce using &lt;code&gt;apt&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Trusted Package Repository Keys
&lt;/h3&gt;

&lt;p&gt;The command below does the following.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Performs a HTTP GET request with no additional verbose logging.&lt;/li&gt;
&lt;li&gt;Outputs the contents of the response to standard output.&lt;/li&gt;
&lt;li&gt;Pipes the content of standard output to &lt;code&gt;gpg&lt;/code&gt;, a tool used for encryption, and decodes the file which is typically encoded in base64 (making it transmittable as text).&lt;/li&gt;
&lt;li&gt;Stores the contents of the base64 decoded file into a file path that can then be read by &lt;code&gt;apt&lt;/code&gt; when searching for packages from the Perforce repository (read next).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
wget -qO - https://package.perforce.com/perforce.pubkey | sudo gpg --dearmor -o /usr/share/keyrings/perforce-archive-keyring.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add Repository Source
&lt;/h3&gt;

&lt;p&gt;This section outlines steps required for adding the &lt;a href="https://perforce.com/" rel="noopener noreferrer"&gt;Perforce&lt;/a&gt; repository as an installable source.&lt;/p&gt;

&lt;p&gt;The following commands need to be executed so that &lt;code&gt;apt&lt;/code&gt; will be able to pull package information from the official &lt;a href="https://www.perforce.com/products/helix-core" rel="noopener noreferrer"&gt;Perforce&lt;/a&gt; package repository. You'll note that it references the public key that we downloaded in the previous section; this is used for verifying that packages installed from this source are signed with this key-pair.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
echo "deb [signed-by=/usr/share/keyrings/perforce-archive-keyring.gpg] http://package.perforce.com/apt/ubuntu $(lsb_release -c -s) release" | sudo tee /etc/apt/sources.list.d/perforce.list
sudo apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, &lt;code&gt;helix-cli&lt;/code&gt; should now be available as an installable package from the Perforce repository. You can check whether the new package repository source is available by running the following command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fperforce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fperforce.png" title="Helix Core CLI" alt="Resulting output from running the command apt search, providing that you have followed the steps correctly." width="594" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Resulting output from running the command "apt search", providing that you have followed the steps correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;This section outlines how to install &lt;a href="https://www.perforce.com/products/helix-core" rel="noopener noreferrer"&gt;Helix Core&lt;/a&gt; CLI (&lt;code&gt;p4&lt;/code&gt;), assuming that you have successfully completed the steps above.&lt;/p&gt;

&lt;p&gt;Installing &lt;code&gt;helix-cli&lt;/code&gt; is as simple as running the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
apt install helix-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything installed correctly, you should now be able to use the &lt;code&gt;p4&lt;/code&gt; command from the terminal. You can check if it is correctly installed simply by running the command "&lt;code&gt;p4&lt;/code&gt;", or by running the following command instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
command -v p4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting output should be the absolute path to where the command is made available from in the &lt;code&gt;$PATH&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;If you wish to be even more pedantic, you can run the following command which should display the status of the newly installed package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
dpkg -l helix-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>SilverStripe: Generating URL Segments for DataObjects</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sat, 23 Apr 2022 18:23:27 +0000</pubDate>
      <link>https://dev.to/theloveduckie/silverstripe-generating-url-segments-for-dataobjects-483n</link>
      <guid>https://dev.to/theloveduckie/silverstripe-generating-url-segments-for-dataobjects-483n</guid>
      <description>&lt;p&gt;As documented on the official &lt;a href="https://www.silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe website&lt;/a&gt;, you can render &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObjects&lt;/a&gt; as &lt;a href="https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1" rel="noopener noreferrer"&gt;individual pages using controller actions&lt;/a&gt;. This is fine if you're comfortable in using the ID of the &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; as part of the page URL, but if you're looking to create and use human-friendly URLs similar to that of &lt;a href="https://api.silverstripe.org/4/SilverStripe/CMS/Model/SiteTree.html" rel="noopener noreferrer"&gt;SiteTree&lt;/a&gt; pages, then you will want to implement something that generates hyphenated and sanitized URL segments instead.&lt;/p&gt;

&lt;p&gt;You can consider a "URL segment" to be a sanitized, human-friendly readable string generated from the &lt;code&gt;Title&lt;/code&gt; field of the &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; it is intended for. It doesn't contain any special URL encoding characters, and strictly only uses alpha-numeric characters. Anything that isn't alpha numeric is replaced by a hyphen.&lt;/p&gt;

&lt;p&gt;By default, these are automatically generated for pages that derive from the &lt;a href="https://api.silverstripe.org/4/SilverStripe/CMS/Model/SiteTree.html" rel="noopener noreferrer"&gt;SiteTree&lt;/a&gt; type. Unfortunately this same kind of behaviour does not exist for &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObjects&lt;/a&gt;, but this can be easily remedied by introducing a new data extension to your project and applying it to your DataObject type's list of extensions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;SilverStripe\ORM\DataExtension&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;SilverStripe\View\Parsers\URLSegmentFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;URLSegmentExtension&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DataExtension&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'URLSegment'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Varchar(255)'&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;onBeforeWrite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;onBeforeWrite&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'URLSegment'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;URLSegment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;URLSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateURLSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isInDB&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'URLSegment'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;URLSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateURLSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;URLSegment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;makeURLSegmentUnique&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;IsURLSegmentInUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$URLSegment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'URLSegment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$URLSegment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&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="nv"&gt;$items&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;makeURLSegmentUnique&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$currentURLSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;URLSegment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;IsURLSegmentInUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentURLSegment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$currentURLSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/-[0-9]+$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$currentURLSegment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;URLSegment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$currentURLSegment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;generateURLSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLSegmentFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$filteredTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$filter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$ownerClassName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;ClassName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$ownerClassName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ownerClassName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$filteredTitle&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$filteredTitle&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$filteredTitle&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'-1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$filteredTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ownerClassName&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$this-&amp;gt;ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$filteredTitle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Breakdown
&lt;/h2&gt;

&lt;p&gt;There's a bit to digest here, but the summary of what's going on in this extension is the following.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This extension registers the field "&lt;code&gt;URLSegment&lt;/code&gt;" to whichever &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; that this extension type is appended to through the &lt;code&gt;.yml&lt;/code&gt; configuration file.&lt;/li&gt;
&lt;li&gt;Each time the DataObject is published or written through from the Object-Relational Mapping system, it will automatically generate a new URL segment string if none has been generated yet.&lt;/li&gt;
&lt;li&gt;It achieves this by checking the &lt;code&gt;URLSegment&lt;/code&gt; field, and if it has not been changed or updated, then it will attempt to use the &lt;code&gt;Title&lt;/code&gt; field of the &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; to generate a string suitable for the &lt;code&gt;URLSegment&lt;/code&gt; database field.&lt;/li&gt;
&lt;li&gt;It checks recursively to see if there is an existing &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; in the database that has the generated &lt;code&gt;URLSegment&lt;/code&gt; already.&lt;/li&gt;
&lt;li&gt;If there is an existing &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; in the database with the generated &lt;code&gt;URLSegment&lt;/code&gt;, it will instead increment an integer, append it to the string, and recursively check again to see if the newly generated &lt;code&gt;URLSegment&lt;/code&gt; has already been assigned.&lt;/li&gt;
&lt;li&gt;Once it has determined that the generated string is not in use, it will then apply it to the &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt;'s field and continue with committing the data to the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Now that we have an applicable &lt;code&gt;URLSegment&lt;/code&gt; string for our &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt;, we can put it into use in a similar manner as what has already been described on &lt;a href="https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1" rel="noopener noreferrer"&gt;these pages&lt;/a&gt;. Except, in this instance we will be making use of the &lt;code&gt;URLSegment&lt;/code&gt; field for filtering (based on the &lt;code&gt;$Action&lt;/code&gt; value) instead of the ID of the &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The code for achieving this would look something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (is_numeric($params['ID'])) {
    $eventImages = EventImage::get()-&amp;gt;filter([
        'EventID' =&amp;gt; $this-&amp;gt;dataRecord-&amp;gt;ID,
        'ID' =&amp;gt; intval($params['ID'])
    ]);
} else {
    $eventImages = EventImage::get()-&amp;gt;filter([
        'EventID' =&amp;gt; $this-&amp;gt;dataRecord-&amp;gt;ID,
        'URLSegment' =&amp;gt; $params['ID']
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure that you have configured your project to use this data extension. In the example configuration file below, I am applying the extension that I've written above to the &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; that I want the URL segments to be generated for.&lt;/p&gt;

&lt;p&gt;Once the configuration file is saved, simply navigate to &lt;code&gt;/dev/build?flush=all&lt;/code&gt; on your project, and this should now automatically generate the URL segments each time you publish modifications to your &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObjects&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
Name: urlsegments
---
Portfolio\Models\TechnologyTag:
  extensions:
    - Portfolio\Extensions\URLSegmentExtension
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all there is to it. You could make further modifications if you like, including ensuring that the &lt;code&gt;URLSegment&lt;/code&gt; field for a &lt;a href="https://api.silverstripe.org/4/SilverStripe/ORM/DataObject.html" rel="noopener noreferrer"&gt;DataObject&lt;/a&gt; uses the appropriate form field when modifying it from the CMS admin.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>CSS: Loading Custom Fonts</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sat, 23 Apr 2022 13:16:25 +0000</pubDate>
      <link>https://dev.to/theloveduckie/css-loading-custom-fonts-23oo</link>
      <guid>https://dev.to/theloveduckie/css-loading-custom-fonts-23oo</guid>
      <description>&lt;p&gt;There are a number of methods you can adopt for optimizing overall page performance of your website. An area of concern for optimization can often be custom fonts. Modern websites often make use of custom fonts as part of their overall style of design, however there unfortunately comes the performance overhead in loading them. There are number of tricks you can use to ensure that you are loading custom fonts correctly using CSS.&lt;/p&gt;

&lt;p&gt;My website uses two custom fonts that were heavily inspired by the likes of &lt;a href="https://hashnode.com/" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; - a blogging platform for developers. Those fonts are &lt;a href="https://fonts.google.com/specimen/Manrope" rel="noopener noreferrer"&gt;Manrope&lt;/a&gt; and &lt;a href="https://fonts.google.com/specimen/Inter" rel="noopener noreferrer"&gt;Inter&lt;/a&gt;, which I personally think make for a great font pairing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minification with Font Awesome
&lt;/h2&gt;

&lt;p&gt;Static font assets (such as &lt;a href="https://www.w3.org/TR/WOFF2/" rel="noopener noreferrer"&gt;WOFF2&lt;/a&gt; and TTF) can't be "minified", however if you are using a icon library such as Font Awesome that ships with a custom font for rendering, then it's important to ensure that you are using minification for the CSS asset file that it also comes with (typically &lt;code&gt;fontawesome.css&lt;/code&gt;). For example, the CSS file that it ships with has classes for &lt;strong&gt;every single icon&lt;/strong&gt; that is available as part of the font glyph. This is what makes each icon capable of being rendered as part of text (using the &lt;code&gt;&amp;lt;i&amp;gt;&amp;lt;/i&amp;gt;&lt;/code&gt; tag).&lt;/p&gt;

&lt;p&gt;Depending on what your tech stack is will ultimately determine what kind of tooling you ought to use for minification. You may wish to consider using one of the following.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript and Single-Page Application Frameworks (SPAs)&lt;/li&gt;
&lt;li&gt;PHP and SilverStripe&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Preloading JavaScript Libraries
&lt;/h3&gt;

&lt;p&gt;Font Awesome ships with a supporting JavaScript library that will automatically load the correct icon as a fallback, if the user's browser does not support any of the underlying static assets (including &lt;code&gt;.ttf&lt;/code&gt;, &lt;code&gt;.eot&lt;/code&gt;, and &lt;code&gt;.woff2&lt;/code&gt;). If you want to begin loading the supporting JavaScript library asynchronously, you can use the following code snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/js/fontawesome.min.js"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"script"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;usedLaterScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;usedLaterScript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/js/fontawesome.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usedLaterScript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use WOFF2
&lt;/h2&gt;

&lt;p&gt;Typically most systems make use of the True-Type Format (TTF) for font glyphs that are used system-wide. This is perfectly fine for local usage on each system, but unfortunately comes with added baggage of irrelevant meta data used solely for system installation, when loading static assets over the web. &lt;a href="https://en.wikipedia.org/wiki/Web_Open_Font_Format" rel="noopener noreferrer"&gt;WOFF2&lt;/a&gt; was developed to address this by ensure that compression was taken into account, and that additional metadata originally intended for local system installation was removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocking Resources
&lt;/h2&gt;

&lt;p&gt;When developing web applications, you want to ensure that you defer loading non-critical resources otherwise this will heavily impact your &lt;a href="https://web.dev/first-contentful-paint/" rel="noopener noreferrer"&gt;First-Contentful Paint&lt;/a&gt; load times. "&lt;a href="https://web.dev/first-contentful-paint/" rel="noopener noreferrer"&gt;First-Contentful Paint&lt;/a&gt;" is the metric used by most web benchmarks for indicating how long it takes for the browser to load all critical static assets, in order to begin rendering elements on screen.&lt;/p&gt;

&lt;p&gt;Ideally you want to ensure that the browser is rendering your website in as little time possible, with perhaps the caveat that some styling or CSS classes will be loaded &lt;em&gt;after&lt;/em&gt; elements have begun to render to the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prioritise Loading Custom Fonts (Asynchronously)
&lt;/h3&gt;

&lt;p&gt;Custom font assets should typically ahead of most static assets on your website. To ensure that this happens, you must declare a &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag within the head of your page with the following attributes included.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/resources/themes/portfolio-devlog-theme/fonts/manrope/woff2/manrope.woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Given that the &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag can be used for various static assets (including &lt;code&gt;.css&lt;/code&gt; and &lt;code&gt;.js&lt;/code&gt; files), it's important to identify the asset's type by using the following attributes &lt;code&gt;as="font" type="font/woff2"&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prevent Invisible Text
&lt;/h3&gt;

&lt;p&gt;If you choose to load your custom font asynchronously, you still run the risk of displaying text that will appear as invisible on your page. The reasoning for is simple - the font has not loaded yet, so the browser has nothing to render the text with! Fortunately there's a convenient way around this, and all it requires is ensure that your &lt;code&gt;@font-family&lt;/code&gt; declaration in your static CSS file asset is properly declared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Manrope'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;unicode-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;000-5&lt;/span&gt;&lt;span class="n"&gt;FF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'Manrope'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sx"&gt;url('/resources/themes/portfolio-devlog-theme/fonts/manrope/woff2/manrope.woff2')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above snippet can be declared in the head of a page as part of an inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; declaration, or included as part of a static &lt;code&gt;.css&lt;/code&gt; file asset.&lt;/p&gt;

&lt;p&gt;I'm using my own website here as an example. Pay close attention to the CSS attribute &lt;code&gt;font-display&lt;/code&gt;. We're setting the value to &lt;code&gt;swap&lt;/code&gt; simply so that placeholder text (and fallback font) is used while the custom font is still being loaded (providing that it can be loaded).&lt;/p&gt;

&lt;p&gt;Quoting the exact description from &lt;a href="https://drafts.csswg.org/css-fonts-4/#valdef-font-face-font-display-swap" rel="noopener noreferrer"&gt;the W3C draft&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In other words, the browser draws the text immediately with a fallback if the font face isn’t loaded, but swaps the font face in as soon as it loads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Quoted from &lt;cite title="Source Title"&gt;official W3C draft.&lt;/cite&gt;&lt;/p&gt;

&lt;p&gt;We're also doing three other other additional things...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stating that the browser should avoid loading the custom font if it's already installed locally.&lt;/li&gt;
&lt;li&gt;Load only a certain character range of glyphs from the font (e.g. latin glyphs).&lt;/li&gt;
&lt;li&gt;Load the &lt;a href="https://www.w3.org/TR/WOFF2/" rel="noopener noreferrer"&gt;WOFF2&lt;/a&gt; version of the font (where possible), otherwise use WOFF and TTF version of the font as fallbacks if the browser does not support it.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Automatically Serve WebP Images with Silverstripe and NGINX</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sat, 12 Mar 2022 19:47:42 +0000</pubDate>
      <link>https://dev.to/theloveduckie/automatically-serve-webp-images-with-silverstripe-and-nginx-beg</link>
      <guid>https://dev.to/theloveduckie/automatically-serve-webp-images-with-silverstripe-and-nginx-beg</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Fast page load times are vital for any modern website with heavy traffic. To achieve quick response times, it often this means having to compress, minify, and cache static page assets that are served regularly on page load; including JavaScript, plain HTML, or images in particular. However, the problem with heavy compression often comes with a loss of quality in image assets, which can compromise aesthetics or styling, and detract from overall user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebP
&lt;/h2&gt;

&lt;p&gt;Fortunately there's a solution to serving &lt;em&gt;mostly&lt;/em&gt; lossless images with small payloads. Quoting from &lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;WebP's homepage.&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;WebP&lt;/a&gt; is a modern image format that provides superior lossless and lossy compression for images on the web. Using &lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;WebP&lt;/a&gt;, webmasters and web developers can create smaller, richer images that make the web faster.&lt;br&gt;
The WebP image file format is not anything terribly new and has been for a while now, however many websites have yet to implement steps for generating &lt;code&gt;.webp&lt;/code&gt; images from their existing image assets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Integrating with Silverstripe
&lt;/h2&gt;

&lt;p&gt;As a case study for this blog, I've decided to talk about how to integrate the usage of this image file format with Silverstripe. What is &lt;a href="https://www.silverstripe.org/" rel="noopener noreferrer"&gt;Silverstripe&lt;/a&gt;? For those of you who don't already know...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.silverstripe.org/" rel="noopener noreferrer"&gt;Silverstripe CMS&lt;/a&gt; is a free and open source content management system and framework for creating and maintaining websites and web applications. It provides an out of the box web-based administration panel that enables users to make modifications to parts of the website, which includes a WYSIWYG website editor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can otherwise consider it as an alternative CMS to WordPress, although arguably better and less bloated. 😁 Assuming that you already are working on a Silverstripe project, you can install a convenient module that I've developed that will automatically convert any image asset that has been rasterized, resized, or scaled from your Silverstripe project (through using &lt;a href="https://docs.silverstripe.org/en/4/developer_guides/files/images/" rel="noopener noreferrer"&gt;Silverstripe's image templating syntax&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;You can install the module with the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer require loveduckie/silverstripe-webp-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing this with the Composer package manager, simply run &lt;code&gt;/dev/build?flush=all&lt;/code&gt;. Without having done anything else, your Silverstripe project is now automatically generating and serving &lt;code&gt;.webp&lt;/code&gt; assets on page load! Woah! Let's take a look at how it's achieving this.&lt;/p&gt;

&lt;p&gt;If we take a look at the repository for the module, you will see that we are using a LibGD function (made available as part of PHP) for converting PNG images when intercepting requests for serving assets. &lt;a href="https://github.com/LoveDuckie/silverstripe-webp-image/blob/master/src/Flysystem/FlysystemAssetStore.php#L55" rel="noopener noreferrer"&gt;This line in particular.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="mf"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createWebPImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'imagewebp'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'imagecreatefromjpeg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'imagecreatefrompng'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'imagecreatefromgif'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$orgpath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'./'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAsURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$variant&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getimagesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="nv"&gt;$img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;imagecreatefromjpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;imagewebp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createWebPName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orgpath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;webp_quality&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="nv"&gt;$img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;imagecreatefrompng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;imagesavealpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// save alphablending setting (important)&lt;/span&gt;
                &lt;span class="nb"&gt;imagewebp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createWebPName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orgpath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;webp_quality&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;imagedestroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$img&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="mf"&gt;...&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;After the conversion is made in memory (using &lt;code&gt;imagewebp&lt;/code&gt;), it then gets served to disk using the file-path generated by &lt;code&gt;createWebPName($orgPath)&lt;/code&gt;. This path is something along the lines of this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/silverstripe-project-root/public/assets/Upload/your-image-filename.png.webp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The way that the file is named is important to note for later on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving Generated WebP Images from NGINX
&lt;/h2&gt;

&lt;p&gt;You can use the following NGINX configuration for serving WebP images automatically from your Silverstripe project's public assets directory. You can compare this configuration &lt;a href="https://docs.silverstripe.org/en/3/getting_started/installation/how_to/configure_nginx/" rel="noopener noreferrer"&gt;to the default one that Silverstripe has on their documentation pages for NGINX connectivity with PHP-FPM.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$http_accept&lt;/span&gt; &lt;span class="nv"&gt;$webp_suffix&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;default&lt;/span&gt;   &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;"~*webp"&lt;/span&gt;  &lt;span class="s"&gt;".webp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;server_name&lt;/span&gt; $&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;WEBSITE_DOMAIN_NAMES&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/mime.types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;...&lt;/span&gt;

  &lt;span class="s"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="n"&gt;/index.php?&lt;/span&gt;&lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="n"&gt;/assets/.+\.&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;extension&amp;gt;jpe?g|png|gif|webp)&lt;/span&gt;$ 
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_types&lt;/span&gt; &lt;span class="nc"&gt;image/png&lt;/span&gt; &lt;span class="nc"&gt;image/x-icon&lt;/span&gt; &lt;span class="nc"&gt;image/webp&lt;/span&gt; &lt;span class="nc"&gt;image/svg&lt;/span&gt;&lt;span class="s"&gt;+xml&lt;/span&gt; &lt;span class="nc"&gt;image/jpeg&lt;/span&gt; &lt;span class="nc"&gt;image/gif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Vary&lt;/span&gt; &lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;sendfile&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;$&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;request_uri&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;$&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;webp_suffix&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="kn"&gt;...&lt;/span&gt;

  &lt;span class="s"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_read_timeout&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kn"&gt;fastcgi_buffers&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="mi"&gt;65k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_buffer_size&lt;/span&gt; &lt;span class="mi"&gt;64k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_busy_buffers_size&lt;/span&gt; &lt;span class="mi"&gt;128k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_keep_conn&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt;   &lt;span class="nf"&gt;your-php-fpm-server-goes-here&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt;  &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt;  &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kn"&gt;include&lt;/span&gt;        &lt;span class="n"&gt;/etc/nginx/fastcgi_params&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;From the snippet above, &lt;code&gt;${WEBSITE_DOMAIN_NAMES}&lt;/code&gt; is automatically replaced when the NGINX server starts by using &lt;code&gt;envsubst&lt;/code&gt;, but you can replace it with your own  domain names, too. I've simply taken this configuration from my own webserver, as I am using the &lt;a href="https://hub.docker.com/_/nginx?tab=tags" rel="noopener noreferrer"&gt;Alpine Linux Docker container image for NGINX.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most important part of this configuration is this section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$http_accept&lt;/span&gt; &lt;span class="nv"&gt;$webp_suffix&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;default&lt;/span&gt;   &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;"~*webp"&lt;/span&gt;  &lt;span class="s"&gt;".webp"&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;You can think of it as a &lt;code&gt;switch&lt;/code&gt; statement in most languages. Depending on what &lt;code&gt;$http_accept&lt;/code&gt; resolves to when a request is served, will ultimately determine whether the resulting variable &lt;code&gt;$webp_suffix&lt;/code&gt; will resolve to either &lt;code&gt;.webp&lt;/code&gt; or nothing. This resulting variable will then be used when serving static files from the assets directory, which occurs in this part of the configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;  &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="n"&gt;/assets/.+\.&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;extension&amp;gt;jpe?g|png|gif|webp)&lt;/span&gt;$ 
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_types&lt;/span&gt; &lt;span class="nc"&gt;image/png&lt;/span&gt; &lt;span class="nc"&gt;image/x-icon&lt;/span&gt; &lt;span class="nc"&gt;image/webp&lt;/span&gt; &lt;span class="nc"&gt;image/svg&lt;/span&gt;&lt;span class="s"&gt;+xml&lt;/span&gt; &lt;span class="nc"&gt;image/jpeg&lt;/span&gt; &lt;span class="nc"&gt;image/gif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Vary&lt;/span&gt; &lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;max&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;sendfile&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;$&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;request_uri&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;$&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;webp_suffix&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&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 configuration block basically instructs NGINX to respond to requests that end with the suffix of &lt;code&gt;.jpeg&lt;/code&gt;, &lt;code&gt;.png&lt;/code&gt;, &lt;code&gt;.gif&lt;/code&gt;, or &lt;code&gt;.webp&lt;/code&gt;. It will then attempt to locate and serve the &lt;code&gt;.webp&lt;/code&gt; of the same requesting asset &lt;em&gt;if&lt;/em&gt; the &lt;code&gt;Accept&lt;/code&gt; header has been defined to include &lt;code&gt;.webp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can see that happening on this line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;    &lt;span class="k"&gt;try_files&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;$&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;request_uri&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;$&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;webp_suffix&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on what the incoming browser request has in their &lt;code&gt;Accept&lt;/code&gt; header, will automatically determine if the &lt;code&gt;$webp_suffix&lt;/code&gt; is populated, which will in-turn decide which version of the same asset is retrieved from your server's filesystem.&lt;/p&gt;

&lt;p&gt;Once you've applied this configuration, you can then run the following command to test it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Followed by this one to reload it, assuming that the previous one did not fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx -s reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you navigate to your running Silverstripe project, you should notice something interesting if you open up your Developer Console in Chrome.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwysh9qok85nfnawyjac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwysh9qok85nfnawyjac.png" alt="image.png" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll notice that even though your browser is loading an image asset with a .jpeg extension, the MIME type for the response is that of &lt;code&gt;image/webp&lt;/code&gt;. This means that NGINX was able to locate the generated &lt;code&gt;.webp&lt;/code&gt; image asset, and was able to serve it back. This is good news!&lt;/p&gt;

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

&lt;p&gt;And there you have it. An end-to-end solution for integrating &lt;code&gt;.webp&lt;/code&gt; image assets with your Silverstripe project, and how to serve them through a modern web-server such as &lt;a href="https://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt;. Using some clever NGINX configuration trickery, we can automatically pick and serve &lt;code&gt;.webp&lt;/code&gt; should it be discoverable on the filesystem. These image assets are being automatically generated by Silverstripe whenever templates are being rendered that make use of the syntax for manipulating images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/LoveDuckie/f6ed0b13041f6ee5de9cfc95b37369dc" rel="noopener noreferrer"&gt;NGINX configuration for Silverstripe with WebP image serving&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading! Feel free to &lt;a href="https://twitter.com/TheLoveDuckie" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;, or &lt;a href="https://lucshelton.com/" rel="noopener noreferrer"&gt;check out my website&lt;/a&gt; where I often blog about other topics.&lt;/p&gt;

</description>
      <category>php</category>
      <category>silverstripe</category>
      <category>nginx</category>
      <category>webp</category>
    </item>
    <item>
      <title>SilverStripe: Integrating Memcached Caching with Docker</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Fri, 04 Mar 2022 17:07:05 +0000</pubDate>
      <link>https://dev.to/theloveduckie/silverstripe-integrating-memcached-caching-with-docker-5beo</link>
      <guid>https://dev.to/theloveduckie/silverstripe-integrating-memcached-caching-with-docker-5beo</guid>
      <description>&lt;p&gt;In this blog post, I am going to demonstrate how you can hook up an existing &lt;code&gt;memcached&lt;/code&gt; server instance with &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt;. The purpose for this is so that you can in-memory based caching, instead of the default alternative that is available which is a file-based OpCache solution that &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; uses as the fallback.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;There is already existing documentation on the &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; pages for configuring &lt;code&gt;memcached&lt;/code&gt;, but it makes use of the YAML-based configuration engine along with &lt;a href="https://docs.silverstripe.org/en/4/developer_guides/extending/injector/" rel="noopener noreferrer"&gt;SilverStripe's type injector&lt;/a&gt; for instantiating the type. For some reason, this configuration sample doesn't work despite being a convincing solution.&lt;/p&gt;

&lt;p&gt;Instead, for my solution, I will be making use of the &lt;code&gt;_config.php&lt;/code&gt; file for writing the code explicitly for instantiating the &lt;code&gt;memcached&lt;/code&gt; caching interface adapter. It looks something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Cache\MemcachedCacheFactory;

$cacheClient = new Memcached();
$cacheClient-&amp;gt;addServer('your-memcached-server-hostname-goes-here', 11211);
$cacheFactory = new MemcachedCacheFactory($cacheClient);

Injector::inst()-&amp;gt;registerService($cacheFactory, "SilverStripe\\Core\\Cache\\CacheFactory");
Injector::inst()-&amp;gt;registerService($cacheFactory, "CacheFactory");
Injector::nest();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can alternatively find &lt;a href="https://gist.github.com/LoveDuckie/b2a28fac0bea24dd3f21dacd9951f0d9" rel="noopener noreferrer"&gt;the Gist snippet for this configuration here.&lt;/a&gt; Afterwards, all you need to do is simply navigate to &lt;code&gt;/dev/build?flush=all&lt;/code&gt;, and it should automatically work providing that you have correctly configured your &lt;code&gt;memcached&lt;/code&gt; server.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does it do?
&lt;/h3&gt;

&lt;p&gt;Basically this configuration updates the class manifest of the Injector service, to say that should a type be instantiated named "&lt;a href="https://api.silverstripe.org/4/SilverStripe/Core/Cache/CacheFactory.html" rel="noopener noreferrer"&gt;CacheFactory&lt;/a&gt;" or more specifically &lt;code&gt;SilverStripe\Core\Cache\CacheFactory&lt;/code&gt;, you should the instantiate the &lt;a href="https://api.silverstripe.org/master/SilverStripe/Core/Cache/MemcachedCacheFactory.html" rel="noopener noreferrer"&gt;MemcachedCacheFactory&lt;/a&gt; instead. All we're doing in the snippet above is instantiating the adapter, or configuration for &lt;code&gt;memcached&lt;/code&gt; with the relevant properties, and then ensuring that it is used everywhere in the Silverstripe project.&lt;/p&gt;

&lt;p&gt;The injector is globally present service that is responsible for instantiating objects of every detectable type in the project.&lt;/p&gt;

&lt;p&gt;Refer to the section below if you need further information on running this with &lt;a href="https://docker.io/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;.&lt;/p&gt;




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

&lt;p&gt;If you're using &lt;a href="https://docker.io/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;, you might also appreciating using this docker-compose configuration. This is how best to describe a &lt;code&gt;memcached&lt;/code&gt; service if you are intending on using it with your &lt;a href="https://www.silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.9'

services:
  memcached:
    container_name: '${SERVICE_NAME}-memcached'
    image: memcached:1.6.9-alpine
    restart: always
    ports:
      - "11211:11211"
    environment:
      MEMCACHED_MEMUSAGE: 256
      MEMCACHED_THREADS: 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this &lt;code&gt;docker-compose&lt;/code&gt; configuration is as simple as running the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
docker-compose up memcached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it. Leave a comment if you need any more information.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>SilverStripe: JSON-LD Structured Data</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Fri, 04 Feb 2022 11:51:17 +0000</pubDate>
      <link>https://dev.to/theloveduckie/silverstripe-json-ld-structured-data-14hn</link>
      <guid>https://dev.to/theloveduckie/silverstripe-json-ld-structured-data-14hn</guid>
      <description>&lt;p&gt;This website is currently powered by &lt;a href="https://www.silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; which is an all encompassing &lt;a href="https://en.wikipedia.org/wiki/Content_management_system" rel="noopener noreferrer"&gt;Content Management System (CMS)&lt;/a&gt; and framework for building web applications using &lt;a href="https://php.net/" rel="noopener noreferrer"&gt;PHP&lt;/a&gt;. I chose to adopt this framework some time ago because I had yet to find anything that came close, using the web-development stack that I would otherwise enjoy using (TypeScript etc.).&lt;/p&gt;

&lt;p&gt;SilverStripe fortunately comes out of the box with a lot of features (including blogging), but there are some things that are missing that could make it much more competitive when developing websites that are performant with search engine results.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON-LD
&lt;/h2&gt;

&lt;p&gt;I've been spending some time trawling through the &lt;a href="https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data" rel="noopener noreferrer"&gt;Google Developer pages&lt;/a&gt; trying to find better ways of optimising my website for search engine performance. &lt;a href="https://dev.to/theloveduckie/tips-for-optimizing-page-speeds-3f80-temp-slug-7379223"&gt;I've already written a blog post here&lt;/a&gt; on best practices for optimising page performance and load times (which can impact search result rankings), but this blog post is more focused on exposing relevant metadata that search engines can use for recognising the type of content that your website has.&lt;/p&gt;

&lt;p&gt;In particular, Google provides support for enhancing search results for pages that publishes information such as Recipes, Events, and News Articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending SilverStripe for JSON-LD Support
&lt;/h2&gt;

&lt;p&gt;After reading through the Google Developer pages, I naturally was inclined to search SilverStripe's module repository to find out whether or not there was any thing already available that suited my needs. While there were some interesting projects that seemed almost relevant, they also still seemed largely incomplete, or only supported an older version of the framework.&lt;/p&gt;

&lt;p&gt;Relevant SilverStripe module projects include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://addons.silverstripe.org/add-ons/pixelspin/silverstripe-structureddata" rel="noopener noreferrer"&gt;pixelspin/silverstripe-structureddata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addons.silverstripe.org/add-ons/toastnz/silverstripe-structureddata" rel="noopener noreferrer"&gt;toastnz/silverstripe-structureddata&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decided to take it upon myself to write a module extension that would suit my needs, and offer comprehensive support to the entirety of the available and supported schema supported by Google search engine crawlers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/LoveDuckie/silverstripe-json-ld-structured-data" rel="noopener noreferrer"&gt;https://github.com/LoveDuckie/silverstripe-json-ld-structured-data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Packagist&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/loveduckie/silverstripe-json-ld-structured-data" rel="noopener noreferrer"&gt;https://packagist.org/packages/loveduckie/silverstripe-json-ld-structured-data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;You can install my module with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
composer require loveduckie/silverstripe-json-ld-structured-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It shouldn't require any other additional configuration, but you will need to add the following code to your parent &lt;code&gt;Page.ss&lt;/code&gt; template file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;

...

$PageStructuredData().RAW

...

&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

...

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will the invoke this piece of code that is responsible for generating breadcrumbs and invoking any extension functions available for the &lt;code&gt;SilverStripe\CMS\Model\SiteTree&lt;/code&gt; type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace LoveDuckie\SilverStripe\JsonLDStructuredData\Extensions;

use Exception;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Config\Config;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\CMS\Controllers\ContentController;

class JsonLDStructuredDataExtension extends DataExtension
{

...

    public function PageStructuredData()
    {
        $structuredDataContainer = [];
        return $this-&amp;gt;InjectedStructuredData($structuredDataContainer);
    }

...

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/LoveDuckie/silverstripe-json-ld-structured-data" rel="noopener noreferrer"&gt;You can view the rest of the source code for this module on my GitHub repository here.&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>NGINX: Default Server Configurations</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sun, 04 Jul 2021 15:28:27 +0000</pubDate>
      <link>https://dev.to/theloveduckie/nginx-default-server-configurations-4gie</link>
      <guid>https://dev.to/theloveduckie/nginx-default-server-configurations-4gie</guid>
      <description>&lt;p&gt;I recently encountered a critical issue when configuring my NGINX server (that serves this website), when I had multiple (unrelated) domain names configured to point to the same virtual private server (VPS). The problem was that only one set were meant to be in use (such as &lt;code&gt;loveduckie.*&lt;/code&gt;). Unfortunately, this then meant that the remaining domain names (the ones intended to be left unused) were erroneously pointing to my portfolio website when they should not have been. This is can be particularly problematic, &lt;a href="https://developers.google.com/search/docs/advanced/crawling/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;because Google can severely relegate the search ranking for your website, if it deems it not to be the "canonical" version of it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What this means exactly is that there could be two completely separate and unrelated domain names pointing to the same page or content, but because Google considers the &lt;em&gt;wrong&lt;/em&gt; one to be the "one true source", it then defines it as the canonical version which is not our intention. I don't want an unrelated domain name to become the "canonical" source for my portfolio!&lt;/p&gt;

&lt;p&gt;To fix this, I produced a NGINX configuration that ensured that any time the unused set of domains were visited, they would be redirected to a default error landing page (much like you would expect when navigating to a HTTP 404). This means that subsequent crawls from Google will be able to determine a difference between my portfolio's domain names, and the ones that are considered to be unrelated.&lt;/p&gt;

&lt;p&gt;The error pages look a little something like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FScreenshot-2021-07-04-164337.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FScreenshot-2021-07-04-164337.png" title="Generic Landing Page" alt="The default landing page that is presented to viewers when they navigate to the wrong domain name." width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The default landing page that is presented to viewers when they navigate to the wrong domain name.&lt;/p&gt;

&lt;p&gt;And of course, there are custom error pages depending on the HTTP status code that is being returned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F404.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F404.png" title="HTTP 404 Error Page" alt="The error page that is served to the user when the HTTP 404 error code is returned." width="759" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The error page that is served to the user when the HTTP 404 error code is returned.&lt;/p&gt;

&lt;p&gt;Aside from the overkill templating of the error pages with Bootstrap, there's nothing particularly fancy about this so far.&lt;/p&gt;




&lt;h2&gt;
  
  
  NGINX Configuration
&lt;/h2&gt;

&lt;p&gt;Configuring your NGINX server is pretty straight forward, and only relies on you needing to use a particular set of keywords that NGINX parses when reading your configuration files. To begin with, you are going to want to create a new server configuration file called &lt;code&gt;default.conf&lt;/code&gt;. The name of the configuration file is largely irrelevant, as your NGINX server should be configured to read all configuration files under a certain directory. For instance, your default &lt;code&gt;nginx.conf&lt;/code&gt; configuration file should contain a statement such as &lt;code&gt;include /etc/nginx/conf.d/*.conf&lt;/code&gt; so that it can read all configuration files (that presumably have &lt;code&gt;server&lt;/code&gt; blocks) and load your virtual servers accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server 
{
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name_in_redirect off;
    server_name default_server;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, so good. All this server block is ensuring that it is binding itself to both port &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt;, which are used for HTTP and HTTPS traffic. You'll also note the usage of "&lt;code&gt;default_server&lt;/code&gt;", which basically tells NGINX that if the domain name does not have a server block configuration available for it on the server, then simply make use of this "&lt;code&gt;default&lt;/code&gt;" server block configuration instead.&lt;/p&gt;

&lt;p&gt;There's a few other things going on here as well.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server_name_in_redirect off;&lt;/code&gt; basically states that there doesn't need to be a match between the host name defined in the HTTP request Host header and the &lt;code&gt;server_name&lt;/code&gt; configuration value in order for the our default configuration to be considered a valid match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server_tokens off;&lt;/code&gt; is not strictly related to this article, but basically states that the HTTP response mustn't specify that this was served by NGINX (i.e. &lt;code&gt;Server&lt;/code&gt; HTTP header).&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Handling Specific HTTP Errors
&lt;/h3&gt;

&lt;p&gt;In the instance that someone navigates to a page that does not exist or cannot be served by any of the "server block" configurations loaded by NGINX, you will likely want to redirect them to a 40x or 50x error status page. Configuring page redirects for both range of error codes is straight forward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server 
{

    ...

    root /var/www/default;
    index index.html index.htm;

    location ~* ^.+ {
        try_files $uri $uri/ =404;
    }

    location / {
        try_files $uri $uri/ =404;
    }

    error_page 404 /404.html;
    error_page 403 /403.html;
    location = /404.html {
        root /var/www/default;
    }

    error_page 500 502 503 504 /500.html;
    location = /500.html {
        root /var/www/default;
    }

    ...

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

&lt;/div&gt;



&lt;p&gt;In the example above, I set the root directory to &lt;code&gt;/var/www/default&lt;/code&gt; which is the path I am using for storing static page files for my error pages in my NGINX Docker container (as shown in the screenshots above). If you are building a NGINX service from a Docker image, you will want to make sure that the path exists, and that there are static files that you can serve from the path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling SSL Traffic
&lt;/h3&gt;

&lt;p&gt;Next, you are going to want to make sure that you have some kind of SSL certificate that you can use for serving HTTPS traffic. Unless you actually have a valid HTTPS certificate for the traffic that you are intending on redirecting, you will want to create your own self-signed one using the available SSL command-line tooling.&lt;/p&gt;

&lt;h4&gt;
  
  
  Installing Dependencies for SSL in Docker (Optional)
&lt;/h4&gt;

&lt;p&gt;If you are using the Alpine Linux variant of the NGINX Docker image (&lt;code&gt;nginx:stable-alpine&lt;/code&gt; for example), you must ensure that you've installed the required dependencies through the Alpine Linux package manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN apk add --no-cache openssl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then you will want to generate your own self-signed certificate, and then store it somewhere appropriate in the filesystem for the Docker container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN openssl req -new -x509 -nodes -days 365 -newkey rsa:4096 -extensions 'v3_req' \
        -keyout /etc/nginx/ssl-default/default-privkey.pem \
        -out /etc/nginx/ssl-default/default-fullchain.pem \
        -config /etc/nginx/openssl-gen.cnf &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll note that this command-line expression is referring to a configuration file that is located at &lt;code&gt;/etc/nginx/openssl-gen.cnf&lt;/code&gt;. This is a custom configuration file that I've copied into the Docker image from a previous &lt;code&gt;COPY&lt;/code&gt; statement. The path can be changed with wherever you decide to copy this configuration file to inside your Docker container. The configuration file looks little something like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[req]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
name = Your Name Goes Here
countryName= Your Country Name Goes Here
stateOrProvinceName = Your State or Province Name Goes Here
emailAddress = Your Email Address Goes Here
localityName = London
organizationalUnitName = Your Name Goes Here
commonName = localhost

[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing too fancy, and it doesn't necessarily need to have the SAN (subject alternate names) definitions for the unsupported domain names that you intend on redirecting to your default landing pages. Of course, because it is a self-signed certificate (i.e. a certificate signed using your own created certificate authority), you should assume that this will throw HTTPS errors should people navigate to the domain through HTTPS.&lt;/p&gt;

&lt;h4&gt;
  
  
  Testing Configuration Changes
&lt;/h4&gt;

&lt;p&gt;Ensure that you've tested your changes before restarting your Docker container, or reloading your configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
nginx -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then reload your configuration if the response is without errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
nginx -s reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, if you are running NGINX from a Docker container, you can do it from the command-line (outside of the container) using a command similar to this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
docker exec -it your-nginx-container-name-goes-here nginx -s reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;Use a default configuration to prevent there being "search result collisions" between two unrelated domain names that target the same host.&lt;/p&gt;

&lt;p&gt;I hope you found this useful. There is another approach to this, and that is to adjust the firewall configuration for your virtual private server, so that all traffic to that particular host (read: domain) name is rejected. This is largely contingent on what Linux operating system you are using, and is arguably not as convenient as managing it at container-level (i.e. from the NGINX instance itself).&lt;/p&gt;

&lt;p&gt;You can find the complete NGINX configuration snippet for everything discussed in this article, &lt;a href="https://gist.github.com/LoveDuckie/685e55c1e2c5695b011232b894190f8f" rel="noopener noreferrer"&gt;in this Gist on GitHub.&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Complete NGINX Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server 
{
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name_in_redirect off;
    server_name default_server;
    server_tokens off;

    charset utf-8;

    access_log /var/log/nginx/host.access.log main;
    error_log /var/log/nginx/host.error.log warn;

    ssl_certificate /etc/nginx/ssl-default/default-fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl-default/default-privkey.pem;

    root /var/www/default;
    index index.html index.htm;

    location ~* ^.+ 
    {
        try_files $uri $uri/ =404;
    }

    location / 
    {
        try_files $uri $uri/ =404;
    }

    error_page 404 /404.html;
    error_page 403 /403.html;
    location = /404.html 
    {
        root /var/www/default;
    }

    error_page 500 502 503 504 /500.html;
    location = /500.html 
    {
        root /var/www/default;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Useful Reading
&lt;/h2&gt;

&lt;p&gt;Find below some other useful links that I found when trying to troubleshoot my woes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://nginx.org/en/docs/http/request_processing.html" rel="noopener noreferrer"&gt;NGINX: Request Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/advanced/crawling/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;Google Search Console: Canonical URLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/LoveDuckie/685e55c1e2c5695b011232b894190f8f" rel="noopener noreferrer"&gt;GitHub Gist: NGINX Default Server Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you found this useful. Feel free to get in touch if you require any help!&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>configuration</category>
      <category>security</category>
      <category>docker</category>
    </item>
    <item>
      <title>Tips for Optimizing Page Speeds</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sat, 03 Jul 2021 14:30:55 +0000</pubDate>
      <link>https://dev.to/theloveduckie/tips-for-optimizing-page-speeds-2dii</link>
      <guid>https://dev.to/theloveduckie/tips-for-optimizing-page-speeds-2dii</guid>
      <description>&lt;p&gt;I've been spending a lot of time optimising the page load performance of my website on both mobile and desktop devices. The bulk of my effort has been guided by the documentation listed by &lt;a href="https://pagespeed.web.dev/" rel="noopener noreferrer"&gt;Google's PageSpeed Insights&lt;/a&gt; tool. I've documented some of the things that I've learned in the process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Server
&lt;/h2&gt;

&lt;p&gt;For the &lt;a href="https://docker.io/" rel="noopener noreferrer"&gt;Docker container image&lt;/a&gt;, my biggest concern were largely to do with CPU usage and memory footprint. I am making use of a all-encompassing CMS framework called &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; which (fortunately) ships with some additional features for caching to either a local filesystem, or to a distributed caching system such as Redis or Memcached. However, it does come with some added bloat, and often requires me to install a lot of additional extensions for PHP in order for it to work correctly.&lt;/p&gt;




&lt;h3&gt;
  
  
  NGINX
&lt;/h3&gt;

&lt;p&gt;There are a few simple ways that you can overall improve page performance of your website by tweaking a few configuration options that are defined by default in NGINX.&lt;/p&gt;

&lt;h4&gt;
  
  
  Use GZip Compression Rules
&lt;/h4&gt;

&lt;p&gt;By default &lt;a href="https://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; only serves &lt;code&gt;text/html&lt;/code&gt; as with &lt;code&gt;gzip&lt;/code&gt; compression. You'll note that if you navigate to a file of any other type than a simple &lt;code&gt;.html&lt;/code&gt; document, that the response headers won't specify the Accept-Encoding header with &lt;code&gt;gzip&lt;/code&gt; as the corresponding value. This can be tweaked within the scope of a location configuration block.&lt;/p&gt;

&lt;p&gt;For instance, within the location block for my &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; installation's &lt;a href="https://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; configuration, I've defined a wider range of assets that can be supported for compression.&lt;/p&gt;

&lt;p&gt;Refer to the NGINX location block configuration below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; location ~* /assets/.+\.(?&amp;lt;extension&amp;gt;js|css|xml|json)$
  {
    gzip on;
    gzip_static on;
    gunzip on;
    gzip_http_version 1.0;
    gzip_comp_level 2;
    gzip_min_length 1100;
    gzip_buffers 4 8k;
    gzip_proxied any;
    gzip_types
      text/plain
      text/xml
      text/css
      text/comma-separated-values
      image/png
      image/x-icon
      image/webp
      image/svg+xml
      image/jpeg
      image/gif
      text/javascript
      application/x-javascript
      application/javascript

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

&lt;/div&gt;



&lt;p&gt;The rule does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Matches the applicable file extensions based on the incoming request, by using the regular expression pattern specified (&lt;code&gt;/assets/.+\.(?&amp;lt;extension&amp;gt;js|css|xml|json)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Depending MIME type recognised, it will then serve the request with the applicable gzip headers, ensuring that the static asset will be served as such.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Use Reverse Proxy Caching
&lt;/h4&gt;

&lt;p&gt;If you are using NGINX as a reverse proxy - which is often the case for PHP-FPM servers - then you might want to consider adding rules for caching content to a local path on your NGINX instance.&lt;/p&gt;

&lt;p&gt;Refer to the location configuration block below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http {
    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:10m
    inactive=24h max_size=1g;
    server {
        location / {
            proxy_pass http://1.2.3.4;
            proxy_set_header Host $host;
            proxy_buffering on;
            proxy_cache STATIC;
            proxy_cache_valid 200 1d;
            proxy_cache_use_stale error timeout invalid_header updating
                                   http_500 http_502 http_503 http_504;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Apply Cache Control Headers
&lt;/h4&gt;

&lt;p&gt;I'm working on this section!&lt;/p&gt;

&lt;h4&gt;
  
  
  Automatically Serving WebP Optimized Image Assets
&lt;/h4&gt;

&lt;p&gt;Depending on how your project is developed, it's possible to write a configuration for &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; so that it automatically attempts to send the &lt;code&gt;.webp&lt;/code&gt; version of an image to the requesting user, instead of the original image asset itself. The benefit of this approach is that if your server-sided software automatically converts your image assets to &lt;code&gt;.webp&lt;/code&gt; once they are uploaded, or rendered by the server, the optimized version of the asset will be used by &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt;, and the original asset will be used as a fallback should there not be one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;map $http_accept $webp_suffix {
  default "";
  "~*webp" ".webp";
}

location ~* /assets/.+\.(?&amp;lt;extension&amp;gt;jpe?g|png|gif|webp)$ {

...

    # more_set_headers 'Content-Type: image/webp';
    add_header Vary Accept;
    sendfile on;
    try_files "${request_uri}${webp_suffix}" $uri =404;

...

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

&lt;/div&gt;



&lt;p&gt;Which translates to logic that can be otherwise described by this flowchart diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FWebP-Request-Flow.drawio-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FWebP-Request-Flow.drawio-1.png" title="A simple diagram illustrating the logic flow of requesting and retrieving a static image asset with SilverStripe using the NGINX configuration." alt="A simple diagram illustrating the logic flow of requesting and retrieving a static image asset with SilverStripe using the NGINX configuration." width="692" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A simple diagram illustrating the logic flow of requesting and retrieving a static image asset with SilverStripe using the NGINX configuration.&lt;/p&gt;

&lt;p&gt;The above code snippet is an example of a NGINX "location block" that this website uses for serving publicly visible image assets to the requesting user. It's doing several things here, but most importantly the &lt;code&gt;try_files&lt;/code&gt; statement at the bottom of the location block is attempting to do one task. If the &lt;code&gt;.webp&lt;/code&gt; counterpart for the image that is being requested exists, then serve that back to the requesting user of the website. If it doesn't exist, then simply use the original version of the asset as a fallback.&lt;/p&gt;

&lt;p&gt;Providing that you have integrated the &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; location block correctly with your website, HTTP responses for image assets should be appearing in your developer tools as such.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F9f473023a9%2FScreenshot-2021-07-03-175201-v2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F9f473023a9%2FScreenshot-2021-07-03-175201-v2.png" title="Google Chrome's Developer Console" alt="A screenshot from Google Chrome's Developer Tools displaying a response from a HTTP request for an image asset on the website." width="450" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A screenshot from Google Chrome's Developer Tools displaying a response from a HTTP request for an image asset on the website.&lt;/p&gt;

&lt;p&gt;You'll notice that the &lt;code&gt;Content-Type&lt;/code&gt; header is now resolving &lt;code&gt;image/webp&lt;/code&gt; as opposed to the image assets actual MIME type which would typically be &lt;code&gt;image/jpeg&lt;/code&gt; or &lt;code&gt;image/png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You might also find this project interesting: &lt;a href="https://lucshelton.com/projects/personal/silverstripe-module-automatic-webp-image-conversion/" rel="noopener noreferrer"&gt;SilverStripe Automatic WebP Image Conversion&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Website Performance
&lt;/h2&gt;

&lt;p&gt;Improving the performance of your website itself is a significantly more complex subject, as it is of course largely contingent on which tools you use for developing your website with in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minify Text-based Static Assets
&lt;/h3&gt;

&lt;p&gt;Most modern &lt;a href="https://en.wikipedia.org/wiki/Single-page_application" rel="noopener noreferrer"&gt;Single Page Application&lt;/a&gt; frameworks such as &lt;a href="https://angular.io/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt; already minify and "bundle" JavaScript and CSS static assets, but if you're like me and not using a &lt;a href="https://en.wikipedia.org/wiki/Single-page_application" rel="noopener noreferrer"&gt;SPA&lt;/a&gt; framework such as &lt;a href="https://angular.io/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;, then you will need to seek other tooling. "Minification" quite simply is the process of reducing the amount of unnecessary characters found in most JavaScript and CSS assets. Certain strategies in this approach include, but not limited to...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reducing symbol length&lt;/strong&gt; - reducing the length or size of syntactically recognized keywords such as variable names, numerical values, and operators (e.g. inline &lt;code&gt;function&lt;/code&gt; statements converted to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions" rel="noopener noreferrer"&gt;lambda declarations&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing white-space characters, and condensing all interpreted text&lt;/strong&gt; - Applicable in both JavaScript or Cascading Style Sheet assets; this ensures there are fewer characters (and therefore bytes) saved in the file, and served to a user's browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing duplicates&lt;/strong&gt; - Removing any potentially duplicate code that might be imported or loaded by libraries with shared dependencies.&lt;/li&gt;
&lt;li&gt;Bundling - combining multiple JavaScript files into a single one so that it reduces the amount of HTTP requests to the server for individual JavaScript assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Tree-shaking"&lt;/strong&gt; - Removing "loose" or unused code that is otherwise found in third-party libraries. Typically this is only done in production environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately there's already an abundance of tooling available, and you don't need to waste your spare time on the weekend reinventing the wheel.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tooling - NodeJS and JavaScript
&lt;/h4&gt;

&lt;p&gt;Much to what I was alluding to previously about SPA frameworks, there's already a significant amount of JavaScript tooling available for minifying and bundling JavaScript and CSS assets. I &lt;em&gt;personally&lt;/em&gt; recommend the following libraries, as they have been suitable for my projects...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://browserify.org/#install" rel="noopener noreferrer"&gt;Browserify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webpack.js.org/" rel="noopener noreferrer"&gt;Webpack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jspm.org/" rel="noopener noreferrer"&gt;jspm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Tooling - PHP
&lt;/h4&gt;

&lt;p&gt;If you're like me, and you're using reluctantly using PHP because your website's backend is powered by it (ahem, SilverStripe), you will probably be seeking alternatives to what has been mentioned above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/matthiasmullie/minify" rel="noopener noreferrer"&gt;Minify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Optimising Image Assets with WebP
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;As described on the official website&lt;/a&gt;, "WebP is a modern  &lt;strong&gt;image format&lt;/strong&gt;  that provides superior  &lt;strong&gt;lossless and lossy&lt;/strong&gt;  compression for images on the web". It's an low-memory image format that is widely compatible with most modern browsers and has a significantly reduced on-disk memory footprint with minimal reduction in overall image quality.&lt;/p&gt;

&lt;p&gt;Depending on how your website is developed will ultimately determine how best to implement the usage of this image format, but the tooling available is widely compatible with most (popular) operating systems including Linux, OS X, and Windows. Furthermore, the tooling is largely compatible with other existing image formats such as JPEG, PNG, and GIF.&lt;/p&gt;

&lt;p&gt;In general, if your website makes use of some form of backend for rendering pages (i.e. Server-Sided Rendering or SSR for short) or for serving an API for data retrieval, you should be attempting to automatically rasterize or convert image assets to WebP image file format where possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;You can read more about WebP image format here.&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  SilverStripe and Automatic WebP Image Conversion
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; doesn't have any official support for the &lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;WebP image format&lt;/a&gt;, but there are community members that have developed extensions for automatically generating &lt;code&gt;.webp&lt;/code&gt; assets for popular formats including JPEG and PNG when &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; attempts to render images at a reduced size. This largely depends on how you have developed the templates for your &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; website, but arbitrary template statements used for resizing images such as &lt;code&gt;$Image.ResizedImage(200, 300)&lt;/code&gt; and &lt;code&gt;$Image.Fit(300,300)&lt;/code&gt; will trigger &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; to rasterize or resize the image. This behaviour or functionality can be overridden, so that the resulting resized image can be saved or compressed to a &lt;a href="https://developers.google.com/speed/webp" rel="noopener noreferrer"&gt;WebP image&lt;/a&gt; at the same time that it is being rasterized or resized.&lt;/p&gt;

&lt;p&gt;Find below a screenshot of how the resulting image asset appears in the filesystem once it has been resized on disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F31ab88a16a%2FScreenshot-2021-07-03-172228-v2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F31ab88a16a%2FScreenshot-2021-07-03-172228-v2.png" title="A screenshot of how the newly resized or rasterized images appear on disk once they are converted to the .webp image file format." alt="A screenshot of how the newly resized or rasterized images appear on disk once they are converted to the .webp image file format." width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A screenshot of how the newly resized or rasterized images appear on disk once they are converted to the .webp image file format.&lt;/p&gt;

&lt;p&gt;If you are using the &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; framework for your website, then I strongly recommend that you use a plugin or add-on such as the one that was originally developed by a community member by "&lt;a href="https://github.com/nomidi" rel="noopener noreferrer"&gt;nomidi&lt;/a&gt;". It's a &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt; add-on that seamlessly integrates the ability to automatically generate .webp images if the WebP PHP extension is loaded and available for usage on the server.&lt;/p&gt;

&lt;p&gt;If you are using PHP (or &lt;a href="https://silverstripe.org/" rel="noopener noreferrer"&gt;SilverStripe&lt;/a&gt;), you can make use of this simple PHP code snippet for testing whether the WebP GD image library extension is loaded and available to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

if (function_exists('imagewebp'))
{
    echo 'webp support available';
}
else
{
    echo 'function not found';
}

echo phpinfo();

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

&lt;/div&gt;



&lt;p&gt;As it may be possible to infer from the code, it simply checks whether or not the function "&lt;code&gt;imagewebp&lt;/code&gt;" is loaded and available globally.&lt;/p&gt;

&lt;p&gt;I made a fork of this add-on that only does one change, and that's to save newly created &lt;code&gt;*.webp&lt;/code&gt; images as &lt;code&gt;*.jpeg.webp&lt;/code&gt; instead of &lt;code&gt;*_webp.jpeg&lt;/code&gt;. The reasoning behind this modification is simply so that it's easier to produce an NGINX configuration that can construct a path to the &lt;code&gt;.webp&lt;/code&gt; version of the image instead of the original. This constructed file path is then of course used for loading and serving the &lt;code&gt;.webp&lt;/code&gt; image asset to the user (if it exists on disk).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/LoveDuckie/silverstripe-webp-image" rel="noopener noreferrer"&gt;You can find my fork for this add-on here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've since added support for &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIF Images&lt;/li&gt;
&lt;li&gt;Silverstripe 4.10&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  WebP Image Format Compatibility with PHP
&lt;/h4&gt;

&lt;p&gt;In the event that &lt;code&gt;imagewebp&lt;/code&gt; functions are not available for usage with your PHP Docker container, can make use of the following snippet as part of your Dockerfile for building PHP FPM containers. These snippets assume that you are making use of the &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine Linux&lt;/a&gt; variant of the PHP FPM container.&lt;/p&gt;

&lt;p&gt;The first snippet ensures the system-wide dependencies for creating &lt;code&gt;.webp&lt;/code&gt; images are available, and that the supporting PHP extension can create &lt;code&gt;.webp&lt;/code&gt; images. The second snippet makes the function listed above available for usage within PHP, so that it can actually access the system-wide tooling for creating and converting assets to &lt;code&gt;.webp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Install packages and dependencies of WebP. This is _ &lt;strong&gt;only&lt;/strong&gt; _ applicable if you are using both Docker, and the Alpine Linux image variant of PHP FPM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN apk add --no-cache libwebp-dev libwebp

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

&lt;/div&gt;



&lt;p&gt;Next, the GD library has to be configured so that it points to the relevant paths for the PNG, JPEG, FreeType, and WebP system libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN docker-php-ext-configure gd \
    --with-freetype-dir=/usr/include/ \
    --with-jpeg-dir=/usr/include/ \
    --with-png-dir=/usr/include/ --with-webp-dir=/usr/include/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, you will of course want to run your container with the aforementioned modifications, and test that the required modules are loaded and are accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improving "First Contentful Paint" Loading Speed
&lt;/h3&gt;

&lt;p&gt;In summary, "&lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/First_contentful_paint" rel="noopener noreferrer"&gt;First Contentful Paint&lt;/a&gt;" is the amount of time it takes for any DOM element to be rendered to the screen. This doesn't necessarily mean the entire page must be loaded, rather it simply means that a single element has begun rendering, and that the browser has acquired the all required assets to render the page with. In order for the browser to begin rendering, it must first download all required assets. These are typically defined in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; element of the page as &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; statements. Unless defined otherwise, these statements or HTML elements are considered to be "blocking", therefore meaning that the rest of the page cannot be loaded, or rendered to the screen until the assets defined in those tags have been downloaded by the browser. As you might imagine, this can potentially significantly impact the "First Contentful Paint" (FCP for short) speed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Structured Usage of Async and Defer Statements
&lt;/h4&gt;

&lt;p&gt;In some instances it could be the case that not all script or CSS assets need to be loaded in order for the page to be rendered to the user. Instead, they could be deferred or done asynchronously while the remainder of the website or page is being rendered or displayed to the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lazy Load Blocking Resources
&lt;/h3&gt;

&lt;p&gt;Static assets such as images, cascading style sheets and JavaScript files are considered to be blocking resources when they are loaded by default. Where possible, it's important to ensure that these resources are loaded &lt;em&gt;after&lt;/em&gt; text content and other DOM elements have. There are a number of methods that you can adopt for reducing the time it takes for the "First Contentful" Paint (FCP) to complete, depending on the type of static assets you are optimizing. The idea of "lazy loading" is simple. Only load static, blocking assets when they are required such as when the window has focus, or if the DOM element is made visible to the user.&lt;/p&gt;

&lt;h4&gt;
  
  
  Images
&lt;/h4&gt;

&lt;p&gt;Most images on a page can be "lazy loaded", particularly ones that are being rendered as part of a home page carousel (i.e. revolving gallery of images). For instance, not every image in a carousel is displayed to the user at once (this is by design), so therefore they don't need to be loaded immediately. However, by default, a web-browser will automatically load all images regardless of whether they are considered visible (i.e. &lt;code&gt;display: none;&lt;/code&gt; or &lt;code&gt;display: block;&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Fortunately there is a convenient way of achieving this kind of asynchronous loading behaviour for images, by using a third-party JavaScript library named "&lt;a href="https://github.com/aFarkas/lazysizes" rel="noopener noreferrer"&gt;lazysizes&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;It can be loaded as part of your website by simply declaring it as part of your header as such.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;script src="lazysizes.min.js" async="true"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrating it with existing templates and images is as simple as doing 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;&amp;lt;!-- responsive example with automatic sizes calculation: --&amp;gt;
&amp;lt;img
    data-sizes="auto"
    data-src="image2.jpg"
    data-srcset="image1.jpg 300w,
    image2.jpg 600w,
    image3.jpg 900w" class="lazyload" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original image asset might look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- responsive example with automatic sizes calculation: --&amp;gt;
&amp;lt;img src="image2.jpg" class="lazyload" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Fonts, Stylesheets, and JavaScript
&lt;/h4&gt;

&lt;p&gt;No additional third-party library is required for loading static assets that are fonts, CSS stylesheets or JS assets. Instead, depending on the kind of behaviour you are seeking, you can use one of the following specifiers as part of &lt;code&gt;&amp;lt;link/&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;script/&amp;gt;&lt;/code&gt; statements when loading static assets.&lt;/p&gt;

&lt;h4&gt;
  
  
  Google's Third-party Libraries
&lt;/h4&gt;

&lt;p&gt;Lazily loading third-party libraries such as the ones that are used by Google Analytics engine, and Google AdSense are a little bit more tricky. It should be noted that in doing this, it can incur edge-case behaviour should you continue to use and rely on the data that is recorded in Google Analytics.&lt;/p&gt;

&lt;h4&gt;
  
  
  Google AdSense
&lt;/h4&gt;

&lt;p&gt;Observe the following code snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script type="text/javascript"&amp;gt;
    window.addEventListener('load', function() {
        var is_adsense_load = 0

        window.addEventListener('scroll', function() {
        if (is_adsense_load == 0) {
            is_adsense_load = 1;

            var ele = document.createElement('script');
            ele.async = true;
            ele.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'
            var sc = document.getElementsByTagName('script')[0]
            sc.parentNode.insertBefore(ele, sc);

            //google_ad_client: "ca-pub-xxxxxxxxx",
            (adsbygoogle = window.adsbygoogle || []).push({
            google_ad_client: "$AdSenseID",
            enable_page_level_ads: true
            });
        }
        })
    })
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typically such a code snippet would be placed in the &lt;/p&gt; section of your page. If you look closely enough, you'll note that this script is in fact just a revised version of the one that Google asks you to integrate as part of your pages, but it ensures that the script is only loaded &lt;em&gt;after&lt;/em&gt; the JavaScript event call back is fired when the window has loaded.

&lt;p&gt;It constructs the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; element manually, and appends it to the page's Document Object Model (DOM). This code will only get invoked once the page has loaded (and therefore invokes the callback).&lt;/p&gt;

&lt;p&gt;This works, but the caveat with this is that it may affect the accuracy of your statistics, as it will likely discount users who are only visiting your page for less than a couple of seconds. However, you may also not consider this to be of importance, so this would be a significant improvement if this is the case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools for Testing Performance
&lt;/h2&gt;

&lt;p&gt;Once you have made the necessary modifications for optimizing your website for improved load times, you can make use of some of the following websites for testing page load times, and for identifying any other errors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/speed/pagespeed/insights/" rel="noopener noreferrer"&gt;Google PageSpeed Insights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk?hl=en" rel="noopener noreferrer"&gt;Lighthouse Chrome Extension&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Miscellaneous Links
&lt;/h2&gt;

&lt;p&gt;You may also find these other links useful.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uhop/grunt-tight-sprite/wiki/Recipe:-serve-WebP-with-nginx-conditionally" rel="noopener noreferrer"&gt;Configuring your NGINX server to conditionally serve WebP images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/speed" rel="noopener noreferrer"&gt;Google Developer Page Speed: Tips for Improving Page Performance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pagespeed</category>
      <category>seo</category>
      <category>docker</category>
      <category>nginx</category>
    </item>
    <item>
      <title>Securing HTTP Headers with NGINX in Docker</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Sat, 05 Jun 2021 17:46:57 +0000</pubDate>
      <link>https://dev.to/theloveduckie/securing-http-headers-with-nginx-in-docker-4fc</link>
      <guid>https://dev.to/theloveduckie/securing-http-headers-with-nginx-in-docker-4fc</guid>
      <description>&lt;p&gt;Nginx, stylized as NGINX, nginx or NginX, is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache. More typically it is the web server of choice for most applications and APIs targeting the web. Like other popular web servers, NGINX tends to emit more data than necessary when serving HTTP requests when using the default configuration, which in turn can make it susceptible to (&lt;a href="https://en.wikipedia.org/wiki/Zero-day_(computing)" rel="noopener noreferrer"&gt;zero day&lt;/a&gt;) exploits.&lt;/p&gt;

&lt;p&gt;An example of what is meant can be found in the image below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Faac684ff83%2FScreenshot-2021-06-12-130857-v2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Faac684ff83%2FScreenshot-2021-06-12-130857-v2.png" title="Google Chrome Developer Tools - HTTP Headers" alt="HTTP headers from our page response using Google Chrome's developer tools." width="501" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;HTTP headers from our page response using Google Chrome's developer tools.&lt;/p&gt;

&lt;p&gt;This is the emitted response from navigating to any page on this website when using the default configuration. This is not ideal, as hackers or penetration testers can use automated tools for scanning and detecting websites that have potential vulnerabilities.&lt;/p&gt;

&lt;p&gt;This article will cover some steps that you can take for securing the HTTP responses that your NGINX server emits when using the Alpine Linux version of the Docker image.&lt;/p&gt;

&lt;p&gt;This article assumes that you have appropriately configured your development environment for usage with Docker's &lt;a href="https://docs.docker.com/develop/develop-images/build_enhancements/" rel="noopener noreferrer"&gt;BuildKit&lt;/a&gt; features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extending NGINX
&lt;/h2&gt;

&lt;p&gt;In the root of your project, create the following directory layout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;containers/
- nginx/
-- build/
--- Dockerfile
-- configuration/
--- templates/
docker-compose.yaml
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;docker-compose.yaml&lt;/code&gt; should look something like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.9'

services:
  nginx:
    container_name: '${SERVICE_NAME}-nginx'
    image: '${REMOTE_REGISTRY_HOST}${SERVICE_NAME}/nginx:${BUILD_VERSION}'
    build: 
      context: '${BUILD_ROOT}' 
      dockerfile: '${CONTAINERS_ROOT}/nginx/build/Dockerfile'
      target: portfolio-nginx-build
      args:
        NGINX_VERSION: ${NGINX_VERSION}
        NGINX_HEADERS_MORE_VERSION: ${NGINX_HEADERS_MORE_VERSION}
    environment:
      NGINX_ENVSUBST_TEMPLATE_DIR: /etc/nginx/templates
      NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
      NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; file should look something like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SERVICE_NAME=your-service-name-goes-here
COMPOSE_PROJECT_NAME=${SERVICE_NAME}
BUILD_ROOT=${PWD}
PROJECT_ROOT=${PWD}
CONTAINERS_ROOT=${PROJECT_ROOT}/containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, so good, and nothing out of the ordinary either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dockerfile
&lt;/h3&gt;

&lt;p&gt;Keep in mind that this article assumes that you are intending on using the Alpine Linux flavour or variant of NGINX's Docker image. It's significantly smaller in size, as the operating system variant comes typically bundled with far less system utilities and running services. The Linux variant is typically used in embedded systems for the same reasoning.&lt;/p&gt;

&lt;p&gt;The beginning of our Dockerfile is nothing out of the ordinary. It provides 3 arguments that are used for describing this particular flavour of the image, which are&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CUSTOM_BUILD_VERSION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CUSTOM_BUILD_DATE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CUSTOM_BUILD_UID&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are ideal if you are generating the &lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; images as part of your &lt;a href="https://en.wikipedia.org/wiki/Continuous_integration" rel="noopener noreferrer"&gt;continuous integration&lt;/a&gt; or build pipeline. At the time of writing this article, we are using version 1.20.1 of &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt;, which is compatible with version 0.33 of the &lt;a href="https://www.nginx.com/resources/wiki/modules/headers_more/" rel="noopener noreferrer"&gt;Headers More&lt;/a&gt; plugin.&lt;/p&gt;

&lt;p&gt;In addition, our Dockerfile "parameterises" the version numbers used for retrieving the correct version of NGINX and Headers More.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG CUSTOM_BUILD_VERSION
ARG CUSTOM_BUILD_DATE
ARG CUSTOM_BUILD_UID

ARG NGINX_VERSION 1.20.1

FROM nginx:${NGINX_VERSION}-alpine AS builder

ARG CUSTOM_BUILD_VERSION
ARG CUSTOM_BUILD_DATE
ARG CUSTOM_BUILD_UID

ARG NGINX_VERSION 1.20.1
ARG NGINX_HEADERS_MORE_VERSION 0.33
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we are going to have to download the source files for both NGINX and the Headers More plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN wget "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -O nginx.tar.gz &amp;amp;&amp;amp; \
    wget "https://github.com/openresty/headers-more-nginx-module/archive/v${NGINX_HEADERS_MORE_VERSION}.tar.gz" -O headers-more.tar.gz

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

&lt;/div&gt;



&lt;p&gt;This is necessary as we have to produce a local build of NGINX that integrates Headers More as a dynamic module. Therefore this means that we need to ensure that Alpine Linux has the GCC compiler toolchain available, along with make tool and some additional packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN apk add --no-cache --virtual .build-deps \
  git \
  gcc \
  libc-dev \
  make \
  openssl-dev \
  pcre-dev \
  zlib-dev \
  linux-headers \
  curl \
  gnupg \
  libxslt-dev \
  gd-dev \
  geoip-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bit consists of preparing the compilation environment for NGINX, including adjusting environment variables used by the compilation toolchain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN mkdir -p /usr/src

# Reuse same cli arguments as the nginx:alpine image used to build
RUN CONFARGS=$(nginx -V 2&amp;gt;&amp;amp;1 | sed -n -e 's/^.*arguments: //p') \
    tar -zxC /usr/src -f "nginx.tar.gz"

RUN tar -zxvC /usr/src -f "headers-more.tar.gz" 

RUN HEADERSMOREDIR="/usr/src/headers-more-nginx-module-0.33" &amp;amp;&amp;amp; \
  cd /usr/src/nginx-$NGINX_VERSION &amp;amp;&amp;amp; \
  ./configure --without-http_autoindex_module --with-compat $CONFARGS --add-dynamic-module=$HEADERSMOREDIR &amp;amp;&amp;amp; \
  make &amp;amp;&amp;amp; make install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Uses the tool &lt;code&gt;sed&lt;/code&gt; to search and replace part of the string output from invoking &lt;code&gt;nginx -V&lt;/code&gt; in the shell.

&lt;ol&gt;
&lt;li&gt;The output from &lt;code&gt;nginx -V&lt;/code&gt; displays the switch parameters used for compiling NGINX and configuring make.&lt;/li&gt;
&lt;li&gt;It makes use of the output generated, while appending our additional module (which is Headers More).&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Extracts the contents of the Headers More plugin that we downloaded in our previous snippet.&lt;/li&gt;

&lt;li&gt;Defines a variable pointing to the path where Headers More has been extracted to&lt;/li&gt;

&lt;li&gt;Runs &lt;code&gt;make&lt;/code&gt;.&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;In this next stage, we finally make use of the generated build output after downloading and building NGINX and Headers More together. We make use of a feature in Docker called "multi-stage" builds, which enables us to copy the contents from another stage of a build. This approach to generating Docker images is considered to be significantly more efficient, as it enables us to reuse cached stages when rebuilding the Docker image, or making iterative changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM nginx:${NGINX_VERSION}-alpine as your-service-name-goes-here-nginx

# Extract the dynamic module "headers more" from the builder image
COPY --from=builder /usr/local/nginx/modules/ngx_http_headers_more_filter_module.so /usr/local/nginx/modules/ngx_http_headers_more_filter_module.so

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

&lt;/div&gt;



&lt;p&gt;The rest of the Dockerfile definition is entirely up to you. Depending on the flavour of the build (i.e. development or production), you can choose to copy or include other files that you would find useful in that particular flavour of the build.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/LoveDuckie/ed255536e0939de4dec66a0ea036050d" rel="noopener noreferrer"&gt;You can find the complete Dockerfile constructed in this article here.&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Now that you have a working Dockerfile definition for your &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; image, we can now configure how the NGINX server will behave. The intent behind this configuration is to ensure that we are not sending more data that necessary in our HTTP responses. Using the &lt;a href="https://www.nginx.com/resources/wiki/modules/headers_more/" rel="noopener noreferrer"&gt;Headers More&lt;/a&gt; extension that we downloaded, compiled, included as part of our &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; &lt;a href="https://docs.docker.com/engine/reference/builder/" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt;, we can now prepare a &lt;a href="http://nginx.org/" rel="noopener noreferrer"&gt;NGINX&lt;/a&gt; server block configuration that can remove headers using the appropriate syntax.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.nginx.com/resources/wiki/modules/headers_more/" rel="noopener noreferrer"&gt;Headers More&lt;/a&gt; extension makes the &lt;code&gt;more_clear_headers&lt;/code&gt; command available for us to use, meaning that we can now remove headers that are added by default by the NGINX web server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';

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

&lt;/div&gt;



&lt;p&gt;A more complete example would look like this. Find below an example root &lt;code&gt;nginx.conf&lt;/code&gt; configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Load in the headers more module
load_module /usr/local/nginx/modules/ngx_http_headers_more_filter_module.so;

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {

    more_clear_headers 'Server';
    more_clear_headers 'X-Powered-By';

    server_tokens off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    #tcp_nopush on;

    keepalive_timeout 65;

    include /etc/nginx/conf.d/*.conf;
}

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

&lt;/div&gt;



&lt;p&gt;This configuration ensures that all HTTP responses served, regardless of which server blocks you have configured, will have the &lt;code&gt;X-Powered-By&lt;/code&gt; and &lt;code&gt;Server&lt;/code&gt; headers stripped from the HTTP response. Handy!&lt;/p&gt;

&lt;p&gt;Now you can rest assured that visitors will be none-the-wiser about which kind of software you are using for serving your web applications, and thus mitigating the ability for a malicious visitor to find or target exploits in your software stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;You might find these links interesting if you wish to take further steps to mitigate security vulnerabilities on your web application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://securityheaders.com/" rel="noopener noreferrer"&gt;Security Headers&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;A web-based tool for scanning HTTP responses emitted by your website for recommended HTTP headers.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/openresty/headers-more-nginx-module" rel="noopener noreferrer"&gt;Headers More Plugin GitHub Repository&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://www.nginx.com/resources/wiki/modules/headers_more/" rel="noopener noreferrer"&gt;NGINX Documentation for Headers More&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Games Development at Scale: Personal Builds</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Fri, 04 Dec 2020 14:14:19 +0000</pubDate>
      <link>https://dev.to/theloveduckie/games-development-at-scale-personal-builds-38cj</link>
      <guid>https://dev.to/theloveduckie/games-development-at-scale-personal-builds-38cj</guid>
      <description>&lt;p&gt;In this article I am going to breakdown some of my previous professional experiences in developing and using personal build systems, that enforced robust sanity checks on code submitted to mainline repositories of large multi-team projects&lt;/p&gt;

&lt;p&gt;During my time at &lt;a href="https://lucshelton.com/career/splash-damage/" rel="noopener noreferrer"&gt;Splash Damage&lt;/a&gt;, I was involved in developing the "personal build pipeline" that has since been highlighted in a talk presented by Valentin Galea, former Technical Director at &lt;a href="https://lucshelton.com/career/splash-damage/" rel="noopener noreferrer"&gt;Splash Damage&lt;/a&gt;. &lt;a href="https://github.com/splash-damage/unrealfest" rel="noopener noreferrer"&gt;You can check out the slides from that presentation here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For most of my professional career, I've made use of a continuous integration system called &lt;a href="https://www.jetbrains.com/teamcity/" rel="noopener noreferrer"&gt;TeamCity&lt;/a&gt;. It's a closed-source, flexible, user-friendly build and automation system developed by &lt;a href="https://www.jetbrains.com/" rel="noopener noreferrer"&gt;JetBrains&lt;/a&gt;. It comes shipped with a wide range of server-sided plugins, including a vendor-supported plugin for running personal builds.&lt;/p&gt;

&lt;p&gt;Links to documentation about Personal Builds with &lt;a href="https://www.jetbrains.com/teamcity/" rel="noopener noreferrer"&gt;TeamCity&lt;/a&gt;, and it's servers-sided &lt;em&gt;Remote Run&lt;/em&gt; plugin can be found here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/help/teamcity/personal-build.html" rel="noopener noreferrer"&gt;JetBrains TeamCity: Personal Builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.jetbrains.com/help/teamcity/vs-addin/Personal_Build.html" rel="noopener noreferrer"&gt;JetBrains TeamCity: Personal Builds - Visual Studio Extensions&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/help/teamcity/ij-addin/tc-run-build-remotely.html" rel="noopener noreferrer"&gt;JetBrains TeamCity: Remote Run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://confluence.jetbrains.com/display/TW/Command+Line+Remote+Run+Tool" rel="noopener noreferrer"&gt;JetBrains TeamCity: Remote Run Command-Line Tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We made use of these set of vendor supported tools for creating a personal build pipeline, whereby changes made locally on the user's machine, were tested on a remote build machine for verification purposes.&lt;/p&gt;

&lt;p&gt;The "personal build pipeline" that I developed was contingent on there being a few tools available.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perforce Changelist Tool&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A user-facing tool that would construct a valid changelist description that would include all relevant information about a change, including a link or build ID (i.e. a build that is succeeding with the changes found in the changelist) to a valid personal build, a valid link or ID (i.e. approved or greenlit) to a Helix Swarm code review.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Perforce Trigger Handler&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A Perforce daemon trigger (&lt;code&gt;p4d&lt;/code&gt; trigger) that was responsible for verifying that the changelist could be submitted by checking the contents of the changelist and its description.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Personal Build Tool&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A tool that would aggregate local changes, and submit those changes directly to the build server for triggering a personal build.&lt;/li&gt;
&lt;li&gt;Otherwise known as the &lt;em&gt;Content Validation Tool&lt;/em&gt;, as it was previously only ever meant to be used for testing changes made to packages and content assets.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;You can read more about the personal build pipeline from &lt;a href="https://github.com/splash-damage/unrealfest" rel="noopener noreferrer"&gt;the UnrealFest presentation here.&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enable developers to conveniently run "personal builds" containing changes that are stored locally on the user's machine.&lt;/li&gt;
&lt;li&gt;Prevent developers from breaking the build by ensuring code compiles in a "live" build system environment.&lt;/li&gt;
&lt;li&gt;Ensure that change list descriptions or commit messages contain relevant information that conform to a enforced standard.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Personal Build Tool
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;Personal Build Tool&lt;/em&gt; is a desktop application responsible for aggregating local changes in a Perforce workspace, submitting those local changes directly to the TeamCity server, and subsequently triggering a new build on TeamCity with the submitted changes. The local changes would be applied on top of the head revision of the repository checkout from mainline.&lt;/p&gt;

&lt;h3&gt;
  
  
  API
&lt;/h3&gt;

&lt;p&gt;TeamCity exposes some of its functionality through a HTTP based "RESTful" API. This &lt;a href="https://en.wikipedia.org/wiki/Representational_state_transfer" rel="noopener noreferrer"&gt;REST&lt;/a&gt; API is used in conjunction with the &lt;a href="https://www.jetbrains.com/help/teamcity/remote-run.html" rel="noopener noreferrer"&gt;Remote Run plugin&lt;/a&gt; to perform a few tasks including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking the current status of the build, including whether or not it is queued, if it's running, and also retrieving the most recent build log data.&lt;/li&gt;
&lt;li&gt;Starting or queueing a new build with the changes generated with the patch file generated by the &lt;em&gt;Personal Build Tool&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Cancelling the build if there are any changes.&lt;/li&gt;
&lt;li&gt;Updating the build description or information with a checksum generated from the local modifications on disk.

&lt;ul&gt;
&lt;li&gt;This is used as part of the verification step with the &lt;em&gt;Perforce Trigger Handler&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.jetbrains.com/help/teamcity/rest-api.html" rel="noopener noreferrer"&gt;Information on the TeamCity REST API can be found here.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Decompiling Tools
&lt;/h3&gt;

&lt;p&gt;Like most &lt;a href="https://www.jetbrains.com/" rel="noopener noreferrer"&gt;JetBrains&lt;/a&gt; products, the command-line runner was a server-sided plugin that was written in Java. This server-sided plugin was responsible for handling requests for personal builds from another command-line based tool that was also written in Java.&lt;/p&gt;

&lt;p&gt;This command-line tool enables a user to specify one or more local files, a build type configuration ID, along with additional parameters (optional), and then submit those changes directly to the TeamCity server for starting a new personal build.&lt;/p&gt;

&lt;p&gt;In order to achieve this, the command-line tool constructs a "patch file" which comprises of the user's local changes, along with additional metadata that enables the build system to understand where the files are to be placed in relation to the Version Control System (VCS). In this instance, the VCS was Perforce&lt;/p&gt;

&lt;h4&gt;
  
  
  Constructing the Patch File
&lt;/h4&gt;

&lt;p&gt;The patch file is a simple binary file that consists of metadata, and the raw contents of local changes from the user's machine. The local changes included in this patch file is what the build system will use, on top of the existing head revision from the mainline of the code repository in the VCS (in this instance, Perforce). The goal of this entire system is to prevent the user from submitting local changes into the repository without first verifying whether or not those changes would compile successfully in a "live" build system environment (e.g. a machine not belonging to the user). This is the entire premise of the personal build system.&lt;/p&gt;

&lt;p&gt;The structure of format specification for this patch file are unfortunately not documented anywhere, and it will require you to decompile the Java-based command-line remote run tool in order to understand how the patch file is constructed. The code itself is relatively simple enough to understand even without prior knowledge of Java.&lt;/p&gt;

&lt;p&gt;The patch file consisted of information including the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version control system information, and which branch or stream this was meant to use as the head revision.&lt;/li&gt;
&lt;li&gt;Information about each locally changed file, including their names, total file size, and where they are meant to be placed on a build agent when building in a live environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this knowledge, we were then able to develop a basic serialization mechanism as part of our &lt;em&gt;Personal Build Tool&lt;/em&gt;, that was able to aggregate all local changes on the system, and produce a "patch" file that can be used for starting a personal build with.&lt;/p&gt;

&lt;p&gt;This patch file would then be submitted directly to the build system, so that the developer can avoid submitting changes to the repository first. The changes found in the patch file would then be combined with the head revision of changes in the repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  Perforce Changelist Tool
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;Perforce Changelist Tool&lt;/em&gt; is a simple desktop application that produces a changelist description that conforms to a format or "specification" that the &lt;em&gt;Perforce Trigger Handler&lt;/em&gt; expects. The goal of having a specific format or specification for changelist descriptions is to ensure that they are both informative, and consistent. The information found in a changelist description assists in tracking the completion of work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fbf21d10a40%2FGrowing-a-Healthy-UE4-Ecosystem-How-Splash-Damage-approaches-development-Google-Chrome_8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fbf21d10a40%2FGrowing-a-Healthy-UE4-Ecosystem-How-Splash-Damage-approaches-development-Google-Chrome_8.jpg" title="Perforce Changelist Tool" alt="The Perforce Changelist Tool, as documented in the UnrealFest presentation by Valentin Galea from Splash Damage." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Perforce Changelist Tool, as documented in the UnrealFest presentation by Valentin Galea from Splash Damage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;Perforce Changelist Tool&lt;/em&gt; is a simple desktop application that is launched from &lt;a href="https://www.perforce.com/downloads/helix-visual-client-p4v" rel="noopener noreferrer"&gt;P4V&lt;/a&gt;, the GUI application "frontend" for Perforce.&lt;/p&gt;

&lt;h4&gt;
  
  
  Perforce Integration
&lt;/h4&gt;

&lt;p&gt;When the tool is launched for the first-time from a context menu, the tool will automatically integrate itself with &lt;a href="https://www.perforce.com/downloads/helix-visual-client-p4v" rel="noopener noreferrer"&gt;P4V&lt;/a&gt; by locating the configuration files that are available under the following path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%USERPROFILE%\.p4qt\customtools.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an XML based file that gets loaded by &lt;a href="https://www.perforce.com/downloads/helix-visual-client-p4v" rel="noopener noreferrer"&gt;P4V&lt;/a&gt; upon when the application is started. It provides definitions for custom tools that the client must use, including additional context menu actions that enable you to launch an external application with injected parameters (including the number of a changelist).&lt;/p&gt;

&lt;p&gt;The changelist number then gets passed to the &lt;em&gt;Perforce Changelist Tool&lt;/em&gt; through a command-line argument (i.e. &lt;code&gt;--changelist 1234&lt;/code&gt;), which it then uses to load additional information about a changelist (e.g. files checked out, author, time created).&lt;/p&gt;

&lt;h4&gt;
  
  
  Generated Output
&lt;/h4&gt;

&lt;p&gt;The purpose of the tool is to populate the description of a changelist with all required information in a format that is recognisable by the &lt;em&gt;Perforce Trigger Handler&lt;/em&gt;, which is described later on in this article. The _Perforce Changelist Tool _makes use of a predetermined "specification", that was designed for ensuring that all subsequent changelists submitted into the repository had clear and concise information associated with them.&lt;/p&gt;

&lt;p&gt;This "specification" ensured that the following fields were populated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Helix Swarm Review URL and ID&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;This only was required if the changes contained code-based changes.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Jira Issue ID&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;If the change being submitted was associated with a Jira user story, then this was required.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Tags&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;These were optional identifiers that simply made it easier to observe change lists at a glance when scrolling through changelist history in P4V.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.perforce.com/manuals/p4v/Content/P4V/advanced_options.custom.html" rel="noopener noreferrer"&gt;Read more documentation here about integrating "custom tools" with P4V.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;The tool made use of C#, WPF, and various other third-party libraries for UI controls and styling. In addition, the tool made use of &lt;a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=builder-syntax" rel="noopener noreferrer"&gt;toast notifications&lt;/a&gt; for notifying the user when an operation completed, and presented progress information in the task bar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Perforce Trigger Handler
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;Perforce Trigger Handler&lt;/em&gt; is a lightweight console-based application that is responsible for validating submitted changelists, by performing a series of checks. If the changelist is considered invalid, the application will terminate with an exit code of 1 (or anything not 0), otherwise if deemed acceptable the application will terminate gracefully with an exit code of 0. The behavior of this application is largely dictated by the specification for Perforce triggers &lt;a href="https://www.perforce.com/perforce/r14.2/manuals/p4sag/chapter.scripting.html" rel="noopener noreferrer"&gt;that is documented here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As stated, the console application performs a series of checks including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validation Checks&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Does it contain all required information?&lt;/li&gt;
&lt;li&gt;Does it conform to the specified format for changelist descriptions?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Time of Day&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Is this changelist being submitted outside of work hours?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Build Health&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Is the latest build on TeamCity currently failing?&lt;/li&gt;
&lt;li&gt;If so, is the author of the changelist the one who broke the build?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Validation Checks
&lt;/h3&gt;

&lt;p&gt;Each changelist description needed to conform to an enforced standard of what a changelist description should look like. The specification stipulates how a changelist should appear, and what information it should contain. This information is generally populated by the aforementioned &lt;em&gt;Perforce Changelist Tool&lt;/em&gt;, that ingests all necessary data from external sources and then produces a conformant changelist description.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Health
&lt;/h3&gt;

&lt;p&gt;Before another user can submit code into the repository, it's important that existing build breakages are resolved otherwise subsequent commits to the code repository could worsen the state of the build. If the user submitting the changelist is not the one considered to be responsible for breaking the build, then the user will be prevented from submitting their changelist until the build goes green.&lt;/p&gt;

&lt;p&gt;This ensures that existing build breakages are urgently resolved, otherwise there will be a blockage in the pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time of Day
&lt;/h3&gt;

&lt;p&gt;This a simple check that ensures that there is no one outside of working hours that is submitting changes into the repository, and that there's no possibility that the build will fail before people arrive at the office the following day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fe94093fb2a%2FGrowing-a-Healthy-UE4-Ecosystem-How-Splash-Damage-approaches-development-Google-Chrome_10.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fe94093fb2a%2FGrowing-a-Healthy-UE4-Ecosystem-How-Splash-Damage-approaches-development-Google-Chrome_10.jpg" title="Perforce Trigger Handler - Failed Changelist Submission" alt="The message presented to users through P4V if their changelist was considered invalid. The output seen in the image is produced by the Perforce Trigger Handler, a server-sided console application responsible for performing validation checks against the changelist beign submitted." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The message presented to users through P4V if their changelist was considered invalid. The output seen in the image is produced by the Perforce Trigger Handler, a server-sided console application responsible for performing validation checks against the changelist beign submitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The decision process for submitting the changelist looked a little like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FPersonalBuildFlow.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2FPersonalBuildFlow.jpg" title="Personal Build Pipeline" alt="A simple flow diagram of how the personal build pipeline works." width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Perforce Trigger Handler had to perform a series of checks before allow for a changelist to be submitted to the Perforce server. The Perforce Trigger Handler could ultimately prevent a changelist from being accepted.&lt;/p&gt;

&lt;p&gt;The above diagram encapsulates what happens when a changelist is submitted to the Perforce server.&lt;/p&gt;




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

&lt;p&gt;Personal build pipelines are an extremely powerful tool, but they likely will not suit everyone's needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problems
&lt;/h3&gt;

&lt;p&gt;Personal builds are a great means for ensuring that all changes submitted into the mainline stream of the code repository are vigorously tested, and are less likely to break the build once changes are submitted into the repository.&lt;/p&gt;

&lt;p&gt;Of course, the biggest issue with using such a pipeline is that it significantly reduces the speed of iteration across the team. Now, the team must successfully complete multiple steps in order to be able to submit a change into the repository.&lt;/p&gt;

&lt;p&gt;There are some instances where this is a hindrance more than a benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exceptions
&lt;/h3&gt;

&lt;p&gt;With the usage of a specifically configured "magic" keyword, it's possible to enable for the &lt;em&gt;Perforce Trigger Handler&lt;/em&gt; to ignore performing all checks before allow a changelist to be submitted to the repository.&lt;/p&gt;

&lt;p&gt;There is of course the possibility that such a "magic" keyword could be abused by members of the team, which is why it was imperative for the &lt;em&gt;Perforce Trigger Handler&lt;/em&gt; to send out email notifications to team leads each time this particular keyword was used. This was simply to ensure accountability, and so that changes that were submitted using this keyword that were considered problematic can be accounted for (and reverted if necessary).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F408b590c7f%2FGrowing-a-Healthy-UE4-Ecosystem-How-Splash-Damage-approaches-development-Google-Chrome_12.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2F408b590c7f%2FGrowing-a-Healthy-UE4-Ecosystem-How-Splash-Damage-approaches-development-Google-Chrome_12.jpg" title="Rules for Submission Validation" alt="Splash Damage makes use of a special [BuildFix] keyword found in the changelist description to get around the Perforce Trigger Handler's preventative measures." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Splash Damage makes use of a special "[BuildFix]" keyword found in the changelist description to get around the Perforce Trigger Handler's preventative measures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build System Resources
&lt;/h3&gt;

&lt;p&gt;As stated in the presentation by &lt;a href="https://lucshelton.com/career/splash-damage/" rel="noopener noreferrer"&gt;Splash Damage&lt;/a&gt;, allowing for personal builds at any time of the day, and launched on request by any development team member, means that there is an significantly increased demand on the "build farm" (i.e. systems dedicated for doing nothing other than build the game). Therefore, it's important that there are sufficient build machines made available if a personal build system is adopted, and that those build machines are placed into separate agent pools from the ones dedicated to "mission critical" build type configurations (i.e. ones that must run on a regular basis, or at a particular time of day).&lt;/p&gt;

&lt;p&gt;This then ensures that "mission critical" build type configurations will continue to run uninterrupted (and not placed in queues) at any time of day, or whichever time of day that they are configured to run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comments
&lt;/h2&gt;

&lt;p&gt;Having a personal build system available to a large-scale development team is an invaluable asset when it is integrated correctly. The fundamental goal is to ensure that a development team can continue to work at a fast and regular cadence without hindrance, and without compromising on the quality of the "product" they are delivering.&lt;/p&gt;

&lt;p&gt;Whether or not you wish to integrate a personal build system with your team depends on a few things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How often are users committing code?&lt;/li&gt;
&lt;li&gt;How big is the team?&lt;/li&gt;
&lt;li&gt;Does your team have sufficient build system resources available (i.e. build agents)?&lt;/li&gt;
&lt;li&gt;Do you have sufficient development resources for building the aforementioned components?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  User Experience
&lt;/h3&gt;

&lt;p&gt;It's important to note that if you do not provide adequate user-friendly tooling around this system, then you will very quickly frustrate and demoralize members of the team. This is understandable, as you are expecting them to adopt an otherwise contrived workflow for results that may not be immediately clear. If you expect users to perform additional steps before submitting their work, then be sure to do as much as you can for them, and with minimal interference.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tooling
&lt;/h4&gt;

&lt;p&gt;Here's what each of the tools in the pipeline did for the user to mitigate confusion, frustration, or the amount of time required to read documentation.&lt;/p&gt;

&lt;h5&gt;
  
  
  Perforce Changelist Tool
&lt;/h5&gt;

&lt;h6&gt;
  
  
  P4V Integration
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;On first-time launch, the tool made itself launchable through P4V by locating P4V's configuration file on disk, and injecting its own configuration into the file, so the tool was available as a context menu option upon right-clicking any changelist in P4V.&lt;/li&gt;
&lt;li&gt;Automatically pulled information about the user and their superior by making use of LDAP or Active Directory, and made "smart" suggestions about who the reviewers were.&lt;/li&gt;
&lt;li&gt;Automatically pulled information about code reviews associated with the changelist (from Helix Swarm).&lt;/li&gt;
&lt;li&gt;Automatically detected which Perforce client the user was using by iterating over all locally available Perforce clients and determining which one the tool resided in (the tool was distributed through Perforce).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.perforce.com/manuals/p4v/Content/P4V/advanced_options.custom.html" rel="noopener noreferrer"&gt;Read more documentation here about "custom tools" in P4V.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Typically custom tools configuration are typically stored on the user's machine at the following path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%USERPROFILE%\.p4qt\customtools.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a simple XML file that any external tool can simply inject values into, providing that the changes made compliant with the XSD (XML Schema Definition).&lt;/p&gt;

&lt;h5&gt;
  
  
  Perforce Build Tool
&lt;/h5&gt;

&lt;h6&gt;
  
  
  P4V Integration
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;Similar to how &lt;em&gt;Perforce Changelist Tool&lt;/em&gt; behaved, the tool integrated itself as part of P4V custom tools configuration file that is located at the path mentioned earlier on in this article. &lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Personal Builds Support
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;The tool made use of the official JetBrains server-sided plugin for performing personal builds, by constructing "patch files" that the TeamCity server instance would recognise. In order to achieve this, we decompiled the existing CLI tooling, and understood how it was constructing the patch files.&lt;/li&gt;
&lt;li&gt;The tool automatically started new builds on the TeamCity server instance with local changes, and provided the user with regular feedback in the form of &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.windows.shell.taskbariteminfo.progressstate?view=net-5.0" rel="noopener noreferrer"&gt;taskbar progress information&lt;/a&gt; or toast notifications through Windows 10.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Perforce Trigger Handler
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Remotely Manageable and Configurable
&lt;/h5&gt;

&lt;p&gt;The &lt;em&gt;Perforce Trigger Handler&lt;/em&gt; is a command-line application that sits on the same server as the Perforce daemon. More often than not, developers on the team won't have (nor will they require) direct access to the system that powers the Perforce daemon. Therefore, being able to remotely control or alter the behaviour of the &lt;em&gt;Perforce Trigger Handler&lt;/em&gt; on the server is important. In our case, the approach for enabling this was simple. We had the application run from a network share that was accessible by both developers, and the server that the Perforce daemon operated from.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Perforce: Formatting, Scripting, and the Command-line Interface</title>
      <dc:creator>Luc Shelton</dc:creator>
      <pubDate>Wed, 10 Jul 2019 12:50:52 +0000</pubDate>
      <link>https://dev.to/theloveduckie/perforce-formatting-scripting-and-the-command-line-interface-3bm7</link>
      <guid>https://dev.to/theloveduckie/perforce-formatting-scripting-and-the-command-line-interface-3bm7</guid>
      <description>&lt;p&gt;In this blog post I will discuss some of the convenient tricks that you can leverage when navigating Perforce and its command-line interface. A lot of these are well documented in &lt;a href="https://community.perforce.com/s/article/15148" rel="noopener noreferrer"&gt;a post that's available on Perforce's website&lt;/a&gt;, but this blog post will look at a few of those features with more detail.&lt;/p&gt;




&lt;h3&gt;
  
  
  Formatting Output
&lt;/h3&gt;

&lt;p&gt;The Perforce command-line interface features support for a variety of useful global command-line arguments that can be used for altering the output and and operation of the command-line tool.&lt;/p&gt;

&lt;p&gt;The output from the Perforce command-line interface can be formatted using a combination of the &lt;code&gt;-ztag&lt;/code&gt;, &lt;code&gt;-e&lt;/code&gt;, and &lt;code&gt;-F&lt;/code&gt; command-line parameters.&lt;/p&gt;

&lt;h4&gt;
  
  
  Using -ztag parameter
&lt;/h4&gt;

&lt;p&gt;The -&lt;code&gt;ztag&lt;/code&gt; global command-line parameter displays the standard output from the Perforce command-line interface in a format that conforms to a specification.&lt;/p&gt;

&lt;p&gt;The specification is denoted in that each "key" or "attribute" of an object, is prefixed with "... " and then proceeded by the name of the key. The data for that particular attribute or property is then displayed afterwards using a white-space character as the delimiter. Find below an example.&lt;/p&gt;

&lt;h5&gt;
  
  
  Usage
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p4 -ztag info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which produces an an output similar to  the screenshot below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fdc4518b183%2Fcommandpromptexample-v4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flucshelton.com%2Fassets%2FUploads%2Fdc4518b183%2Fcommandpromptexample-v4.png" title="Command Prompt Example" alt="Command Prompt Example" width="684" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The resulting output from executing "p4 -ztag info"&lt;/p&gt;

&lt;p&gt;More information on the &lt;code&gt;p4 info&lt;/code&gt; command &lt;a href="https://www.perforce.com/manuals/cmdref/Content/CmdRef/p4_info.html" rel="noopener noreferrer"&gt;can be found here.&lt;/a&gt;``&lt;/p&gt;

&lt;h4&gt;
  
  
  Using -e and -F parameters
&lt;/h4&gt;

&lt;p&gt;These global command-line arguments are particularly useful for filtering and identifying "keys", "properties", or "attributes" from command outputs. It's possible to see which "keys" are available to filter or "format" our output with by using the &lt;code&gt;-e&lt;/code&gt; parameter. The resulting output should look something like this.&lt;/p&gt;

&lt;h5&gt;
  
  
  Usage
&lt;/h5&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
p4 -e info&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-e&lt;/code&gt; in this command will now reveal all "formattable" data that is produced by the command info. The parameter "&lt;code&gt;info&lt;/code&gt;" can be replaced with another command including "&lt;code&gt;clients&lt;/code&gt;", which can display all clients that are owned by the currently logged in user.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
p4 -e clients&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The above command will display available parameters for each entry that is displayed the "&lt;code&gt;clients&lt;/code&gt;" command. Look below at an example of a resulting entry.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
info: **REDACTED**&lt;br&gt;
code0 352327909 (sub 229 sys 6 gen 0 args 5 sev 1 uniq 6373)&lt;br&gt;
... code0 352327909&lt;br&gt;
... fmt0 %domainType% %domainName% %updateDate% %'root'% %domainMount% '%description%'&lt;br&gt;
... domainType Client&lt;br&gt;
... domainName **REDACTED**... updateDate 2021/03/18&lt;br&gt;
... domainMount **REDACTED**... description Created by **REDACTED**&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Under the entry &lt;code&gt;fmt0&lt;/code&gt;, we can see that there is a space delimited list of available formatting parameters that we can use. Let's try using the clients command again using one of these parameters.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
p4 -F %domainName% clients&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The above command should now output each entry that is emitted by the clients command, but only the name. This is because we have selectively chosen which entry to output using &lt;code&gt;%domainName%&lt;/code&gt; as the selector.&lt;/p&gt;

&lt;p&gt;The same command can be changed to another available formatting parameter such as &lt;code&gt;%updateDate%&lt;/code&gt;, which should display the last time the respective client has been updated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
p4 -F %updateDate% clients&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Scripting
&lt;/h3&gt;

&lt;p&gt;Understanding how these global command-line parameters can be used is incredibly powerful. With this, we can now write applications that are capable of executing these commands and parsing the &lt;code&gt;stdout&lt;/code&gt; stream from the commands we execute with minimal time spent on parsing.&lt;/p&gt;

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