<?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: Andrew Welch</title>
    <description>The latest articles on DEV Community by Andrew Welch (@gaijinity).</description>
    <link>https://dev.to/gaijinity</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%2F218985%2F2daf1d64-86e4-4568-8acf-2de82361f042.jpg</url>
      <title>DEV Community: Andrew Welch</title>
      <link>https://dev.to/gaijinity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaijinity"/>
    <language>en</language>
    <item>
      <title>Speeding Up Tailwind CSS Builds</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Tue, 13 Oct 2020 12:29:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/speeding-up-tailwind-css-builds-55d3</link>
      <guid>https://dev.to/gaijinity/speeding-up-tailwind-css-builds-55d3</guid>
      <description>&lt;h1&gt;
  
  
  Speeding Up Tailwind CSS Builds
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Learn how to opti­mize your Tail­wind CSS PostC­SS build times to make local devel­op­ment with Hot Mod­ule Replace­ment or Live Reload orders of mag­ni­tude faster!
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--73q3GHmI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/10743/speeding-up-tailwind-css-builds2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--73q3GHmI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/10743/speeding-up-tailwind-css-builds2.jpg" alt="Speeding up tailwind css builds2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailwindcss.com/"&gt;Tail­wind CSS&lt;/a&gt; is a util­i­ty-first CSS frame­work that we’ve been using for sev­er­al years, and it’s been pret­ty fan­tas­tic. We first talked about it on the &lt;a href="https://devmode.fm/episodes/tailwind-css-utility-first-css-with-adam-watham"&gt;Tail­wind CSS util­i­ty-first CSS with Adam Wathan&lt;/a&gt; episode of &lt;a href="https://devmode.fm/"&gt;dev​Mode​.fm&lt;/a&gt; way back in 2017!&lt;/p&gt;

&lt;p&gt;We use it in the base of every project we do, as part of a web­pack build process described in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;One issue that we’ve run into recent­ly is that build times can be slow in local devel­op­ment, where you real­ly want the speed as you are build­ing out the site CSS.&lt;/p&gt;

&lt;p&gt;This arti­cle will lead you through how to opti­mize your Tail­wind CSS build process so that your &lt;a href="https://webpack.js.org/concepts/hot-module-replacement/"&gt;hot mod­ule replace­ment&lt;/a&gt; / live reload will be fast again, and explains what’s going on under the hood.&lt;/p&gt;

&lt;p&gt;So let’s dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Fram­ing the Problem
&lt;/h2&gt;

&lt;p&gt;The prob­lem I was hav­ing was that a sim­ple edit of one of my .pcss files would result in a good 10 sec­ond or more delay as the CSS was rebuilt.&lt;/p&gt;

&lt;p&gt;Grant­ed, one of the love­ly things about Tail­wind CSS is that you don’t have to write much cus­tom CSS; most­ly you’re putting util­i­ty class­es in your markup.&lt;/p&gt;


                                How­ev­er, when you do want to add cus­tom &lt;span&gt;CSS&lt;/span&gt;, it should­n’t be this painful
                            

&lt;p&gt;So if I want­ed to do some­thing like change the back­ground col­or of a par­tic­u­lar CSS class, instead of the instant feed­back I was used to from web­pack hot mod­ule replace­ment, I’d be wait­ing a good while for it to recompile.&lt;/p&gt;

&lt;p&gt;I wrote up &lt;a href="https://github.com/tailwindlabs/tailwindcss/issues/2544"&gt;Tail­wind CSS issue #2544&lt;/a&gt;, and also cre­at­ed a min­i­mal GitHub repo &lt;a href="https://github.com/nystudio107/tailwind-css-performance"&gt;nys­tu­dio107/­tail­wind-css-per­for­mance&lt;/a&gt; to repro­duce it.&lt;/p&gt;

&lt;p&gt;But then start­ed spelunk­ing a bit fur­ther to see if I could find a way to mit­i­gate the situation.&lt;/p&gt;

&lt;p&gt;I found a tech­nique that sped up my build times immea­sur­ably, and you can use it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prob­lem Setup
&lt;/h2&gt;

&lt;p&gt;My orig­i­nal base Tail­wind CSS set­up looked rough­ly 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;
css
├── app.pcss
├── components
│ ├── global.pcss
│ ├── typography.pcss
│ └── webfonts.pcss
├── pages
│ └── homepage.pcss
└── vendor.pcss

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

&lt;/div&gt;



&lt;p&gt;The meat of this is the app.css, which looked 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;
/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 */
 @import "tailwindcss/base";

/**
 * This injects any component classes registered by plugins.
 *
 */
@import 'tailwindcss/components';

/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';

/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@import 'tailwindcss/utilities';

/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';

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

&lt;/div&gt;



&lt;p&gt;This app.pcss file then gets import­ed into my app.ts via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Import our CSS
import '../css/app.pcss';

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

&lt;/div&gt;



&lt;p&gt;This caus­es web­pack to be aware of it, so the .pcss gets pulled into the build pipeline, and gets processed.&lt;/p&gt;

&lt;p&gt;And then my postcss.config.js file looked 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;
module.exports = {
    plugins: [
        require('postcss-import')({
            plugins: [
            ],
            path: ['./node_modules'],
        }),
        require('tailwindcss')('./tailwind.config.js'),
        require('postcss-preset-env')({
            autoprefixer: { },
            features: {
                'nesting-rules': true
            }
        })
    ]
};

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

&lt;/div&gt;



&lt;p&gt;And we have a super-stan­dard tailwind.config.js file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
module.exports = {
  theme: {
    // Extend the default Tailwind config here
    extend: {
    },
    // Replace the default Tailwind config here
  },
  corePlugins: {},
  plugins: [],
};

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Prob­lem
&lt;/h2&gt;

&lt;p&gt;This set­up above is pret­ty much what is laid out Tail­wind CSS docs sec­tion on &lt;a href="https://tailwindcss.com/docs/using-with-preprocessors#using-post-css-as-your-preprocessor"&gt;Using with Pre­proces­sors: PostC­SS as your pre­proces­sor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Tail­wind CSS is built using &lt;a href="https://postcss.org/"&gt;PostC­SS&lt;/a&gt;, so it makes sense that if you’re using a build­chain that uses &lt;a href="https://webpack.js.org/"&gt;web­pack&lt;/a&gt; or &lt;a href="https://laravel.com/docs/8.x/mix"&gt;Lar­avel Mix&lt;/a&gt; (which uses web­pack under the hood) or &lt;a href="https://gulpjs.com/"&gt;Gulp&lt;/a&gt; that you’d lever­age PostC­SS there, too.&lt;/p&gt;

&lt;p&gt;The doc­u­men­ta­tion rec­om­mends that if you’re using the &lt;a href="https://github.com/postcss/postcss-import"&gt;postc­ss-import&lt;/a&gt; plu­g­in (which we are, and is a very com­mon­ly used PostC­SS plu­g­in), that you change this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
@tailwind base;
@import "./custom-base-styles.css";

@tailwind components;
@import "./custom-components.css";

@tailwind utilities;
@import "./custom-utilities.css";

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

&lt;/div&gt;



&lt;p&gt;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;
@import "tailwindcss/base";
@import "./custom-base-styles.css";

@import "tailwindcss/components";
@import "./custom-components.css";

@import "tailwindcss/utilities";
@import "./custom-utilities.css";

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

&lt;/div&gt;



&lt;p&gt;This is because postcss-import strict­ly adheres to the CSS spec and dis­al­lows @import state­ments any­where except at the very top of a file, so we can’t mix them in togeth­er with our oth­er CSS or @tailwind directives.&lt;/p&gt;


                                And this is where things start to go pear-shaped
                            

&lt;p&gt;When we use @import "tailwindcss/utilities"; instead of @tailwind utilities; all that’s real­ly hap­pen­ing is the tailwindcss/utilities.css file is imported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
@tailwind utilities;

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

&lt;/div&gt;



&lt;p&gt;So we’re just side-step­ping the @import loca­tion require­ment in postcss-import by adding a lay­er of indirection.&lt;/p&gt;

&lt;p&gt;But we can have a look at node_modules/tailwindcss/dist/ to get a rough idea how large this gen­er­at­ed file is going to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
❯ ls -alh dist
total 43568
drwxr-xr-x 12 andrew staff 384B Sep 19 11:34 .
drwxr-xr-x 19 andrew staff 608B Sep 19 11:35 ..
-rw-r--r-- 1 andrew staff 11K Oct 26 1985 base.css
-rw-r--r-- 1 andrew staff 3.1K Oct 26 1985 base.min.css
-rw-r--r-- 1 andrew staff 1.9K Oct 26 1985 components.css
-rw-r--r-- 1 andrew staff 1.3K Oct 26 1985 components.min.css
-rw-r--r-- 1 andrew staff 5.4M Oct 26 1985 tailwind-experimental.css
-rw-r--r-- 1 andrew staff 4.3M Oct 26 1985 tailwind-experimental.min.css
-rw-r--r-- 1 andrew staff 2.3M Oct 26 1985 tailwind.css
-rw-r--r-- 1 andrew staff 1.9M Oct 26 1985 tailwind.min.css
-rw-r--r-- 1 andrew staff 2.2M Oct 26 1985 utilities.css
-rw-r--r-- 1 andrew staff 1.8M Oct 26 1985 utilities.min.css

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

&lt;/div&gt;



&lt;p&gt;(inci­den­tal­ly, if you look close­ly, you’ll also have learned Adam Wathan’s birthday)&lt;/p&gt;

&lt;p&gt;We can see that the utilities.css file weighs in at a hefty 2.2M itself; and while this comes out in the wash for pro­duc­tion builds when you’re using &lt;a href="https://purgecss.com/"&gt;PurgeC­SS&lt;/a&gt; as rec­om­mend­ed in &lt;a href="https://tailwindcss.com/docs/controlling-file-size"&gt;Tail­wind CSS docs: Con­trol­ling file size&lt;/a&gt;, it can be prob­lem­at­ic for local development.&lt;/p&gt;

&lt;p&gt;So but why is this a prob­lem? If we’re just @import​’ing the file, why would this be so slow?&lt;/p&gt;

&lt;p&gt;The rea­son twofold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Although Tail­wind CSS has &lt;a href="https://github.com/tailwindlabs/tailwindcss/compare/v1.7.1..v1.7.2#diff-cf591df84e9e87cb0fa15f602337bfc34479a6a5eedb03fffce0cdf0e756bd80R298"&gt;opti­miza­tions in place&lt;/a&gt;to mit­i­gate it, there’s still a ton of CSS gen­er­a­tion that has to hap­pen each time a change is made in any of your .pcss files. We lumped them all in togeth­er, so they all get rebuild together.&lt;/li&gt;
&lt;li&gt;The postcss-import plu­g­in &lt;a href="https://github.com/postcss/postcss-import/blob/master/index.js#L129"&gt;actu­al­ly pars­es&lt;/a&gt; any files you @import, look­ing for oth­er @import state­ments in that import­ed file, and it does this on the CSS that the @tailwind direc­tive gen­er­ates, too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And our result­ing utilities.css file has by default over 100,000 lines of CSS gen­er­at­ed &amp;amp; parsed through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
❯ wc -l utilities.css
  102503 utilities.css

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

&lt;/div&gt;



&lt;p&gt;So that’s not good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solu­tion
&lt;/h2&gt;

&lt;p&gt;So what can we do? It’s inher­ent to Tail­wind CSS that it’s going to cre­ate a ton of util­i­ty CSS for you, and that gen­er­a­tion can only be opti­mized so much.&lt;/p&gt;

&lt;p&gt;I start­ed think­ing of var­i­ous caching mech­a­nism that could be added to Tail­wind CSS, but I real­ized the right solu­tion was to just lever­age the platform.&lt;/p&gt;

&lt;p&gt;I remem­bered an old Comp Sci maxim:&lt;/p&gt;


                                The fastest code is the code you nev­er have to execute
                            

&lt;p&gt;We’re already using web­pack, which adroit­ly han­dles tons of imports of var­i­ous kinds through a vari­ety of load­ers… why not just break our .pcss up into chunks, and let web­pack sort it out?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solu­tion Setup
&lt;/h2&gt;

&lt;p&gt;So that’s exact­ly what I did in the &lt;a href="https://github.com/nystudio107/tailwind-css-performance/tree/solution"&gt;solu­tion branch of nys­tu­dio107/­tail­wind-css-per­for­mance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now our CSS direc­to­ry looks 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;
css
├── app-base.pcss
├── app-components.pcss
├── app-utilities.pcss
├── components
│ ├── global.pcss
│ ├── typography.pcss
│ └── webfonts.pcss
├── pages
│ └── homepage.pcss
├── tailwind-base.pcss
├── tailwind-components.pcss
├── tailwind-utilities.pcss
└── vendor.pcss

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

&lt;/div&gt;



&lt;p&gt;Our app.pcss file has been chopped up into 6 sep­a­rate .pcss files that cor­re­spond with Tail­wind’s base, components, and utilities methodology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 */
@tailwind base;



/**
 * Here we add custom base styles, applied after the tailwind-base
 * classes
 *
 */



/**
 * This injects any component classes registered by plugins.
 *
 */
@tailwind components;



/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';



/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@tailwind utilities;



/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';

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

&lt;/div&gt;



&lt;p&gt;And then these .pcss files then get import­ed into our app.ts via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Import our CSS
import '../css/tailwind-base.pcss';
import '../css/app-base.pcss';
import '../css/tailwind-components.pcss';
import '../css/app-components.pcss';
import '../css/tailwind-utilities.pcss';
import '../css/app-utilities.pcss';

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

&lt;/div&gt;



&lt;p&gt;Noth­ing else was changed in our con­fig or setup.&lt;/p&gt;

&lt;p&gt;This allows web­pack to han­dle each chunk of import­ed .pcss sep­a­rate­ly, so the Tail­wind-gen­er­at­ed CSS (and impor­tant­ly the huge utilities.css) only needs to be rebuilt if some­thing that affects it (like the tailwind.config.js) is changed.&lt;/p&gt;


                                Lever­age the platform
                            

&lt;p&gt;Changes to any of the .pcss files that we write are rebuilt sep­a­rate­ly &amp;amp; instantly.&lt;/p&gt;

&lt;p&gt;💥&lt;/p&gt;

&lt;h2&gt;
  
  
  Bench­mark­ing Prob­lem vs. Solution
&lt;/h2&gt;

&lt;p&gt;I did a few infor­mal bench­marks while test­ing all of this out. I used a 2019 Mac­Book Pro with 64gb RAM.&lt;/p&gt;

&lt;p&gt;For the test, all I did was change the background-color: yellow; to background-color: blue; in the css/components/global.pcss file.&lt;/p&gt;

&lt;p&gt;When we do a rebuild using the ​“Prob­lem Set­up”, the webpack-dev-server [WDS] out­put in the browser’s Devel­op­er JavaScript Con­sole looks 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;
[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR] - ../src/css/app.pcss
[HMR] App is up to date.

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

&lt;/div&gt;



&lt;p&gt;…and it took 11.74s to do this recom­pile on my 2019 Mac­Book Pro.&lt;/p&gt;

&lt;p&gt;Notice that it rebuild the entire app.pcss here.&lt;/p&gt;

&lt;p&gt;When we do a rebuild using the ​“Solu­tion Set­up”, the webpack-dev-server [WDS] out­put in the browser’s Devel­op­er JavaScript Con­sole looks 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;
[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR] - ../src/css/app-components.pcss
[HMR] App is up to date.

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

&lt;/div&gt;



&lt;p&gt;…and it only took 0.52s to do this HMR rebuild.&lt;/p&gt;

&lt;p&gt;Notice that it rebuilt &lt;em&gt;only&lt;/em&gt; the app-components.pcss here, and deliv­ered just the diff of it to the brows­er in a high­ly effi­cient manner.&lt;/p&gt;


                                So with our changes, it’s now &lt;span&gt;22&lt;/span&gt;.&lt;span&gt;5&lt;/span&gt;x faster
                            

&lt;p&gt;This is a nice gain, and makes the devel­op­ment expe­ri­ence much more enjoyable!&lt;/p&gt;

&lt;p&gt;Hap­py sail­ing! &lt;strong&gt;≈ 🚀&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Running Node.js in Docker for local development</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Mon, 14 Sep 2020 04:00:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/running-node-js-in-docker-for-local-development-76p</link>
      <guid>https://dev.to/gaijinity/running-node-js-in-docker-for-local-development-76p</guid>
      <description>&lt;h1&gt;
  
  
  Running Node.js in Docker for local development
&lt;/h1&gt;

&lt;h3&gt;
  
  
  You don’t need to know Dock­er to ben­e­fit from run­ning local dev Node.js build­chains &amp;amp; apps inside of Dock­er con­tain­ers. You get easy onboard­ing, and less hassle.
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JgS3-4K7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/run-your-node-js-apps-buildchains-via-docker.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JgS3-4K7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/run-your-node-js-apps-buildchains-via-docker.jpg" alt="Run your node js apps buildchains via docker"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Devops folks who use &lt;a href="https://www.docker.com/"&gt;Dock­er&lt;/a&gt; often have no desire to use JavaScript, and JavaScript devel­op­ers often have no desire to do devops.&lt;/p&gt;

&lt;p&gt;How­ev­er, &lt;a href="https://nodejs.org/en/"&gt;Node.js&lt;/a&gt; + &lt;a href="https://www.docker.com/"&gt;Dock­er&lt;/a&gt; real­ly is a match made in heaven.&lt;/p&gt;


                                Hear me out.
                            

&lt;p&gt;You don’t have to learn Dock­er in depth to reap the ben­e­fits from using it.&lt;/p&gt;

&lt;p&gt;Whether you’re just using Node.js as a way to run a build­chain to gen­er­ate fron­tend assets that uses &lt;a href="https://gruntjs.com/"&gt;Grunt&lt;/a&gt; / &lt;a href="https://gulpjs.com/"&gt;Gulp&lt;/a&gt; / &lt;a href="https://laravel.com/docs/7.x/mix"&gt;Mix&lt;/a&gt; / &lt;a href="https://webpack.js.org/"&gt;web­pack&lt;/a&gt; / &lt;a href="https://docs.npmjs.com/misc/scripts"&gt;NPM scripts&lt;/a&gt;, or you’re devel­op­ing full blown Node.js apps, you can ben­e­fit from run­ning Node.js in Docker.&lt;/p&gt;

&lt;p&gt;In this arti­cle, we’ll show you how you can uti­lize Dock­er to run your Node.js build­chains &amp;amp; apps &amp;amp; in local dev with­out need­ing to know a whole lot about how Dock­er works.&lt;/p&gt;


                                Unless you install every &lt;span&gt;NPM&lt;/span&gt; pack­age you use glob­al­ly, you already under­stand the need for containerization
                            

&lt;p&gt;We’ll be run­ning Node.js on-demand in Dock­er con­tain­ers that run in local dev only when you’re or build­ing assets with your build­chain or devel­op­ing your application.&lt;/p&gt;

&lt;p&gt;All you’ll need to have &lt;a href="https://docs.docker.com/get-docker/"&gt;installed is Dock­er&lt;/a&gt; itself.&lt;/p&gt;

&lt;p&gt;If you’re the TL;DR type, you can check out the exam­ple project we used &lt;a href="https://github.com/nystudio107/eleventy-base-blog/tree/feature/docker"&gt;eleven­ty-blog-base feature/​docker branch&lt;/a&gt;, and look at the &lt;a href="https://github.com/nystudio107/eleventy-base-blog/compare/master..feature/docker"&gt;master..feature/docker diff&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why in the world would I use Docker?
&lt;/h2&gt;

&lt;p&gt;I think &lt;a href="https://twitter.com/adamwathan/status/1128329090828832769"&gt;this tweet from Adam Wathan&lt;/a&gt; is a per­fect exam­ple of why you would want to use Docker:&lt;/p&gt;


                                &lt;span&gt;“&lt;/span&gt;Upgrad­ed Yarn, which broke &lt;span&gt;PHP&lt;/span&gt;, which needs Python to rein­stall, which needs a new ver­sion of Xcode, which needs the lat­est ver­sion of Mojave, which means I need a beer and it’s only noon.” —Adam Wathan
                            

&lt;p&gt;Adam’s cer­tain­ly not alone, this type of ​“depen­den­cy hell” is some­thing that most devel­op­ers have descend­ed down into at some point or another.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7hpHIRWV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x854_crop_center-center_100_line/dependency-hell.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7hpHIRWV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x854_crop_center-center_100_line/dependency-hell.png" alt="Dependency hell"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And hav­ing one glob­al install for your entire devel­op­ment envi­ron­ment only gets worse from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updat­ing a depen­den­cy like the Node.js ver­sion for one app may break oth­er apps&lt;/li&gt;
&lt;li&gt;You end up using the old­est pos­si­ble ver­sion of every­thing to keep the tee­ter­ing devel­op­ment envi­ron­ment running&lt;/li&gt;
&lt;li&gt;Try­ing new tech­nolo­gies is cost­ly, because your whole devel­op­ment envi­ron­ment is at risk&lt;/li&gt;
&lt;li&gt;Updat­ing oper­at­ing sys­tem ver­sions often means putting aside a day (or more) to rebuild your devel­op­ment environment&lt;/li&gt;
&lt;li&gt;Get­ting a new com­put­er sim­i­lar­ly means putting aside a day (or more) to rebuild your devel­op­ment environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of hav­ing one mono­lith­ic local devel­op­ment envi­ron­ment, using Dock­er adds a lay­er of con­tainer­iza­tion that gives each app you are work­ing on exact­ly what it needs to run.&lt;/p&gt;


                                Your com­put­er isn’t dis­pos­able, but Dock­er con­tain­ers are
                            

&lt;p&gt;Is it quick­er to just start installing stuff via &lt;a href="https://brew.sh/"&gt;Home­brew&lt;/a&gt; on your com­put­er? Sure.&lt;/p&gt;

&lt;p&gt;But peo­ple often con­fuse get­ting start­ed quick­ly with speed. What mat­ters more is the speed (and san­i­ty) with which you finish.&lt;/p&gt;

&lt;p&gt;So let’s give Dock­er a whirl.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dock­er set­up overview
&lt;/h2&gt;

&lt;p&gt;We’re not going to teach you the ins and outs of Dock­er here; if you want that, check out the &lt;a href="https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin"&gt;An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;I also high­ly rec­om­mend the &lt;a href="https://www.udemy.com/course/docker-mastery/"&gt;Dock­er Mas­tery&lt;/a&gt; course (if it’s not on sale now, don’t wor­ry, it will be at some point).&lt;/p&gt;

&lt;p&gt;Instead, we’re just going to put Dock­er to work for us. Here’s an overview of how this is going to work:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qqSQh3Ks--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x267_crop_center-center_100_line/makefile-docker-container.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qqSQh3Ks--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x267_crop_center-center_100_line/makefile-docker-container.png" alt="Makefile docker container"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re using &lt;a href="https://en.wikipedia.org/wiki/Make_(software)"&gt;make&lt;/a&gt; with a &lt;a href="https://opensource.com/article/18/8/what-how-makefile"&gt;Make­file&lt;/a&gt; to pro­vide a nice easy way to type our ter­mi­nal com­mands (yes, Vir­ginia, depen­den­cy man­ag­ing build sys­tems have been around since 1976).&lt;/p&gt;

&lt;p&gt;Then we’re also using a &lt;a href="https://docs.docker.com/engine/reference/builder/"&gt;Dock­er­file&lt;/a&gt; that con­tains the infor­ma­tion need­ed to build &amp;amp; run our Dock­er container.&lt;/p&gt;

&lt;p&gt;We then lever­age &lt;a href="https://docs.npmjs.com/misc/scripts"&gt;NPM scripts&lt;/a&gt; in the scripts sec­tion of our package.json to run our build­chain / application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wShHS4VN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x269_crop_center-center_100_line/docker-package-scripts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wShHS4VN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x269_crop_center-center_100_line/docker-package-scripts.png" alt="Docker package scripts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So we’ll type some­thing like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
make npm build

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



&lt;p&gt;And it will spin up our Node.js Dock­er con­tain­er, and run the build script that’s in the scripts sec­tion of our package.json.&lt;/p&gt;

&lt;p&gt;Since we can put what­ev­er we want in the scripts sec­tion of our package.json, we can run what­ev­er we want.&lt;/p&gt;


                                It may seem com­pli­cat­ed, but it’s actu­al­ly rel­a­tive­ly sim­ple how it all works
                            

&lt;p&gt;So let’s have a look at how this all works in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dock­er set­up detail
&lt;/h2&gt;

&lt;p&gt;So as to have a real-world exam­ple, what we’re going to do is cre­ate a Dock­er con­tain­er that builds a web­site using the pop­u­lar &lt;a href="https://www.11ty.dev/"&gt;11ty&lt;/a&gt; sta­t­ic site generator.&lt;/p&gt;


                                Keep in mind that this is just an exam­ple, we could be con­tainer­iz­ing any Node.js build­chain or app
                            

&lt;p&gt;So what we’ll do is make a clone of the &lt;a href="https://github.com/11ty/eleventy-base-blog"&gt;eleven­ty-base-blog&lt;/a&gt; repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
git clone https://github.com/11ty/eleventy-base-blog

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



&lt;p&gt;Then we’ll make just one change to the package.json that comes from the repos­i­to­ry, adding an install npm script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
  "name": "eleventy-base-blog",
  "version": "5.0.2",
  "description": "A starter repository for a blog web site using the Eleventy static site generator.",
  "scripts": {
    "install": "npm install",
    "build": "eleventy",
    "watch": "eleventy --watch",
    "serve": "eleventy --serve",
    "start": "eleventy --serve",
    "debug": "DEBUG=* eleventy"
  },

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



&lt;p&gt;&lt;strong&gt;MAKE­FILE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next we’ll cre­ate a Makefile in the project direc­to­ry that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
TAG?=12-alpine

docker:
    docker build \
        . \
        -t nystudio107/node:${TAG} \
        --build-arg TAG=${TAG} \
        --no-cache
npm:
    docker container run \
        --name 11ty \
        --rm \
        -t \
        -p 8080:8080 \
        -p 3001:3001 \
        -v `pwd`:/app \
        nystudio107/node:${TAG} \
        $(filter-out $@,$(MAKECMDGOALS))
%:
    @:
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line

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



&lt;p&gt;The way make works is that if you type make, it looks for a Makefile in the cur­rent direc­to­ry for the recipe to make. In our case, we’re just using it as a con­ve­nient way to cre­ate alias­es that are local to a spe­cif­ic project.&lt;/p&gt;

&lt;p&gt;So we can use make as a short­cut to run much more com­pli­cat­ed com­mands that aren’t fun to type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
make docker — this will build our Node.js Dock­er image for us. You need to build a Dock­er image from a Dock­er­file before you can run it as a container&lt;/li&gt;
&lt;li&gt;
make npm xxx — once built, this will run our Dock­er con­tain­er, and exe­cute the NPM script named xxx as list­ed in the package.json. For instance, make npm build will run the build script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The TAG?=12-alpine line pro­vides a default &lt;a href="https://hub.docker.com/_/node"&gt;Node.js tag&lt;/a&gt; to use when build­ing the image, with the num­ber part of it being the Node.js ver­sion (“alpine” is just a very slimmed down Lin­ux distro).&lt;/p&gt;

&lt;p&gt;If we want­ed, say, Node.js 14, we could just change that to be TAG?=14-alpine and do a make docker or we could pass it in via the com­mand line for a quick tem­po­rary change: make docker TAG=14.alpine&lt;/p&gt;


                                It’s just that easy to switch the Node.js version
                            

&lt;p&gt;While it’s not impor­tant that you learn the syn­tax of make, let’s have a look at the two com­mands we have in our Makefile.&lt;/p&gt;

&lt;p&gt;The &amp;lt;/kbd&amp;gt; you see in the Makefile is just a way to allow you to con­tin­ue a shell com­mand on the next line, for read­abil­i­ty reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
docker: # the com­mand alias, so we run it via make docker

&lt;ul&gt;
&lt;li&gt;docker build &amp;lt;/kbd&amp;gt; # &lt;a href="https://docs.docker.com/engine/reference/commandline/build/"&gt;Build a Dock­er&lt;/a&gt; con­tain­er from a Dockerfile&lt;/li&gt;
&lt;li&gt;. &amp;lt;/kbd&amp;gt; # …in the cur­rent directory&lt;/li&gt;
&lt;li&gt;-t nystudio107/node:${TAG} &amp;lt;/kbd&amp;gt; # tag the image with nystudio107/node:12-alpine (or what­ev­er ${TAG} is)&lt;/li&gt;
&lt;li&gt;--build-arg TAG=${TAG} &amp;lt;/kbd&amp;gt; # pass in our ${TAG} vari­able as an argu­ment to the Dockerfile&lt;/li&gt;
&lt;li&gt;
--no-cache # Do not use cache when build­ing the image&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
npm: # the com­mand alias, so we run it via make npm xxx, where xxx is the npm script to run

&lt;ul&gt;
&lt;li&gt;docker container run &amp;lt;/kbd&amp;gt; # &lt;a href="https://docs.docker.com/engine/reference/commandline/container_run/"&gt;Run a Dock­er&lt;/a&gt; con­tain­er from an image&lt;/li&gt;
&lt;li&gt;--name 11ty &amp;lt;/kbd&amp;gt; # name the con­tain­er instance ​“11ty”&lt;/li&gt;
&lt;li&gt;--rm &amp;lt;/kbd&amp;gt; # remove the con­tain­er when it exits&lt;/li&gt;
&lt;li&gt;-t &amp;lt;/kbd&amp;gt; # pro­vide a ter­mi­nal, so we can have pret­ty col­ored text&lt;/li&gt;
&lt;li&gt;-p 8080:8080 &amp;lt;/kbd&amp;gt; # map port 8080 from inside of the con­tain­er to port 8080 to serve our hot reloaded files from &lt;a href="http://localhost:8080"&gt;http://localhost:8080&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;-p 3001:3001 &amp;lt;/kbd&amp;gt; # map port 3001 from inside of the con­tain­er to port 3001 to serve the &lt;a href="https://www.browsersync.io/"&gt;Browser­Sync UI&lt;/a&gt; from &lt;a href="http://localhost:3001"&gt;http://localhost:3001&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;-v &lt;code&gt;pwd&lt;/code&gt;:/app &amp;lt;/kbd&amp;gt; # mount a vol­ume from the cur­rent work­ing direc­to­ry to /app inside of the Dock­er container&lt;/li&gt;
&lt;li&gt;nystudio107/node:${TAG} &amp;lt;/kbd&amp;gt; # use the Dock­er image tagged with nystudio107/node:12-alpine (or what­ev­er ${TAG} is)&lt;/li&gt;
&lt;li&gt;
$(filter-out $@,$(MAKECMDGOALS)) # a fan­cy way to &lt;a href="https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line"&gt;pass any addi­tion­al argu­ments&lt;/a&gt; from the com­mand line down to Docker&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;We do the port map­ping to allow &lt;a href="https://www.11ty.dev/docs/usage/#re-run-eleventy-when-you-save"&gt;11ty’s hot reload­ing&lt;/a&gt; to work dur­ing development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOCK­ER­FILE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now we’ll cre­ate a Dock­er­file in the project root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
ARG TAG=12-alpine
FROM node:$TAG

WORKDIR /app

CMD ["build"]

ENTRYPOINT ["npm", "run"]

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



&lt;p&gt;Our Dockerfile is pret­ty small, but let’s break down what it’s doing:&lt;/p&gt;

&lt;p&gt;ARG TAG=12-alpine — Set the build argu­ment TAG to default to 12-alpine. If a --build-arg is pro­vid­ed, it’ll over­ride this so you can spec­i­fy oth­er Node.js version&lt;/p&gt;

&lt;p&gt;FROM node:$TAG — Des­ig­nate which base image our con­tain­er will be built from &lt;/p&gt;

&lt;p&gt;WORKDIR /app — Set the direc­to­ry where the com­mands in the Dock­er­file are run to /app&lt;/p&gt;

&lt;p&gt;CMD ["build"] — Set the default com­mand to build&lt;/p&gt;

&lt;p&gt;ENTRYPOINT ["npm", "run"] — When the con­tain­er is spun up, it’ll exe­cute npm run xxx where xxx is an argu­ment passed in via the com­mand line, or it’ll fall back on the default build command&lt;/p&gt;

&lt;h2&gt;
  
  
  Tak­ing Dock­er for a spin
&lt;/h2&gt;

&lt;p&gt;So let’s take Dock­er for a spin on this project. First we’ll make sure we’re in the project root direc­to­ry, and build our Dock­er con­tain­er with make docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
❯ make docker
docker build \
        . \
        -t nystudio107/node:12-alpine \
        --build-arg TAG=12-alpine \
        --no-cache
Sending build context to Docker daemon 438.8kB
Step 1/5 : ARG TAG=12-alpine
Step 2/5 : FROM node:$TAG
 ---&amp;gt; 18f4bc975732
Step 3/5 : WORKDIR /app
 ---&amp;gt; Running in 6f5191fe0128
Removing intermediate container 6f5191fe0128
 ---&amp;gt; 29e9346463f9
Step 4/5 : CMD ["build"]
 ---&amp;gt; Running in 38fb3db1e3a3
Removing intermediate container 38fb3db1e3a3
 ---&amp;gt; 22806cd1f11e
Step 5/5 : ENTRYPOINT ["npm", "run"]
 ---&amp;gt; Running in cea25ee21477
Removing intermediate container cea25ee21477
 ---&amp;gt; 29758f87c56c
Successfully built 29758f87c56c
Successfully tagged nystudio107/node:12-alpine

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



&lt;p&gt;Next let’s exe­cute the install script we added to our package.json via make npm install. This runs an npm install, which we only need to do once to get our node_module depen­den­cies installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
❯ make npm install
docker container run \
        --name 11ty \
        --rm \
        -t \
        -p 8080:8080 \
        -p 3001:3001 \
        -v `pwd`:/app \
        nystudio107/node:12-alpine \
        install

&amp;gt; eleventy-base-blog@5.0.2 install /app
&amp;gt; npm install

npm WARN deprecated core-js@2.6.11: core-js@&amp;lt;3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.

&amp;gt; core-js@2.6.11 postinstall /app/node_modules/core-js
&amp;gt; node -e "try{require('./postinstall')}catch(e){}"

&amp;gt; ejs@2.7.4 postinstall /app/node_modules/ejs
&amp;gt; node ./postinstall.js

Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)

npm WARN lifecycle eleventy-base-blog@5.0.2~install: cannot run in wd eleventy-base-blog@5.0.2 npm install (wd=/app)
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

added 437 packages from 397 contributors and audited 439 packages in 30.004s

15 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

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



&lt;p&gt;Final­ly, let’s fire up a hot reload­ing devel­op­ment serv­er, and build our site via make npm serve. This is the only step you’ll nor­mal­ly need to do in order to work on your site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
❯ make npm serve
docker container run \
        --name 11ty \
        --rm \
        -t \
        -p 8080:8080 \
        -p 3001:3001 \
        -v `pwd`:/app \
        nystudio107/node:12-alpine \
        serve

&amp;gt; eleventy-base-blog@5.0.2 serve /app
&amp;gt; eleventy --serve

Writing _site/feed/feed.xml from ./feed/feed.njk.
Writing _site/sitemap.xml from ./sitemap.xml.njk.
Writing _site/feed/.htaccess from ./feed/htaccess.njk.
Writing _site/feed/feed.json from ./feed/json.njk.
Writing _site/posts/fourthpost/index.html from ./posts/fourthpost.md.
Writing _site/posts/thirdpost/index.html from ./posts/thirdpost.md.
Writing _site/posts/firstpost/index.html from ./posts/firstpost.md.
Writing _site/404.html from ./404.md.
Writing _site/posts/index.html from ./archive.njk.
Writing _site/posts/secondpost/index.html from ./posts/secondpost.md.
Writing _site/page-list/index.html from ./page-list.njk.
Writing _site/tags/second-tag/index.html from ./tags.njk.
Writing _site/index.html from ./index.njk.
Writing _site/tags/index.html from ./tags-list.njk.
Writing _site/about/index.html from ./about/index.md.
Writing _site/tags/another-tag/index.html from ./tags.njk.
Writing _site/tags/number-2/index.html from ./tags.njk.
Copied 3 files / Wrote 17 files in 0.74 seconds (43.5ms each, v0.11.0)
Watching…
[Browsersync] Access URLs:
 -----------------------------------
       Local: http://localhost:8080
    External: http://172.17.0.2:8080
 -----------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 -----------------------------------
[Browsersync] Serving files from: _site

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



&lt;p&gt;We can just point our web brows­er at &lt;a href="http://localhost:8080"&gt;http://localhost:8080&lt;/a&gt; and we’ll see our web­site up and running:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JRyZSOzf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1032_crop_center-center_100_line/hot-reloaded-eleventy-blog-base.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JRyZSOzf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1032_crop_center-center_100_line/hot-reloaded-eleventy-blog-base.png" alt="Hot reloaded eleventy blog base"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we make any changes, they’ll auto­mat­i­cal­ly be hot reloaded in the brows­er, so away we go!&lt;/p&gt;


                                &lt;span&gt;“&lt;/span&gt;Yeah, so what?” you say?
                            

&lt;p&gt;Real­ize that with the Makefile and Dockerfile in place, we can hand our project off to some­one else and onboard­ing becomes bliss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We won’t need to care what ver­sion of Node.js they have installed&lt;/li&gt;
&lt;li&gt;They don’t even have to have Node.js installed at all, in fact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Addi­tion­al­ly, we can come back to the project at any time and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project is guar­an­teed to work, since the devops need­ed to run it is ​“shrink wrapped” around it&lt;/li&gt;
&lt;li&gt;We can eas­i­ly switch Node.js ver­sions with­out affect­ing any­thing else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more &lt;a href="https://github.com/nvm-sh/nvm"&gt;nvm&lt;/a&gt;. No more &lt;a href="https://www.npmjs.com/package/n"&gt;n&lt;/a&gt;. No more has­sles &lt;a href="https://dev.to/bnevilleoneill/switching-between-node-versions-during-development-3c2l"&gt;switch­ing Node.js ver­sions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Con­tainer­iza­tion as a way forward
&lt;/h2&gt;

&lt;p&gt;Next time you have the oppor­tu­ni­ty to start fresh with a new com­put­er or a new oper­at­ing sys­tem, con­sid­er tak­ing it.&lt;/p&gt;

&lt;p&gt;Don’t install Homebrew.&lt;/p&gt;

&lt;p&gt;Don’t install Node.js.&lt;/p&gt;

&lt;p&gt;Don’t install dozens of packages.&lt;/p&gt;

&lt;p&gt;Instead, take the con­tainer­iza­tion chal­lenge and &lt;em&gt;just&lt;/em&gt; install Dock­er, and run every­thing you need from containers&lt;/p&gt;

&lt;p&gt;I think you may be pleas­ant­ly sur­prised at how it’ll make your life easier.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
      <category>docker</category>
    </item>
    <item>
      <title>Atomic Deployments Without Tears</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Tue, 30 Jun 2020 04:00:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/atomic-deployments-without-tears-3j7o</link>
      <guid>https://dev.to/gaijinity/atomic-deployments-without-tears-3j7o</guid>
      <description>&lt;h1&gt;
  
  
  Atomic Deployments Without Tears
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Learn how to use atom­ic deploy­ments to auto­mat­i­cal­ly, deter­min­is­ti­cal­ly, and safe­ly deploy changes to your web­site using Con­tin­u­ous Inte­gra­tion (CI) tools
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z7ZjZTch--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/executing-atomic-deployments.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z7ZjZTch--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/executing-atomic-deployments.jpg" alt="Executing atomic deployments"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have devel­oped a web­site, you then have to face the chal­lenge of deploy­ing that web­site to a live pro­duc­tion envi­ron­ment where the world can see it.&lt;/p&gt;

&lt;p&gt;Back in the mean old days, this meant fir­ing up an FTP client to upload the web­site to a remote server.&lt;/p&gt;


                                This type of &lt;span&gt;&lt;/span&gt;​&lt;span&gt;“&lt;/span&gt;cow­boy” deploy­ment is not the best choice
                            

&lt;p&gt;The rea­son doing it this way isn’t so great is that it’s a man­u­al, error-prone process. Many ser­vices in the form of Con­tin­u­ous Inte­gra­tion tools have sprung up to make the process much eas­i­er for you, and impor­tant­ly, automated.&lt;/p&gt;


                                Let com­put­ers do the bor­ing, rep­e­ti­tious work that they are good at
                            

&lt;p&gt;This arti­cle will show you how you can lever­age the CI tool &lt;a href="http://buddy.works"&gt;buddy.works&lt;/a&gt; to atom­i­cal­ly deploy your &lt;a href="https://craftcms.com/"&gt;Craft CMS&lt;/a&gt; web­sites like a pro.&lt;/p&gt;

&lt;p&gt;How­ev­er, the con­cepts pre­sent­ed here are uni­ver­sal, so if you’re using some oth­er CI tool or CMS/​platform, that’s total­ly fine. Read on!&lt;/p&gt;

&lt;h2&gt;
  
  
  Anato­my of a web project
&lt;/h2&gt;

&lt;p&gt;Let’s have a look at what a typ­i­cal project set­up might look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JCX0o-YB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x824_crop_center-center_100_line/web-project-setup-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JCX0o-YB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x824_crop_center-center_100_line/web-project-setup-flow.png" alt="Web project setup flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We work on the project in our local devel­op­ment envi­ron­ment, whether indi­vid­u­al­ly or with a team of oth­er devel­op­ers. We push our code changes up to a git repos­i­to­ry in the cloud.&lt;/p&gt;


                                Local devel­op­ment is &lt;span&gt;&lt;/span&gt;​&lt;span&gt;“&lt;/span&gt;where the mag­ic happens”
                            

&lt;p&gt;The git repos­i­to­ry is where all source code is kept, and allows us to work with mul­ti­ple peo­ple or mul­ti­ple revi­sions with­out fear. This git repo can be host­ed via &lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt;, &lt;a href="https://gitlab.com/"&gt;Git­Lab&lt;/a&gt;, or any num­ber of oth­er places.&lt;/p&gt;

&lt;p&gt;We may also be using cloud file stor­age such as &lt;a href="https://aws.amazon.com/s3/"&gt;Ama­zon S3&lt;/a&gt; as a place to store the client-uploaded con­tent, as described in the &lt;a href="https://dev.to/gaijinity/setting-up-aws-s3-buckets-cloudfront-cdn-for-your-assets-4h24"&gt;Set­ting Up AWS S3 Buck­ets + Cloud­Front CDN for your Assets&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;A gen­er­al work­flow for code is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push code changes from local devel­op­ment up to your git repo&lt;/li&gt;
&lt;li&gt;Pull code changes down from your git repo to your live pro­duc­tion or stag­ing servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re work­ing on a team or in mul­ti­ple envi­ron­ments, you may also be pulling code down to your local devel­op­ment envi­ron­ment from your git repo as well, to stay in sync with changes oth­er peo­ple have made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-Atom­ic Deploy­ment Flow
&lt;/h2&gt;

&lt;p&gt;But how do you pull code changes down to your live pro­duc­tion or stag­ing servers?&lt;/p&gt;

&lt;p&gt;Deploy­ment is get­ting your code from your local devel­op­ment envi­ron­ment to your live pro­duc­tion server. &lt;/p&gt;

&lt;p&gt;A sim­ple method (dubbed the &lt;a href="https://devmode.fm/episodes/amezmo-cloud-hosting-and-deployment-for-php#255"&gt;#YOLO method&lt;/a&gt; by Matthew Stein) could be to trig­ger a shell script when we push to the master branch of our pro­jec­t’s git repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cd /home/forge/devmode.fm
git pull origin master
cd /home/forge/devmode.fm/cms
composer install --no-interaction --prefer-dist --optimize-autoloader
echo "" | sudo -S service php7.1-fpm reload

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



&lt;p&gt;In my case, this is how I was pre­vi­ous­ly doing deploy­ments for the &lt;a href="https://devmode.fm/"&gt;dev​Mode​.fm&lt;/a&gt; web­site: it’s just a shell script that’s exe­cut­ed when a &lt;a href="https://en.wikipedia.org/wiki/Webhook"&gt;web­hook&lt;/a&gt; that is trig­gered when we push to the master branch of our git repo.&lt;/p&gt;

&lt;p&gt;Line by line, here’s what this shell script does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change direc­to­ries to the root direc­to­ry of our project&lt;/li&gt;
&lt;li&gt;Pull down the lat­est changes from the master branch of the pro­jec­t’s git repo&lt;/li&gt;
&lt;li&gt;Change direc­to­ries to the root of the Craft CMS project&lt;/li&gt;
&lt;li&gt;Run composer install to install the lat­est com­pos­er depen­den­cies spec­i­fied in the composer.lock file&lt;/li&gt;
&lt;li&gt;Restart &lt;a href="https://www.php.net/manual/en/install.fpm.php"&gt;php-fpm&lt;/a&gt; to clear our &lt;a href="https://www.sitepoint.com/understanding-opcache/"&gt;opcache&lt;/a&gt;

                            What could pos­si­bly go wrong?
                        
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a hob­by project site, this is total­ly fine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Jz0Tp4OO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x662_crop_center-center_100_line/non-atomic-deploy-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Jz0Tp4OO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x662_crop_center-center_100_line/non-atomic-deploy-flow.png" alt="Non atomic deploy flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But there are down­sides to doing it this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The deploy­ment is done in mul­ti­ple steps&lt;/li&gt;
&lt;li&gt;The work hap­pens on the pro­duc­tion serv­er, which is also serv­ing fron­tend requests&lt;/li&gt;
&lt;li&gt;The entire git repo is deployed to the serv­er, when only part of it is actu­al­ly need­ed on the pro­duc­tion server&lt;/li&gt;
&lt;li&gt;If there’s a prob­lem with the deploy, the site could be left broken&lt;/li&gt;
&lt;li&gt;Any web­site CSS/​JavaScript assets need to built in local devel­op­ment, and checked into the git repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might notice that there are a num­ber of steps list­ed, and some of the steps such as git pull origin master and composer install can be quite lengthy processes.&lt;/p&gt;

&lt;p&gt;And we’re doing them &lt;em&gt;in situ&lt;/em&gt;, so if some­one vis­its the web­site when we’re in the mid­dle of pulling down our code, or &lt;a href="https://getcomposer.org/"&gt;Com­pos­er&lt;/a&gt; is in the mid­dle of installing PHP pack­ages… that per­son may see errors on the frontend.&lt;/p&gt;

&lt;p&gt;The fact that there are mul­ti­ple, lengthy steps in this process makes it a &lt;em&gt;non-atom­ic deploy­ment&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atom­ic Deploy­ment Flow
&lt;/h2&gt;

&lt;p&gt;So while we have an auto­mat­ed deploy­ment method, it’s a bit frag­ile in that there’s a peri­od of time dur­ing which peo­ple vis­it­ing our web­site may see it bro­ken. To solve this, let’s intro­duce how an atom­ic deploy­ment would work.&lt;/p&gt;

&lt;p&gt;An atom­ic deploy­ment is just a fan­cy nomen­cla­ture for a deploy­ment that hap­pens in such a way that the switch to the new ver­sion of the site as a sin­gle — or atom­ic — step.&lt;/p&gt;

&lt;p&gt;This allows for zero down­time, and no weird­ness in par­tial­ly deployed sites.&lt;/p&gt;


                                An atom­ic deploy­ment is a magi­cian’s fin­ger-snap and &lt;span&gt;&lt;/span&gt;​&lt;span&gt;“&lt;/span&gt;tada”!
                            

&lt;p&gt;We’re going to set up our atom­ic deploy­ments using &lt;a href="https://buddy.works"&gt;buddy.works&lt;/a&gt;, which is a tool that I’ve cho­sen because it is easy to use, but also very powerful.&lt;/p&gt;

&lt;p&gt;There’s a &lt;a href="https://buddy.works/pricing"&gt;free tier&lt;/a&gt; that you can use for up to 5 project while you’re test­ing it out, you can give it a whirl, or you can use some oth­er deploy­ment tool like &lt;a href="https://envoyer.io/"&gt;Envoy­er&lt;/a&gt; (and there are many oth­ers). The prin­ci­ple is the same.&lt;/p&gt;

&lt;p&gt;Here’s what an atom­ic deploy­ment set­up might look like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LfU_oVho--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x662_crop_center-center_100_line/atomic-deploy-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LfU_oVho--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x662_crop_center-center_100_line/atomic-deploy-flow.png" alt="Atomic deploy flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that we’re still doing the same work as in our non-atom­ic deploy­ment, but we’re chang­ing &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; that work is done.&lt;/p&gt;

&lt;p&gt;This nice­ly solves all of the down­sides we not­ed in our non-atom­ic deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The switchover to the new­ly deployed web­site code hap­pens in a sin­gle atom­ic step&lt;/li&gt;
&lt;li&gt;No work is done on the live pro­duc­tion serv­er oth­er than deploy­ing the files&lt;/li&gt;
&lt;li&gt;Only the parts of the project need­ed to serve the web­site are deployed&lt;/li&gt;
&lt;li&gt;If there’s a prob­lem with the build, it nev­er reach­es the server&lt;/li&gt;
&lt;li&gt;Any web­site CSS/​JavaScript assets are built ​“in the cloud”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this is all won­der­ful, but how does it work? Con­tin­ue on, dear reader!&lt;/p&gt;

&lt;h2&gt;
  
  
  Atom­ic Deploy­ments Under the Hood
&lt;/h2&gt;

&lt;p&gt;We’ll get to the actu­al set­up in a bit, but first I think it’s instruc­tive to see how it actu­al­ly works under the hood.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bpNtySw8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/introducting-atomic-deployments.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bpNtySw8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/introducting-atomic-deployments.jpg" alt="Introducting atomic deployments"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As usu­al, we’ll be using the &lt;a href="https://devmode.fm/"&gt;dev​Mode​.fm&lt;/a&gt; web­site as our guinea pig, the source code of which is avail­able in the &lt;a href="https://github.com/nystudio107/devmode"&gt;nystudio107/​devmode&lt;/a&gt; repo.&lt;/p&gt;

&lt;p&gt;Our project root direc­to­ry looks like this on our pro­duc­tion server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
forge@nys-production ~/devmode.fm $ ls -Al
total 32
lrwxrwxrwx 1 forge forge 49 Jun 28 19:08 current -&amp;gt; releases/33a5a7f984521811c5db597c7eef1c76c00d48e2
drwxr-xr-x 7 forge forge 4096 Jun 27 01:39 deploy-cache
-rw-rw-r-- 1 forge forge 2191 Jun 22 18:14 .env
drwxrwxr-x 12 forge forge 4096 Jun 28 19:08 releases
drwxrwxr-x 5 forge forge 4096 Jun 22 18:11 storage
drwxrwxr-x 2 forge forge 4096 Jun 26 12:30 transcoder

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



&lt;p&gt;This may look a lit­tle for­eign to you, but bear with me, you’ll get it!&lt;/p&gt;

&lt;p&gt;The deploy-cache/ direc­to­ry is where files are stored as they are being uploaded to the serv­er. In our case, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
forge@nys-production ~/devmode.fm $ ls -Al deploy-cache/
total 328
-rw-r--r-- 1 forge forge 2027 Jun 26 22:46 composer.json
-rw-r--r-- 1 forge forge 287399 Jun 27 01:39 composer.lock
drwxr-xr-x 4 forge forge 4096 Jun 27 01:39 config
-rwxr-xr-x 1 forge forge 577 Jun 23 07:25 craft
-rw-r--r-- 1 forge forge 330 Jun 23 07:25 craft.bat
-rw-r--r-- 1 forge forge 1582 Jun 23 07:25 example.env
drwxr-xr-x 3 forge forge 4096 Jun 23 07:25 modules
drwxr-xr-x 11 forge forge 4096 Jun 23 07:25 templates
drwxr-xr-x 60 forge forge 4096 Jun 27 01:40 vendor
drwxr-xr-x 5 forge forge 4096 Jun 28 19:08 web

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



&lt;p&gt;This should look pret­ty famil­iar to you if you’re a Craft CMS devel­op­er, it’s the project root for the actu­al Craft CMS project. Check out the &lt;a href="https://dev.to/gaijinity/setting-up-a-new-craft-cms-3-project-bl0"&gt;Set­ting up a New Craft CMS 3 Project&lt;/a&gt; arti­cle for more infor­ma­tion on that.&lt;/p&gt;

&lt;p&gt;Since this is a cache direc­to­ry, the con­tents can be delet­ed with­out any ill effect, oth­er than our next deploy­ment will be slow­er since it’ll need to be done from scratch.&lt;/p&gt;

&lt;p&gt;Next let’s have a look at the releases/ directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
forge@nys-production ~/devmode.fm $ ls -Al releases/
total 48
drwxr-xr-x 7 forge forge 4096 Jun 27 14:17 2c8eef7c73f20df9d02f6f071656331ca9e08eb0
drwxr-xr-x 7 forge forge 4096 Jun 28 19:08 33a5a7f984521811c5db597c7eef1c76c00d48e2
drwxrwxr-x 7 forge forge 4096 Jun 26 22:48 42372b0cd7a66f98d7f4dc83d8d99c4d9a0fb1f6
drwxrwxr-x 7 forge forge 4096 Jun 27 01:43 7b3d57dfedf5bf275aeddc6d799e3264e02d2b88
drwxrwxr-x 8 forge forge 4096 Jun 26 21:21 8c2448d252651b8cb0d69a72e327dac3541c9ba9
drwxr-xr-x 7 forge forge 4096 Jun 27 14:08 9b5c8c7cf6a7111220b66d21d811f8e5a1800507
drwxrwxr-x 8 forge forge 4096 Jun 23 08:16 beaef13f5bda9d7c2bb0e88b300f68d3b663528e
drwxrwxr-x 8 forge forge 4096 Jun 26 21:26 c56c13127b4a5ff779a155a211c07f604a4dcf8b
drwxrwxr-x 7 forge forge 4096 Jun 27 14:04 ce831a76075f57ceff8822641944e255ab9bf556
drwxrwxr-x 8 forge forge 4096 Jun 23 07:57 ebba675ccd2bb372ef82795f076ffd933ea14a31

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



&lt;p&gt;Here we see 10 real­ly weird­ly named direc­to­ries. The names here don’t real­ly mat­ter (they are auto­mat­i­cal­ly gen­er­at­ed hash­es), but what does mat­ter is that each one of these direc­to­ries con­tains a full deploy­ment of your website.&lt;/p&gt;

&lt;p&gt;You can set how many of these direc­to­ries should be kept on the serv­er, in my case I have it set to 10.&lt;/p&gt;

&lt;p&gt;If you look care­ful­ly at the current symlink:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
lrwxrwxrwx 1 forge forge 49 Jun 28 19:08 current -&amp;gt; releases/33a5a7f984521811c5db597c7eef1c76c00d48e2

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



&lt;p&gt;…you’ll see that it actu­al­ly points to the &lt;em&gt;cur­rent&lt;/em&gt; deploy­ment in the releases/ direc­to­ry (notice that the hash-named direc­to­ry it points to has the lat­est mod­i­fi­ca­tion date on it, too).&lt;/p&gt;

&lt;p&gt;So when a deploy­ment happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files are synced into the deploy-caches/ direc­to­ry (we’ll get into this more later)&lt;/li&gt;
&lt;li&gt;Then those files are copied from deploy-caches/ direc­to­ry to a hash-named direc­to­ry in the releases/ directory&lt;/li&gt;
&lt;li&gt;After every­thing is done, the current sym­link is updat­ed to point to the lat­est deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it! That’s the atom­ic part: the chang­ing of the current sym­link is the sin­gle atom­ic oper­a­tion that makes that ver­sion of the web­site live.&lt;/p&gt;

&lt;p&gt;We just have to make sure that our web serv­er root path con­tains the sym­link, so we can swap out where it points to as needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    root /home/forge/devmode.fm/current/web;

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



&lt;p&gt;If you ever encounter a regres­sion, you can roll your web­site back to a pre­vi­ous revi­sion just by chang­ing the current symlink.&lt;/p&gt;

&lt;p&gt;Also note that we have storage/ and transcoder/ direc­to­ries in our project root, as well as a .env file.&lt;/p&gt;

&lt;p&gt;These are all direc­to­ries &amp;amp; files that we want to per­sist between and be shared by each atom­ic deploy­ment. Since each deploy­ment is a clean slate, we just move any­thing we need to keep per­sis­tent into the root direc­to­ry, and sym­link to them from each deployment.&lt;/p&gt;

&lt;p&gt;The .env file is some­thing you’ll have to cre­ate your­self man­u­al­ly, using the example.env as a guide.&lt;/p&gt;

&lt;p&gt;The storage/ direc­to­ry is Craft’s run­time stor­age direc­to­ry. We keep this as a per­sis­tent direc­to­ry so that log files and oth­er Craft run­time files can per­sist across atom­ic deployments.&lt;/p&gt;

&lt;p&gt;The transcoder/ direc­to­ry is used to store the transcod­ed audio for the pod­cast, as cre­at­ed by our &lt;a href="https://nystudio107.com/plugins/transcoder"&gt;Transcoder plu­g­in&lt;/a&gt;. It’s very project spe­cif­ic, so you’re unlike­ly to need it in your projects.&lt;/p&gt;

&lt;p&gt;Let’s have a look at the current deploy­ment in the releases/ directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
forge@nys-production ~/devmode.fm $ ls -Al releases/33a5a7f984521811c5db597c7eef1c76c00d48e2/
total 320
-rw-r--r-- 1 forge forge 2027 Jun 29 14:10 composer.json
-rw-r--r-- 1 forge forge 287399 Jun 29 14:10 composer.lock
drwxr-xr-x 4 forge forge 4096 Jun 29 14:10 config
-rwxr-xr-x 1 forge forge 577 Jun 29 14:10 craft
-rw-r--r-- 1 forge forge 330 Jun 29 14:10 craft.bat
lrwxrwxrwx 1 forge forge 27 Jun 29 14:10 .env -&amp;gt; /home/forge/devmode.fm/.env
-rw-r--r-- 1 forge forge 1582 Jun 29 14:10 example.env
drwxr-xr-x 3 forge forge 4096 Jun 29 14:10 modules
lrwxrwxrwx 1 forge forge 30 Jun 29 14:10 storage -&amp;gt; /home/forge/devmode.fm/storage
drwxr-xr-x 11 forge forge 4096 Jun 29 14:10 templates
drwxr-xr-x 60 forge forge 4096 Jun 29 14:10 vendor
drwxr-xr-x 6 forge forge 4096 Jun 29 14:11 web

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



&lt;p&gt;&lt;strong&gt;N.B.:&lt;/strong&gt; this is the exact same as doing ls -Al current/ since the current sym­link points to this lat­est deployment.&lt;/p&gt;

&lt;p&gt;Here we can see the cur­rent deploy­ment root, with the .env &amp;amp; storage alias­es in place, point­ing back to the per­sis­tent files/​directories in our project root.&lt;/p&gt;

&lt;p&gt;Some­thing that might not be imme­di­ate­ly appar­ent is that we’re only deploy­ing &lt;em&gt;part&lt;/em&gt; of what is in our &lt;a href="https://github.com/nystudio107/devmode"&gt;project git repo&lt;/a&gt;. The git repo root looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
❯ ls -Al
total 80
-rw-r--r-- 1 andrew staff 868 Jun 22 17:24 .gitignore
-rw-r--r-- 1 andrew staff 1828 Feb 18 10:22 CHANGELOG.md
-rw-r--r-- 1 andrew staff 1074 Feb 4 09:54 LICENSE.md
-rw-r--r-- 1 andrew staff 7461 Jun 29 09:03 README.md
-rw-r--r-- 1 andrew staff 5094 Jun 27 14:15 buddy.yml
drwxr-xr-x 16 andrew staff 512 Jun 27 14:06 cms
-rwxr-xr-x 1 andrew staff 2064 Mar 17 16:37 docker-compose.yml
drwxr-xr-x 10 andrew staff 320 Feb 17 16:58 docker-config
drwxr-xr-x 7 andrew staff 224 Mar 17 16:37 scripts
drwxr-xr-x 12 andrew staff 384 Feb 17 15:51 src
lrwxr-xr-x 1 andrew staff 47 Jun 27 14:06 tsconfig.json -&amp;gt; docker-config/webpack-dev-devmode/tsconfig.json
lrwxr-xr-x 1 andrew staff 45 Jun 27 14:06 tslint.json -&amp;gt; docker-config/webpack-dev-devmode/tslint.json

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



&lt;p&gt;So instead of deploy­ing all of the source code and build tools that aren’t need­ed to serve the web­site (they are only need­ed to &lt;em&gt;build&lt;/em&gt; it), we instead just deploy &lt;em&gt;just&lt;/em&gt; what’s in the cms/ directory.&lt;/p&gt;

&lt;p&gt;Nice.&lt;/p&gt;

&lt;p&gt;Now that we know how it works under the hood, let’s cre­ate the atom­ic deploy­ment pipeline!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Cre­at­ing a new project
&lt;/h2&gt;

&lt;p&gt;We’ll go step by step through how to build a sim­ple but effec­tive atom­ic deploy­ment with &lt;a href="https://buddy.works/"&gt;buddy.works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b8SE2elo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/atomic-deployment-new-project.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b8SE2elo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/atomic-deployment-new-project.jpg" alt="Atomic deployment new project"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The deploy­ment pipeline we’re going to set up will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto­mat­i­cal­ly deploy to our pro­duc­tion serv­er when­ev­er we push to the mas­ter branch of our git repo&lt;/li&gt;
&lt;li&gt;Uti­lize the Dock­er con­tain­ers we already use for local devel­op­ment for build­ing the web­site in the cloud, as dis­cussed in the &lt;a href="https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin"&gt;An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article&lt;/li&gt;
&lt;li&gt;Build all of our CSS &amp;amp; JavaScript assets via the web­pack set­up dis­cussed in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article&lt;/li&gt;
&lt;li&gt;Effi­cient­ly sync just the changed files down to our live pro­duc­tion server&lt;/li&gt;
&lt;li&gt;Do an atom­ic deploy­ment by swap­ping the cur­rent site&lt;/li&gt;
&lt;li&gt;Prep Craft CMS by run­ning all migra­tions, sync­ing &lt;a href="https://docs.craftcms.com/v3/project-config.html"&gt;Project Con­fig&lt;/a&gt;, and clear­ing all caches &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let’s get down to it&lt;/p&gt;

&lt;p&gt;After log­ging into buddy.works, make sure that you’ve linked buddy.works to your git repo provider (such as GitHub, Git­Lab, etc.). It needs this to allow you to choose a git repo for your atom­ic deploy­ment set­up, and also to be noti­fied when you push code to that git repo.&lt;/p&gt;

&lt;p&gt;You can con­fig­ure this and oth­er set­tings by click­ing on your user icon in the upper-right cor­ner of the screen, and choos­ing &lt;strong&gt;Man­age your project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once that’s all set, click on &lt;strong&gt;New Project&lt;/strong&gt; from your Dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UCx8r6P8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x775_crop_center-center_100_line/atomic-deployment-buddy-step-create-project.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UCx8r6P8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x775_crop_center-center_100_line/atomic-deployment-buddy-step-create-project.png" alt="Atomic deployment buddy step create project"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next click on the &lt;strong&gt;Add a new pipeline but­ton&lt;/strong&gt; to cre­ate a new deploy­ment pipeline for this project. A pipeline is just a series of instruc­tions to exe­cute in sequence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QJZnPI46--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1104_crop_center-center_100_line/atomic-deployment-buddy-step-add-pipeline.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QJZnPI46--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1104_crop_center-center_100_line/atomic-deployment-buddy-step-add-pipeline.png" alt="Atomic deployment buddy step add pipeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set the &lt;strong&gt;Name&lt;/strong&gt; to &lt;strong&gt;Build &amp;amp; Deploy to Pro­duc­tion&lt;/strong&gt; , set &lt;strong&gt;Trig­ger Mode&lt;/strong&gt; to &lt;strong&gt;On push&lt;/strong&gt; and then set the &lt;strong&gt;Trig­ger&lt;/strong&gt; to &lt;strong&gt;Sin­gle Branch&lt;/strong&gt; and &lt;strong&gt;mas­ter&lt;/strong&gt; (or what­ev­er the name of your pri­ma­ry git repo branch is).&lt;/p&gt;

&lt;p&gt;Then click on &lt;strong&gt;+ Site URL, Cur­rent­ly deployed revi­sion, Clone depth &amp;amp; Vis­i­bil­i­ty&lt;/strong&gt; to dis­play more options, and set the &lt;strong&gt;Tar­get web­site URL&lt;/strong&gt; to what­ev­er your live pro­duc­tion web­site URL is.&lt;/p&gt;

&lt;p&gt;We won’t be chang­ing any­thing else here, so click on &lt;strong&gt;Add a new pipeline&lt;/strong&gt; to cre­ate a new emp­ty pipeline (you can have as many pipelines as you like per project).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Set­ting Variables
&lt;/h2&gt;

&lt;p&gt;Before we add any actions to our pipeline, we’re going to set some envi­ron­ment vari­ables for use in the buddy.works build pipeline.&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;Edit pipeline set­tings&lt;/strong&gt; link on the right, then click on &lt;strong&gt;Vari­ables&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GftddPnj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1124_crop_center-center_100_line/atomic-deployment-buddy-step-variables.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GftddPnj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1124_crop_center-center_100_line/atomic-deployment-buddy-step-variables.png" alt="Atomic deployment buddy step variables"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re adding these vari­ables to our pipeline to make it eas­i­er to build our indi­vid­ual actions, and make our pipeline gener­ic so it can be used with any project.&lt;/p&gt;

&lt;p&gt;Add the fol­low­ing key/​value pair vari­ables by click­ing on &lt;strong&gt;Add a new vari­able&lt;/strong&gt; , chang­ing them to suit your project (by con­ven­tion, envi­ron­ment vari­ables are &lt;a href="https://dev.to/fission/screaming-snake-case-43kj"&gt;SCREAMING_SNAKE_CASE&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
PROJECT_SHORTNAME — devmode — a short name for the project with no spaces or punc­tu­a­tion; it’s used to cre­ate work­ing direc­to­ries in the buddy.works containers&lt;/li&gt;
&lt;li&gt;
PROJECT_URL — &lt;a href="https://devmode.fm"&gt;https://devmode.fm&lt;/a&gt; — a URL to your live pro­duc­tion website&lt;/li&gt;
&lt;li&gt;
REMOTE_PROJECT_ROOT — /home/forge/devmode.fm — a path to the root direc­to­ry of the project on the server&lt;/li&gt;
&lt;li&gt;
REMOTE_SSH_HOST — devmode.fm — the host name that should be used to ssh into your server&lt;/li&gt;
&lt;li&gt;
REMOTE_SSH_USER — forge — the user name that should be used to ssh into your server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;N.B.:&lt;/strong&gt; the buddy.works docs say to use the vari­ables in a ${VARIABLE_NAME} for­mat, but you can also use them just as $VARIABLE_NAME (in fact the lat­ter is how they are auto-com­plet­ed for you).&lt;/p&gt;

&lt;p&gt;These vari­ables are defined inside of the pipeline, but you can also have vari­ables that are project-wide, as well as work­space-wide in buddy.works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Exe­cute: web­pack build
&lt;/h2&gt;

&lt;p&gt;Now that our vari­ables are all set, click on &lt;strong&gt;Actions&lt;/strong&gt; and then click on the &lt;strong&gt;Add the first action&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gNEkPmut--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1015_crop_center-center_100_line/atomic-deployment-buddy-step-webpack-build.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gNEkPmut--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1015_crop_center-center_100_line/atomic-deployment-buddy-step-webpack-build.png" alt="Atomic deployment buddy step webpack build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type webpack into the search field to find the &lt;strong&gt;Web­pack&lt;/strong&gt; action, and click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--n_qdvToz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x916_crop_center-center_100_line/atomic-deployment-buddy-step-webpack-script.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--n_qdvToz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x916_crop_center-center_100_line/atomic-deployment-buddy-step-webpack-script.png" alt="Atomic deployment buddy step webpack script"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re assum­ing you are using the web­pack set­up described in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; arti­cle and the Dock­er set­up described in the &lt;a href="https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin"&gt;An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Add the fol­low­ing script under the &lt;strong&gt;Run&lt;/strong&gt; tab; it installs our &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt; pack­ages via npm ci and then exe­cutes web­pack to build our build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cd docker-config/webpack-dev-devmode
npm ci
npm run build

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



&lt;p&gt;You can change this to be what­ev­er you need to exe­cute your CSS &amp;amp; JavaScript build, if you’re using some­thing oth­er than the afore­men­tioned setups.&lt;/p&gt;

&lt;p&gt;Next click on the &lt;strong&gt;Envi­ron­ment&lt;/strong&gt; tab, and change the Image to our cus­tom webpack-dev-base that we used in the &lt;a href="https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin"&gt;An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; arti­cle, since it has every­thing we need in it for build­ing our CSS &amp;amp; JavaScript:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--l0cqWh4n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1016_crop_center-center_100_line/atomic-deployment-buddy-step-webpack-environment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l0cqWh4n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1016_crop_center-center_100_line/atomic-deployment-buddy-step-webpack-environment.png" alt="Atomic deployment buddy step webpack environment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This &lt;strong&gt;Envi­ron­ment&lt;/strong&gt; tab allows you to pick any Dock­er image you like — pub­lic or pri­vate — to use when run­ning you web­pack build in the cloud. The default is an old (but offi­cial) Node 6 image at the time of this writing.&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Action&lt;/strong&gt; tab allows you to change the name of the action; change it to: &lt;strong&gt;Exe­cute: web­pack build.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Exe­cute: com­pos­er install
&lt;/h2&gt;

&lt;p&gt;Next up we’ll cre­ate anoth­er action to our pipeline by click­ing on the &lt;strong&gt;+&lt;/strong&gt; icon below the &lt;strong&gt;Exe­cute: web­pack build&lt;/strong&gt; action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CbYxBSwN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x925_crop_center-center_100_line/atomic-deployment-buddy-step-php-script.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CbYxBSwN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x925_crop_center-center_100_line/atomic-deployment-buddy-step-php-script.png" alt="Atomic deployment buddy step php script"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type php into the search field to find the &lt;strong&gt;PHP&lt;/strong&gt; action, and click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wSNgmbYn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x815_crop_center-center_100_line/atomic-deployment-buddy-step-php-build.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wSNgmbYn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x815_crop_center-center_100_line/atomic-deployment-buddy-step-php-build.png" alt="Atomic deployment buddy step php build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re assum­ing you are using the Dock­er set­up described in the &lt;a href="https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin"&gt;An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Add the fol­low­ing script under the &lt;strong&gt;Run&lt;/strong&gt; tab; it changes direc­to­ries to the cms/ direc­to­ry, and then runs composer install with some flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cd cms
composer install --no-scripts --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs

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



&lt;p&gt;You can change this to be what­ev­er you need to exe­cute to install your Com­pos­er pack­ages, if you’re using some­thing oth­er than the afore­men­tioned setup.&lt;/p&gt;

&lt;p&gt;Next click on the &lt;strong&gt;Envi­ron­ment&lt;/strong&gt; tab, and change the Image to our cus­tom php-dev-base that we used in the &lt;a href="https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin"&gt;An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; arti­cle, since it has every­thing we need for our PHP application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LY59PDPf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/atomic-deployment-buddy-step-php-environment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LY59PDPf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/atomic-deployment-buddy-step-php-environment.png" alt="Atomic deployment buddy step php environment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This &lt;strong&gt;Envi­ron­ment&lt;/strong&gt; tab allows you to pick any Dock­er image you like — pub­lic or pri­vate — to use when run­ning your composer install in the cloud. The default is php 7.4 image at the time of this writing.&lt;/p&gt;

&lt;p&gt;Still on the &lt;strong&gt;Envi­ron­ment&lt;/strong&gt; tab, scroll down to &lt;strong&gt;CUS­TOMIZE ENVI­RON­MENT&lt;/strong&gt; and paste this in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
echo "memory_limit=-1" &amp;gt;&amp;gt; /usr/local/etc/php/conf.d/buddy.ini
apt-get update &amp;amp;&amp;amp; apt-get install -y git zip
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# php ext pdo_pgsql
docker-php-ext-install pdo_pgsql pgsql

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



&lt;p&gt;This script runs inside the Dock­er con­tain­er to cus­tomize the envi­ron­ment by set­ting PHP to have no mem­o­ry lim­it, installing &lt;a href="https://getcomposer.org/"&gt;Com­pos­er&lt;/a&gt;, and then installing some &lt;a href="https://www.postgresql.org/"&gt;Post­gres&lt;/a&gt; php exten­sions. If you’re using &lt;a href="https://www.mysql.com/"&gt;MySQL&lt;/a&gt;, you’d change it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# php ext pdo_mysql
docker-php-ext-install pdo_mysql mysql

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



&lt;p&gt;In actu­al­i­ty, it does­n’t mat­ter, because we’re not even doing any­thing with the data­base on deploy currently.&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Action&lt;/strong&gt; tab allows you to change the name of the action; change it to: &lt;strong&gt;Exe­cute: com­pos­er install.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rsync files to production
&lt;/h2&gt;

&lt;p&gt;Now that we have our updat­ed web­site code from our git repo, our built CSS &amp;amp; JavaScript assets, and all of our Com­pos­er pack­ages in the Dock­er con­tain­er in the cloud, we need to deploy them to our pro­duc­tion server.&lt;/p&gt;

&lt;p&gt;To do this, we’re going to use &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-use-rsync-to-sync-local-and-remote-directories-on-a-vps"&gt;rsync&lt;/a&gt; to &lt;em&gt;sync&lt;/em&gt; only the files that have changed to our deploy-cache/ directory.&lt;/p&gt;

&lt;p&gt;Cre­ate anoth­er action to our pipeline by click­ing on the &lt;strong&gt;+&lt;/strong&gt; icon below the &lt;strong&gt;Exe­cute: com­pos­er install&lt;/strong&gt; action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qXzrU2yj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x773_crop_center-center_100_line/atomic-deployment-buddy-step-rsync-build.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qXzrU2yj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x773_crop_center-center_100_line/atomic-deployment-buddy-step-rsync-build.png" alt="Atomic deployment buddy step rsync build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type rsync into the search field to find the &lt;strong&gt;RSync&lt;/strong&gt; action, and click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mdtn3boO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x889_crop_center-center_100_line/atomic-deployment-buddy-step-rsync-setup-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mdtn3boO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x889_crop_center-center_100_line/atomic-deployment-buddy-step-rsync-setup-1.png" alt="Atomic deployment buddy step rsync setup 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we’ve cho­sen to syn­chro­nize just the cms/ direc­to­ry of our project with the deploy-caches/ direc­to­ry on our live pro­duc­tion server.&lt;/p&gt;

&lt;p&gt;To allow buddy.works to access our live pro­duc­tion serv­er, we have to pro­vide it with how to con­nect to our serv­er. For­tu­nate­ly, we can use the envi­ron­ment vari­ables set up in step #1.&lt;/p&gt;

&lt;p&gt;So set &lt;strong&gt;Host­name &amp;amp; Port&lt;/strong&gt; to $REMOTE_SSH_HOST, &lt;strong&gt;Login&lt;/strong&gt; to $REMOTE_SSH_USER, and &lt;strong&gt;Authen­ti­ca­tion mode&lt;/strong&gt; to Buddy workspace key.&lt;/p&gt;

&lt;p&gt;We’re using &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys--2"&gt;ssh keys&lt;/a&gt; here because the pro­vi­sion­er I use, &lt;a href="https://forge.laravel.com/"&gt;Lar­avel Forge&lt;/a&gt;, dis­ables pass­word-based auth by default as a secu­ri­ty best practice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EyyJTluz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x845_crop_center-center_100_line/atomic-deployment-buddy-step-rsync-setup-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EyyJTluz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x845_crop_center-center_100_line/atomic-deployment-buddy-step-rsync-setup-2.png" alt="Atomic deployment buddy step rsync setup 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re going to use Buddy workspace key too, you’ll need to ssh into your live pro­duc­tion serv­er, and run the code snip­pet. This will add Bud­dy’s work­space key to your live pro­duc­tion server’s list of hosts that are autho­rized to con­nect to it.&lt;/p&gt;

&lt;p&gt;Then set &lt;strong&gt;Remote path&lt;/strong&gt; to $REMOTE_PROJECT_ROOT/deploy-cache. This tells the rsync action what direc­to­ry on the live pro­duc­tion serv­er should be synced with the cms/ direc­to­ry in our buddy.works Dock­er con­tain­er in the cloud.&lt;/p&gt;

&lt;p&gt;Final­ly, check the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;✓&lt;/strong&gt; Com­press file data dur­ing the transer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✓&lt;/strong&gt; Archive mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✓&lt;/strong&gt; Delete extra­ne­ous files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✓&lt;/strong&gt; Recurse into directories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Rsync for our deploy­ment allows it to be very smart about deploy­ing only files that have actu­al­ly changed, and also com­press the files before they are trans­ferred over the wire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N.B.:&lt;/strong&gt; In the &lt;strong&gt;Ignore paths&lt;/strong&gt; tab, you can add any direc­to­ries you want ignored dur­ing the sync&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Action&lt;/strong&gt; tab allows you to change the name of the action; change it to: &lt;strong&gt;Rsync files to production.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Atom­ic Deploy
&lt;/h2&gt;

&lt;p&gt;Final­ly, we’re get­ting to the actu­al atom­ic deployment!&lt;/p&gt;

&lt;p&gt;Cre­ate anoth­er action to our pipeline by click­ing on the &lt;strong&gt;+&lt;/strong&gt; icon below the &lt;strong&gt;Rsync files to pro­duc­tion&lt;/strong&gt; action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DcMAgVnX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x574_crop_center-center_100_line/atomic-deployment-buddy-step-atomic-template.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DcMAgVnX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x574_crop_center-center_100_line/atomic-deployment-buddy-step-atomic-template.png" alt="Atomic deployment buddy step atomic template"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time we’re going to click on &lt;strong&gt;Tem­plates&lt;/strong&gt; and then click on &lt;strong&gt;Atom­ic Deploy­ment&lt;/strong&gt;. You’ll see some doc­u­men­ta­tion on what the Atom­ic Deploy­ment tem­plate does; click on &lt;strong&gt;Con­fig­ure this tem­plate&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GGB_NuKg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1027_crop_center-center_100_line/atomic-deployment-buddy-step-atomic-confgure-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GGB_NuKg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1027_crop_center-center_100_line/atomic-deployment-buddy-step-atomic-confgure-1.png" alt="Atomic deployment buddy step atomic confgure 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Source&lt;/strong&gt; , click on &lt;strong&gt;Pipeline Filesys­tem&lt;/strong&gt; and leave &lt;strong&gt;Source path&lt;/strong&gt; set to /&lt;/p&gt;

&lt;p&gt;Set &lt;strong&gt;Host­name &amp;amp; Port&lt;/strong&gt; to $REMOTE_SSH_HOST, &lt;strong&gt;Login&lt;/strong&gt; to $REMOTE_SSH_USER, and &lt;strong&gt;Authen­ti­ca­tion mode&lt;/strong&gt; to Buddy workspace key just like we did in step #3.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--q7da95Rs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x991_crop_center-center_100_line/atomic-deployment-buddy-step-atomic-confgure-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--q7da95Rs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x991_crop_center-center_100_line/atomic-deployment-buddy-step-atomic-confgure-2.png" alt="Atomic deployment buddy step atomic confgure 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again we’re using the same Bud­dy work­space key we used in step #3, so we won’t need to re-add this key to our live pro­duc­tion server.&lt;/p&gt;

&lt;p&gt;Leave &lt;strong&gt;Remote path&lt;/strong&gt; set to ~/ and the dou­ble-neg­a­tive &lt;strong&gt;Don’t delete files&lt;/strong&gt; set to Off. You can also con­fig­ure how many releas­es to keep on your serv­er via &lt;strong&gt;How many old releas­es should be kept&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then click on &lt;strong&gt;Add this action&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We’re not quite done with this action though. Click on it again in the list of pipeline actions to edit it, and you’ll see some shell code the tem­plate added for us under &lt;strong&gt;RUN SSH COM­MANDS&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
if [-d "releases/$BUDDY_EXECUTION_REVISION"] &amp;amp;&amp;amp; ["$BUDDY_EXECUTION_REFRESH" = "true"];
then
 echo "Removing: releases/$BUDDY_EXECUTION_REVISION"
 rm -rf releases/$BUDDY_EXECUTION_REVISION;
fi
if [! -d "releases/$BUDDY_EXECUTION_REVISION"];
then
 echo "Creating: releases/$BUDDY_EXECUTION_REVISION"
 cp -dR deploy-cache releases/$BUDDY_EXECUTION_REVISION;
fi
echo "Linking current to revision: $BUDDY_EXECUTION_REVISION"
rm -f current
ln -s releases/$BUDDY_EXECUTION_REVISION current
echo "Removing old releases"
cd releases &amp;amp;&amp;amp; ls -t | tail -n +11 | xargs rm -rf

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



&lt;p&gt;This is the code that han­dles cre­at­ing the hash-named revi­sion direc­to­ries, copy­ing files from the deploy-cache/ direc­to­ry, updat­ing the cur­rent sym­link, and trim­ming old releases.&lt;/p&gt;

&lt;p&gt;You need­n’t grok all that it’s doing, we’re just going to make a small addi­tion to it to cre­ate and sym­link our per­sis­tent direc­to­ries &amp;amp; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
if [-d "releases/$BUDDY_EXECUTION_REVISION"] &amp;amp;&amp;amp; ["$BUDDY_EXECUTION_REFRESH" = "true"];
then
 echo "Removing: releases/$BUDDY_EXECUTION_REVISION"
 rm -rf releases/$BUDDY_EXECUTION_REVISION;
fi
if [! -d "releases/$BUDDY_EXECUTION_REVISION"];
then
 echo "Creating: releases/$BUDDY_EXECUTION_REVISION"
 cp -dR deploy-cache releases/$BUDDY_EXECUTION_REVISION;
fi
echo "Creating: persistent directories"
mkdir -p storage
mkdir -p transcoder
echo "Symlinking: persistent files &amp;amp; directories"
ln -nfs $REMOTE_PROJECT_ROOT/.env $REMOTE_PROJECT_ROOT/releases/$BUDDY_EXECUTION_REVISION
ln -nfs $REMOTE_PROJECT_ROOT/storage $REMOTE_PROJECT_ROOT/releases/$BUDDY_EXECUTION_REVISION
ln -nfs $REMOTE_PROJECT_ROOT/transcoder $REMOTE_PROJECT_ROOT/releases/$BUDDY_EXECUTION_REVISION/web
echo "Linking current to revision: $BUDDY_EXECUTION_REVISION"
rm -f current
ln -s releases/$BUDDY_EXECUTION_REVISION current
echo "Removing old releases"
cd releases &amp;amp;&amp;amp; ls -t | tail -n +11 | xargs rm -rf

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



&lt;p&gt;Here we’re ensur­ing that the storage/ and transcoder/ direc­to­ries exist, and then we’re sym­link’ing them and our .env file from their per­sis­tent loca­tion in the project root in the appro­pri­ate places in the deployed website.&lt;/p&gt;

&lt;p&gt;The transcoder/ direc­to­ry is used to store the transcod­ed audio for the pod­cast, as cre­at­ed by our &lt;a href="https://nystudio107.com/plugins/transcoder"&gt;Transcoder plu­g­in&lt;/a&gt;. It’s very project spe­cif­ic, so you’re unlike­ly to need it in your projects.&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Action&lt;/strong&gt; tab allows you to change the name of the action; change it to: &lt;strong&gt;Atom­ic deploy.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Prep Craft CMS
&lt;/h2&gt;

&lt;p&gt;Cre­ate anoth­er action to our pipeline by click­ing on the &lt;strong&gt;+&lt;/strong&gt; icon below the &lt;strong&gt;Atom­ic deploy&lt;/strong&gt; action.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tech­ni­cal­ly&lt;/em&gt; this action could be com­bined with Step #4, but log­i­cal­ly they do dif­fer­ent things, so keep­ing them sep­a­rate seems appropriate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vRrZmJ61--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x763_crop_center-center_100_line/atomic-deployment-buddy-step-prep-craft-build.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vRrZmJ61--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x763_crop_center-center_100_line/atomic-deployment-buddy-step-prep-craft-build.png" alt="Atomic deployment buddy step prep craft build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type ssh into the search field to find the &lt;strong&gt;SSH&lt;/strong&gt; action, and click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--43e62wuW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1002_crop_center-center_100_line/atomic-deployment-buddy-step-prep-craft-setup-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--43e62wuW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1002_crop_center-center_100_line/atomic-deployment-buddy-step-prep-craft-setup-1.png" alt="Atomic deployment buddy step prep craft setup 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;RUN SSH COM­MANDS&lt;/strong&gt; we have the fol­low­ing shell script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Ensure the craft script is executable
chmod a+x craft
# Run pending migrations, sync project config, and clear caches
./craft migrate/all
./craft project-config/sync
./craft clear-caches/all

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



&lt;p&gt;This ensures that all of the migra­tions are run, Project Con­fig is synced, and all caches are cleared on every deploy.&lt;/p&gt;

&lt;p&gt;Set &lt;strong&gt;Host­name &amp;amp; Port&lt;/strong&gt; to $REMOTE_SSH_HOST, &lt;strong&gt;Login&lt;/strong&gt; to $REMOTE_SSH_USER, and &lt;strong&gt;Authen­ti­ca­tion mode&lt;/strong&gt; to Buddy workspace key just like we did in steps #3 &amp;amp; #4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--652lhREy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x743_crop_center-center_100_line/atomic-deployment-buddy-step-prep-craft-setup-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--652lhREy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x743_crop_center-center_100_line/atomic-deployment-buddy-step-prep-craft-setup-2.png" alt="Atomic deployment buddy step prep craft setup 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again we’re using the same Bud­dy work­space key we used in steps #3 &amp;amp; #4, so we won’t need to re-add this key to our live pro­duc­tion server.&lt;/p&gt;

&lt;p&gt;Then set &lt;strong&gt;Work­ing direc­to­ry&lt;/strong&gt; to $REMOTE_PROJECT_ROOT/deploy-cache to tell buddy.works which direc­to­ry should be cur­rent when the script above is run.&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Action&lt;/strong&gt; tab allows you to change the name of the action; change it to: &lt;strong&gt;Prep Craft CMS.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #6: Send noti­fi­ca­tion to nystudio107 channel
&lt;/h2&gt;

&lt;p&gt;Cre­ate anoth­er action to our pipeline by click­ing on the &lt;strong&gt;+&lt;/strong&gt; icon below the &lt;strong&gt;Prep Craft CMS&lt;/strong&gt; action.&lt;/p&gt;

&lt;p&gt;This option­al action sends a noti­fi­ca­tion on deploy to the #nystudio107 chan­nel on the pri­vate nystudio107 &lt;a href="https://slack.com/"&gt;Slack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xxAYyw8g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x770_crop_center-center_100_line/atomic-deployment-buddy-step-slack.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xxAYyw8g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x770_crop_center-center_100_line/atomic-deployment-buddy-step-slack.png" alt="Atomic deployment buddy step slack"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type slack into the search field to find the &lt;strong&gt;Slack&lt;/strong&gt; action, and click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t7q3mb9N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1122_crop_center-center_100_line/atomic-deployment-buddy-step-slack-setup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t7q3mb9N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1122_crop_center-center_100_line/atomic-deployment-buddy-step-slack-setup.png" alt="Atomic deployment buddy step slack setup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll have to grant buddy.works access to your Slack by auth’ing it, then set the &lt;strong&gt;Send mes­sage&lt;/strong&gt; to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[#$BUDDY_EXECUTION_ID] $BUDDY_PIPELINE_NAME execution by $BUDDY_EXECUTION_REVISION_COMMITTER_NAME .

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



&lt;p&gt;Or cus­tomize it how­ev­er you like, and con­fig­ure the &lt;strong&gt;Inte­gra­tion&lt;/strong&gt; and &lt;strong&gt;Tar­get chan­nel&lt;/strong&gt; as appro­pri­ate for your Slack.&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Action&lt;/strong&gt; tab allows you to change the name of the action; change it to: &lt;strong&gt;Send noti­fi­ca­tion to nystudio107 channel.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gold­en Road (to unlim­it­ed deployment)
&lt;/h2&gt;

&lt;p&gt;If all of this set­up seems like a whole lot of work to you, it’s real­ly not so bad once you are famil­iar with the buddy.works GUI.&lt;/p&gt;

&lt;p&gt;How­ev­er, I also have good news for you. There’s a rea­son why we used envi­ron­ment vari­ables: buddy.works allows you to save your entire con­fig­u­ra­tion out to a buddy.yml file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4b1T9WeH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/unlimited-atomic-deployments-yaml.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4b1T9WeH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/unlimited-atomic-deployments-yaml.jpg" alt="Unlimited atomic deployments yaml"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to your project view, and click on &lt;strong&gt;YAML con­fig­u­ra­tion: OFF&lt;/strong&gt; and you’ll see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A5rilmcR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1212_crop_center-center_100_line/atomic-deployment-buddy-step-yaml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A5rilmcR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1212_crop_center-center_100_line/atomic-deployment-buddy-step-yaml.png" alt="Atomic deployment buddy step yaml"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have a buddy.yml in your project root and switch your project to YAML con­fig­u­ra­tion: ON, then you’ll get your pipelines con­fig­ured for you auto­mat­i­cal­ly by the buddy.yml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
- pipeline: "Build &amp;amp; Deploy to Production"
  trigger_mode: "ON_EVERY_PUSH"
  ref_name: "master"
  ref_type: "BRANCH"
  target_site_url: "https://devmode.fm/"
  trigger_condition: "ALWAYS"
  actions:
    - action: "Execute: webpack build"
      type: "BUILD"
      working_directory: "/buddy/$PROJECT_SHORTNAME"
      docker_image_name: "nystudio107/webpack-dev-base"
      docker_image_tag: "latest"
      execute_commands:
        - "cd docker-config/webpack-dev-devmode"
        - "npm ci"
        - "npm run build"
      volume_mappings:
        - "/:/buddy/$PROJECT_SHORTNAME"
      trigger_condition: "ALWAYS"
      shell: "BASH"
    - action: "Execute: composer install"
      type: "BUILD"
      working_directory: "/buddy/$PROJECT_SHORTNAME"
      docker_image_name: "nystudio107/php-dev-base"
      docker_image_tag: "latest"
      execute_commands:
        - "cd cms"
        - "composer install --no-scripts --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
      setup_commands:
        - "echo \"memory_limit=-1\" &amp;gt;&amp;gt; /usr/local/etc/php/conf.d/buddy.ini"
        - "apt-get update &amp;amp;&amp;amp; apt-get install -y git zip"
        - "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer"
        - "# php ext pdo_mysql"
        - "docker-php-ext-install pdo_pgsql pgsql"
      volume_mappings:
        - "/:/buddy/$PROJECT_SHORTNAME"
      trigger_condition: "ALWAYS"
      shell: "BASH"
    - action: "Rsync files to production"
      type: "RSYNC"
      local_path: "cms/"
      remote_path: "$REMOTE_PROJECT_ROOT/deploy-cache"
      login: "$REMOTE_SSH_USER"
      host: "$REMOTE_SSH_HOST"
      port: "22"
      authentication_mode: "WORKSPACE_KEY"
      archive: true
      delete_extra_files: true
      recursive: true
      compress: true
      deployment_excludes:
        - "/.git/"
      trigger_condition: "ALWAYS"
    - action: "Atomic deploy"
      type: "SSH_COMMAND"
      working_directory: "$REMOTE_PROJECT_ROOT"
      login: "$REMOTE_SSH_USER"
      host: "$REMOTE_SSH_HOST"
      port: "22"
      authentication_mode: "WORKSPACE_KEY"
      commands:
        - "if [-d \"releases/$BUDDY_EXECUTION_REVISION\"] &amp;amp;&amp;amp; [\"$BUDDY_EXECUTION_REFRESH\" = \"true\"];"
        - "then"
        - " echo \"Removing: releases/$BUDDY_EXECUTION_REVISION\""
        - " rm -rf releases/$BUDDY_EXECUTION_REVISION;"
        - "fi"
        - "if [! -d \"releases/$BUDDY_EXECUTION_REVISION\"];"
        - "then"
        - " echo \"Creating: releases/$BUDDY_EXECUTION_REVISION\""
        - " cp -dR deploy-cache releases/$BUDDY_EXECUTION_REVISION;"
        - "fi"
        - "echo \"Creating: persistent directories\""
        - "mkdir -p storage"
        - "echo \"Symlinking: persistent files &amp;amp; directories\""
        - "ln -nfs $REMOTE_PROJECT_ROOT/.env $REMOTE_PROJECT_ROOT/releases/$BUDDY_EXECUTION_REVISION"
        - "ln -nfs $REMOTE_PROJECT_ROOT/storage $REMOTE_PROJECT_ROOT/releases/$BUDDY_EXECUTION_REVISION"
        - "ln -nfs $REMOTE_PROJECT_ROOT/transcoder $REMOTE_PROJECT_ROOT/releases/$BUDDY_EXECUTION_REVISION/web"
        - "echo \"Linking current to revision: $BUDDY_EXECUTION_REVISION\""
        - "rm -f current"
        - "ln -s releases/$BUDDY_EXECUTION_REVISION current"
        - "echo \"Removing old releases\""
        - "cd releases &amp;amp;&amp;amp; ls -t | tail -n +11 | xargs rm -rf"
      trigger_condition: "ALWAYS"
      run_as_script: true
      shell: "BASH"
    - action: "Prep Craft CMS"
      type: "SSH_COMMAND"
      working_directory: "$REMOTE_PROJECT_ROOT/current"
      login: "$REMOTE_SSH_USER"
      host: "$REMOTE_SSH_HOST"
      port: "22"
      authentication_mode: "WORKSPACE_KEY"
      commands:
        - "# Ensure the craft script is executable"
        - "chmod a+x craft"
        - "# Run pending migrations, sync project config, and clear caches"
        - "./craft migrate/all"
        - "./craft project-config/sync"
        - "./craft clear-caches/all"
      trigger_condition: "ALWAYS"
      run_as_script: true
      shell: "BASH"
    - action: "Send notification to nystudio107 channel"
      type: "SLACK"
      content: "[#$BUDDY_EXECUTION_ID] $BUDDY_PIPELINE_NAME execution by $BUDDY_EXECUTION_REVISION_COMMITTER_NAME ."
      blocks: "[{\"type\":\"section\",\"fields\":[{\"type\":\"mrkdwn\",\"text\":\"*Successful execution:* &amp;lt;$BUDDY_EXECUTION_URL|Execution #$BUDDY_EXECUTION_ID $BUDDY_EXECUTION_COMMENT&amp;gt;\"},{\"type\":\"mrkdwn\",\"text\":\"*Pipeline:* &amp;lt;$BUDDY_PIPELINE_URL|$BUDDY_PIPELINE_NAME&amp;gt;\"},{\"type\":\"mrkdwn\",\"text\":\"*Branch:* $BUDDY_EXECUTION_BRANCH\"},{\"type\":\"mrkdwn\",\"text\":\"*Project:* &amp;lt;$BUDDY_PROJECT_URL|$BUDDY_PROJECT_NAME&amp;gt;\"}]}]"
      channel: "CAYN15RD0"
      channel_name: "nystudio107"
      trigger_condition: "ALWAYS"
      integration_hash: "5ef0d26820cfeb531cb10738"
  variables:
    - key: "PROJECT_SHORTNAME"
      value: "devmode"
    - key: "PROJECT_URL"
      value: "https://devmode.fm"
    - key: "REMOTE_PROJECT_ROOT"
      value: "/home/forge/devmode.fm"
    - key: "REMOTE_SSH_HOST"
      value: "devmode.fm"
    - key: "REMOTE_SSH_USER"
      value: "forge"

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



&lt;p&gt;The fact that we refac­tored things that change from project to project into envi­ron­ment vari­ables makes it super easy to re-use this con­fig on mul­ti­ple projects.&lt;/p&gt;

&lt;p&gt;And here’s what the final pipeline looks like in the GUI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jjM_u0iO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1477_crop_center-center_100_line/atomic-deployment-buddy-final-pipeline.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jjM_u0iO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1477_crop_center-center_100_line/atomic-deployment-buddy-final-pipeline.png" alt="Atomic deployment buddy final pipeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One more deploy for the road
&lt;/h2&gt;

&lt;p&gt;The advan­tages that I find with buddy.works over tools like &lt;a href="https://www.ansible.com/"&gt;Ansi­ble&lt;/a&gt; &amp;amp; &lt;a href="https://puppet.com/"&gt;Pup­pet&lt;/a&gt; or ser­vices like &lt;a href="https://deploybot.com/"&gt;Deploy­Bot&lt;/a&gt; &amp;amp; &lt;a href="https://envoyer.io/"&gt;Envoy­er&lt;/a&gt; are that it’s very easy to set up, and you can run all of your build steps in Dock­er con­tain­ers in the cloud.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mQNqRpHX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/atomic-deployment-wrapping-up.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mQNqRpHX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/atomic-deployment-wrapping-up.jpg" alt="Atomic deployment wrapping up"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because every­thing runs in Dock­er con­tain­ers in the cloud, you also do not need Com­pos­er or Node or any­thing else that’s used only to ​“build the thing” installed on your server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.gitlab.com/ee/ci/"&gt;Git­Lab CI/CD&lt;/a&gt; works sim­i­lar­ly to this, and is also a sol­id choice. But I pre­fer buddy.works being decou­pled from where the git repo is host­ed, because this flex­i­bil­i­ty can be very handy when deal­ing with var­ied client needs &amp;amp; requirements.&lt;/p&gt;

&lt;p&gt;There’s also plen­ty more that buddy.works can do that we haven’t explored here. For exam­ple, you’d typ­i­cal­ly set up anoth­er pipeline for your stag­ing serv­er, which would auto-deploy on push­es to the devel­op branch.&lt;/p&gt;

&lt;p&gt;We also could go a step fur­ther with our deploy­ments and do &lt;a href="https://octopus.com/blog/databases-with-blue-green-deployments"&gt;blue/​green data­base deploy­ments&lt;/a&gt; if the project war­rant­ed it.&lt;/p&gt;

&lt;p&gt;Auto­mat­ed accep­tance tests could be run in the buddy.works con­tain­ers, and deploy­ment would only hap­pen if they pass.&lt;/p&gt;

&lt;p&gt;Or we could run acces­si­bil­i­ty tests on deploy, and block deploy­ment if there were regres­sions there.&lt;/p&gt;

&lt;p&gt;The options are lim­it­less, and buddy.works makes it easy &lt;em&gt;for me&lt;/em&gt; to explore them.&lt;/p&gt;

&lt;p&gt;But what­ev­er deploy­ment tool you use… hap­py deploying!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>javascript</category>
      <category>php</category>
      <category>webpack</category>
    </item>
    <item>
      <title>A taste of Vue.js 3: API Changes, Async Components, and Plugins</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Fri, 29 May 2020 23:26:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/a-taste-of-vue-js-3-api-changes-async-components-and-plugins-2aoe</link>
      <guid>https://dev.to/gaijinity/a-taste-of-vue-js-3-api-changes-async-components-and-plugins-2aoe</guid>
      <description>&lt;h1&gt;
  
  
  A taste of Vue.js 3: API Changes, Async Components, and Plugins
&lt;/h1&gt;

&lt;h3&gt;
  
  
  This arti­cle takes you through changes that have to be made when mov­ing to Vue.js 3, cov­er­ing API changes, async com­po­nents, and adapt­ing exist­ing plugins
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com" rel="noopener noreferrer"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnystudio107-ems2qegf7x6qiqq.netdna-ssl.com%2Fimg%2Fblog%2F_1200x675_crop_center-center_82_line%2Fvue-js-3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnystudio107-ems2qegf7x6qiqq.netdna-ssl.com%2Fimg%2Fblog%2F_1200x675_crop_center-center_82_line%2Fvue-js-3.jpg" alt="Vue js 3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re cur­rent­ly in the plan­ning phase for a project, and are choos­ing the tech­nolo­gies that we’ll be using as the basis for it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vuejs.org/" rel="noopener noreferrer"&gt;Vue.js&lt;/a&gt; will be amongst those tech­nolo­gies, but should we go with Vue 2 or Vue 3, which is cur­rent­ly still a beta?&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            It’s at that awk­ward stage where it could go either way
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;At the time of this writ­ing, Vue.js 3 is at ver­sion 3.0.0-beta 14, and is slat­ed for release Q2 2020. For now, it can be found at the &lt;a href="https://github.com/vuejs/vue-next" rel="noopener noreferrer"&gt;vue­js/vue-next&lt;/a&gt; GitHub repo.&lt;/p&gt;

&lt;p&gt;What we decid­ed to do was attempt to con­vert over the scaf­fold­ing we use in the &lt;a href="http://github.com/nystudio107/craft" rel="noopener noreferrer"&gt;nystudio107/​craft&lt;/a&gt; repo and detailed in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;If every­thing went smooth­ly, then away we go… Vue.js 3 it is. If not, then we stick with Vue.js 2.&lt;/p&gt;

&lt;p&gt;We were pri­mar­i­ly inter­est­ed in using the new &lt;a href="https://composition-api.vuejs.org/" rel="noopener noreferrer"&gt;Com­po­si­tion API&lt;/a&gt;, bet­ter &lt;a href="https://dev.to/lmillucci/building-a-vue-3-component-with-typescript-4pge"&gt;Type­Script sup­port&lt;/a&gt;, and some oth­er key improve­ments in Vue.js 3.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            But most­ly, the ver­sion of Vue we picked would be in use for some time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This arti­cle dis­cuss­es the changes we need­ed to make to con­vert the scaf­fold­ing over. It shows some real-world sit­u­a­tions and anno­tates the changes we had to make to get the code up and run­ning on Vue.js 3.&lt;/p&gt;

&lt;p&gt;This arti­cle does­n’t detail every change in Vue.js 3, for that check out the &lt;a href="https://vuejsdevelopers.com/2020/03/16/vue-js-tutorial/" rel="noopener noreferrer"&gt;Vue 3 Tuto­r­i­al (for Vue 2 users)&lt;/a&gt; arti­cle and the &lt;a href="https://devmode.fm/episodes/new-awesomeness-coming-in-vuejs-3-0" rel="noopener noreferrer"&gt;New awe­some­ness com­ing in Vue.js 3.0&lt;/a&gt; podcast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spoil­er alert:&lt;/strong&gt; it went well, we’re using Vue.js 3 for this project!&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of the Changes
&lt;/h2&gt;

&lt;p&gt;The changes we’re going to be mak­ing here are real­ly rel­a­tive­ly triv­ial. We have a sin­gle JavaScript entry point app.js file, and a &lt;a href="https://github.com/alexandermendes/vue-confetti" rel="noopener noreferrer"&gt;Vue­Con­fet­ti&lt;/a&gt; component.&lt;/p&gt;

&lt;p&gt;This skele­ton code is what I use for my scaf­fold­ing, because it’s nice to see some con­fet­ti to indi­cate that your code is work­ing as intended.&lt;/p&gt;

&lt;p&gt;The app.js is just a shell that does­n’t do much of any­thing oth­er than load the Vue­Con­fet­ti com­po­nent, but the project does demon­strate some inter­est­ing things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changes need­ed to your package.json file&lt;/li&gt;
&lt;li&gt;Changes need­ed to your web­pack config
&lt;/li&gt;
&lt;li&gt;The changes need­ed to instan­ti­ate a new Vue app&lt;/li&gt;
&lt;li&gt;How to do &lt;a href="https://webpack.js.org/guides/code-splitting/#dynamic-imports" rel="noopener noreferrer"&gt;web­pack dynam­ic imports&lt;/a&gt; of Vue 3 APIs&lt;/li&gt;
&lt;li&gt;How to use async com­po­nents in Vue 3 using new &lt;a href="https://github.com/vuejs/rfcs/pull/148" rel="noopener noreferrer"&gt;Async Com­po­nent API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How we can adapt a Vue plu­g­in that assumes being able to glob­al­ly inject instance prop­er­ties via Vue.prototype
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re using &lt;a href="https://cli.vuejs.org/" rel="noopener noreferrer"&gt;vue-cli&lt;/a&gt;, there’s a &lt;a href="https://github.com/vuejs/vue-cli-plugin-vue-next" rel="noopener noreferrer"&gt;vue-cli-plu­g­in-vue-next&lt;/a&gt; plu­g­in that will auto­mate some of the project con­ver­sion for you, but I want­ed to get my hands dirty.&lt;/p&gt;

&lt;p&gt;If you’re inter­est­ed in see­ing all of the major changes in Vue.js 3, check out the &lt;a href="https://github.com/vuejs/rfcs/pulls?q=is%3Apr+is%3Amerged+label%3A3.x" rel="noopener noreferrer"&gt;Vue.js merged RFCs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And now, with­out fur­ther ado… let’s get on with the show!&lt;/p&gt;

&lt;h2&gt;
  
  
  Package.json Changes
&lt;/h2&gt;

&lt;p&gt;The first thing we need to do is con­vert over the package.json pack­ages to ver­sions that work with Vue.js 3.&lt;/p&gt;

&lt;p&gt;Here are just the additions/​changes need­ed (not the com­plete package.json):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
    "devDependencies": {
        "@vue/compiler-sfc": "^3.0.0-beta.2",
        "css-loader": "^3.4.2",
        "file-loader": "^6.0.0",
        "mini-css-extract-plugin": "^0.9.0",
        "vue-loader": "^16.0.0-alpha.3"
    },
    "dependencies": {
        "vue": "^3.0.0-beta.14"
    }
}

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  web­pack con­fig changes
&lt;/h2&gt;

&lt;p&gt;Next up we need to make some very minor changes to the web­pack con­fig detailed in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;We just need to make two changes in the webpack.common.js file, and we’re done.&lt;/p&gt;

&lt;p&gt;First, we need to change how we import the &lt;a href="https://vue-loader.vuejs.org/guide/#vue-cli" rel="noopener noreferrer"&gt;Vue­Load­er­Plu­g­in&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;
const VueLoaderPlugin = require('vue-loader/lib/plugin');

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

&lt;/div&gt;



&lt;p&gt;To 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;
const { VueLoaderPlugin } = require('vue-loader');

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

&lt;/div&gt;



&lt;p&gt;Next up we need to change what file we alias vue to, by changing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        },

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

&lt;/div&gt;



&lt;p&gt;To 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;
        alias: {
            'vue$': 'vue/dist/vue.esm-bundler.js'
        },

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  app.js Changes
&lt;/h2&gt;

&lt;p&gt;House­keep­ing out of the way, now we can get into the actu­al changes in the JavaScript &amp;amp; Vue components.&lt;/p&gt;

&lt;p&gt;Here’s what the skele­ton app.js looked like for Vue.js 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Import our CSS
import styles from '../css/app.pcss';

// App main
const main = async () =&amp;gt; {
    // Async load the vue module
    const { default: Vue } = await import(/* webpackChunkName: "vue" */ 'vue');
    // Create our vue instance
    return new Vue({
        el: "#page-container",
        components: {
            'confetti': () =&amp;gt; import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
        },
        data: {
        },
    });
};

// Execute async function
main().then( (vm) =&amp;gt; {
});

// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

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

&lt;/div&gt;



&lt;p&gt;We have an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function" rel="noopener noreferrer"&gt;async func­tion&lt;/a&gt; main() that &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await" rel="noopener noreferrer"&gt;awaits&lt;/a&gt; the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" rel="noopener noreferrer"&gt;promise&lt;/a&gt; returned by the &lt;a href="https://webpack.js.org/guides/code-splitting/#dynamic-imports" rel="noopener noreferrer"&gt;web­pack dynam­ic import&lt;/a&gt; of the &lt;a href="https://012.vuejs.org/api/" rel="noopener noreferrer"&gt;Vue con­struc­tor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This pat­tern allows the main thread to con­tin­ue exe­cut­ing while web­pack han­dles dynam­i­cal­ly load­ing the vue chunk.&lt;/p&gt;

&lt;p&gt;While this is some­what point­less in the skele­ton code, this type of dynam­ic import­ing allows for code split­ting that becomes ben­e­fi­cial from a per­for­mance point of view as our appli­ca­tion gets fleshed out.&lt;/p&gt;

&lt;p&gt;Then we cre­ate a new View­Mod­el, adding in our &lt;a href="https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components" rel="noopener noreferrer"&gt;async com­po­nent&lt;/a&gt; Confetti.vue (we’ll get to the com­po­nent in a bit).&lt;/p&gt;

&lt;p&gt;Let’s have a look at the changes we need to make to this code to get it work­ing on Vue.js 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Import our CSS
import styles from '../css/app.pcss';

// App main
const main = async () =&amp;gt; {
    // Async load the Vue 3 APIs we need from the Vue ESM
    const { createApp, defineAsyncComponent } = await import(/* webpackChunkName: "vue" */ 'vue');
    // Create our root vue instance
    return createApp({
        components: {
            'confetti': defineAsyncComponent(() =&amp;gt; import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue')),
        },
        data: () =&amp;gt; ({
        }),
    }).mount("#page-container");
};

// Execute async function
main().then( (root) =&amp;gt; {
});

// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

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

&lt;/div&gt;



&lt;p&gt;The glob­al Vue con­struc­tor is gone in Vue.js 3, and instead we need to explic­it­ly import the func­tions from the Vue.js 3 API that we need.&lt;/p&gt;

&lt;p&gt;In this case, we’re going to need createApp() to cre­ate our app instance, and we’ll need defineAsyncComponent() to uti­lize the new &lt;a href="https://github.com/vuejs/rfcs/pull/148" rel="noopener noreferrer"&gt;Async Com­po­nent API&lt;/a&gt; for using async components.&lt;/p&gt;

&lt;p&gt;createApp() returns an app instance, which has an app con­text that is avail­able to all com­po­nents in the com­po­nent tree.&lt;/p&gt;

&lt;p&gt;Unlike Vue.js 2, this app does­n’t auto­mat­i­cal­ly mount, so we call .mount("#page-container"), which returns the root com­po­nent instance, which mounts on the DOM ele­ment with the id page-container.&lt;/p&gt;

&lt;p&gt;To get our async com­po­nent Confetti.vue work­ing, all we need to do is wrap the func­tion we used in Vue.js 2 with defineAsyncComponent().&lt;/p&gt;

&lt;p&gt;Also of note is that data can no longer be an object, but rather needs to be a fac­to­ry func­tion that returns a data object. While you’d often do this in Vue.js 2 already, it’s now manda­to­ry in Vue.js 3.&lt;/p&gt;

&lt;p&gt;If you want to learn more about some of these glob­al API changes, check out the &lt;a href="https://github.com/vuejs/rfcs/blob/master/active-rfcs/0009-global-api-change.md" rel="noopener noreferrer"&gt;Glob­al API Change RFC&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confetti.vue changes
&lt;/h2&gt;

&lt;p&gt;Now onto the all impor­tant Confetti.vue component! 🎉&lt;/p&gt;

&lt;p&gt;The exist­ing code for the Confetti.vue com­po­nent looks like this, and is rough­ly a copy &amp;amp; paste of the exam­ple on the &lt;a href="https://github.com/alexandermendes/vue-confetti" rel="noopener noreferrer"&gt;vue-con­fet­ti GitHub repo&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;
&amp;lt;template&amp;gt;
    &amp;lt;main&amp;gt;
    &amp;lt;/main&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
    import Vue from 'vue'
    import VueConfetti from 'vue-confetti'

    Vue.use(VueConfetti);

    export default {
        mounted: function() {
            this.$confetti.start({
                shape: 'heart',
                colors: ['DodgerBlue', 'OliveDrab', 'Gold', 'pink', 'SlateBlue', 'lightblue', 'Violet', 'PaleGreen', 'SteelBlue', 'SandyBrown', 'Chocolate', 'Crimson'],
            });
            setTimeout(() =&amp;gt; {
                this.$confetti.stop();
            }, 5000);
        },
        methods: {}
    }
&amp;lt;/script&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;Unfor­tu­nate­ly, this did­n’t work out of the box on Vue.js 3, giv­ing us the error:&lt;/p&gt;

&lt;p&gt;Uncaught TypeError: Cannot read property '$confetti' of undefined&lt;/p&gt;

&lt;p&gt;So to fig­ure out what was wrong here, I had a look at the &lt;a href="https://vuejs.org/v2/guide/plugins.html" rel="noopener noreferrer"&gt;Vue plu­g­in&lt;/a&gt; VueConfetti we’re import­ing, which looks 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;
import Confetti from './confetti';

export { Confetti };

export default {
  install(Vue, options) {
    if (this.installed) {
      return;
    }
    this.installed = true;
    Vue.prototype.$confetti = new Confetti(options); // eslint-disable-line no-param-reassign
  },
};

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

&lt;/div&gt;



&lt;p&gt;The way plu­g­ins work is that they define an install() func­tion that is called to do what­ev­er they need to do to install them­selves, when Vue.use() is called.&lt;/p&gt;

&lt;p&gt;In Vue.js 2, the glob­al Vue con­struc­tor is passed in as the first para­me­ter, but in Vue.js 3 we’d actu­al­ly be call­ing app.use(), and the first para­me­ter then becomes the app con­text, which is not a con­struc­tor, and thus has no .prototype.&lt;/p&gt;

&lt;p&gt;Indeed, if we console.log() the first para­me­ter passed in via Vue.js 2, we’ll see the Vue constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
ƒ Vue (options) {
  if ( true &amp;amp;&amp;amp;
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

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

&lt;/div&gt;



&lt;p&gt;But a console.log() the first para­me­ter passed in via Vue.js 3, we’ll see the app context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{_component: {…}, _props: null, _container: null, _context: {…}, …}
component: ƒ component(name, component)
config: (...)
directive: ƒ directive(name, directive)
mixin: ƒ mixin(mixin)
mount: (containerOrSelector) =&amp;gt; {…}
provide: ƒ provide(key, value)
unmount: ƒ unmount()
use: ƒ use(plugin, ...options)
_component: {components: {…}, data: ƒ}
_container: null
_context: {config: {…}, mixins: Array(0), components: {…}, directives: {…}, provides: {…}}
_props: null
get config: ƒ config()
set config: ƒ config(v)
__proto__ : Object

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

&lt;/div&gt;



&lt;p&gt;So okay, how can we fix this? The prob­lem is that Vue­Con­fet­ti is try­ing to inject a glob­al­ly shared instance prop­er­ty $confetti via Vue.prototype.$confetti, but don’t have a glob­al con­struc­tor in Vue.js 3, so .prototype isn’t a thing here.&lt;/p&gt;

&lt;p&gt;One way would be to change the vue-confetti/index.js code to use the new app instance’s config.globalProperties to accom­plish the same thing, some­thing like:&lt;/p&gt;

&lt;p&gt;app.config.globalProperties.$confetti = new Confetti(options);&lt;/p&gt;

&lt;p&gt;c.f.: &lt;a href="https://github.com/vuejs/rfcs/blob/master/active-rfcs/0009-global-api-change.md#attaching-globally-shared-instance-properties" rel="noopener noreferrer"&gt;Attach­ing Glob­al­ly Shared Instance Properties&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But this would require chang­ing the Vue­Con­fet­ti code via a fork/​pull request. While I’m not against doing this, I real­ized there was an eas­i­er way to accom­plish the same thing:&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;template&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
    import Confetti from 'vue-confetti/src/confetti.js';
    export default {
        data: () =&amp;gt; ({
            confetti: new Confetti(),
        }),
        mounted: function() {
            this.confetti.start({
                shape: 'heart',
                colors: ['DodgerBlue', 'OliveDrab', 'Gold', 'pink', 'SlateBlue', 'lightblue', 'Violet', 'PaleGreen', 'SteelBlue', 'SandyBrown', 'Chocolate', 'Crimson'],
            });
            setTimeout(() =&amp;gt; {
                this.confetti.stop();
            }, 5000);
        },
        methods: {}
    }
&amp;lt;/script&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;Here we change the Confetti.vue com­po­nent to direct­ly import 'vue-confetti/src/confetti.js' and assign the new Confetti() object to our local data state object, rather than hav­ing it be glob­al­ly available.&lt;/p&gt;

&lt;p&gt;This feels a lit­tle nicer to me in gen­er­al, because there’s prob­a­bly no great rea­son for the $confetti object to be glob­al­ly avail­able, if we’re cre­at­ing a Confetti.vue com­po­nent that can nice­ly encap­su­late it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you use Vue.js 3 now?
&lt;/h2&gt;

&lt;p&gt;We decid­ed to use Vue.js 3 now, but should you?&lt;/p&gt;

&lt;p&gt;I think much depends on how heav­i­ly you lean on third par­ty com­po­nents, plu­g­ins, and mixins.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            The more code you’ll be writ­ing your­self, the safer it is to use Vue.js &amp;lt;span&amp;gt;3&amp;lt;/span&amp;gt; now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;While all soft­ware always has issues, Vue.js 3 itself seems quite sol­id, and the first-par­ty pack­ages like &lt;a href="https://vuex.vuejs.org/" rel="noopener noreferrer"&gt;Vuex&lt;/a&gt; and &lt;a href="https://router.vuejs.org/" rel="noopener noreferrer"&gt;Vue-Router&lt;/a&gt; are com­ing along great.&lt;/p&gt;

&lt;p&gt;There will like­ly be some lag in third par­ty pack­ages get­ting updat­ed for Vue.js 3, and some may nev­er be.&lt;/p&gt;

&lt;p&gt;Thus whether to go with Vue.js 3 now real­ly depends on how much you rely on said third par­ty packages.&lt;/p&gt;

&lt;p&gt;For us, the ben­e­fits are worth­while enough for us to begin learn­ing and using Vue.js 3 now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap­ping up
&lt;/h2&gt;

&lt;p&gt;Hope­ful­ly this small dive into what it looks like updat­ing your code for Vue.js 3 is help­ful to you. While it is rel­a­tive­ly nar­row in scope, it does touch on some top­ics I had­n’t seen cov­ered else­where, at least not wrapped up in a neat package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnystudio107-ems2qegf7x6qiqq.netdna-ssl.com%2Fimg%2Fblog%2F_992x558_crop_center-center_82_line%2Fwrapped-up-in-a-bow.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fnystudio107-ems2qegf7x6qiqq.netdna-ssl.com%2Fimg%2Fblog%2F_992x558_crop_center-center_82_line%2Fwrapped-up-in-a-bow.jpg" alt="Wrapped up in a bow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m excit­ed to explore Vue.js 3 fur­ther, and will very like­ly doc­u­ment more of my jour­ney learn­ing the new hot­ness in Vue.js 3.&lt;/p&gt;

&lt;p&gt;Hap­py coding!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107" rel="noopener noreferrer"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>vue3</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Extending Craft CMS with Validation Rules and Behaviors</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Fri, 24 Apr 2020 02:55:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/extending-craft-cms-with-validation-rules-and-behaviors-4nma</link>
      <guid>https://dev.to/gaijinity/extending-craft-cms-with-validation-rules-and-behaviors-4nma</guid>
      <description>&lt;h1&gt;
  
  
  Extending Craft CMS with Validation Rules and Behaviors
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Craft CMS is a web appli­ca­tion that is amaz­ing­ly flex­i­ble &amp;amp; cus­tomiz­able using the built-in func­tion­al­i­ty that the plat­form offers. Use the platform!
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eD5ugwyZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/craft-cms-rules-behaviors-yii2-module.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eD5ugwyZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/craft-cms-rules-behaviors-yii2-module.jpg" alt="Craft cms rules behaviors yii2 module"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://craftcms.com/"&gt;Craft CMS&lt;/a&gt; is built on the rock-sol­id &lt;a href="https://www.yiiframework.com/"&gt;Yii2 frame­work&lt;/a&gt;, which is some­thing you nor­mal­ly don’t need to think about. It just works, as it should.&lt;/p&gt;

&lt;p&gt;But there are times that you need or want to extend the plat­form into some­thing tru­ly cus­tom, which we looked at in the &lt;a href="https://dev.to/gaijinity/enhancing-a-craft-cms-3-website-with-a-custom-module-7k7"&gt;Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;In this arti­cle, we’ll talk about two ways you can use the plat­form that you nor­mal­ly don’t even have to think about to your advantage.&lt;/p&gt;


                                Use the Platform
                            

&lt;p&gt;Some­thing we com­mon­ly hear in fron­tend devel­op­ment is to &lt;a href="https://timkadlec.com/remembers/2019-10-21-using-the-platform/"&gt;​“use the plat­form”&lt;/a&gt;, which I think is fan­tas­tic advice. Why re-invent an elab­o­rate cus­tom set­up when the plat­form already pro­vides you with a bat­tle-worn way to accom­plish your goals?&lt;/p&gt;

&lt;p&gt;The same holds true with any kind of devel­op­ment. If you’ve writ­ing some­thing on top of a plat­form — what­ev­er that plat­form may be — I think it always makes sense to try to lever­age it as much as possible.&lt;/p&gt;

&lt;p&gt;It’s there. It’s well thought-out. It’s test­ed. Use it!&lt;/p&gt;

&lt;p&gt;When we’re using Craft CMS, we’re also using the Yii2 plat­form that it’s built on. Indeed, as we dis­cussed in the &lt;a href="https://dev.to/gaijinity/so-you-wanna-make-a-craft-3-plugin-76m-temp-slug-4321262"&gt;So You Wan­na Make a Craft 3 Plu­g­in?&lt;/a&gt; arti­cle, to know Craft plu­g­in devel­op­ment, you will want to learn some part of Yii2.&lt;/p&gt;

&lt;p&gt;So let’s do just that! The &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/structure-overview"&gt;Yii2 doc­u­men­ta­tion&lt;/a&gt; is a great place to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mod­els &amp;amp; Rules
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/structure-models"&gt;Mod­els&lt;/a&gt; are a core build­ing block of Yii2, and so also Craft CMS. They are at the core of the &lt;a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller"&gt;Mod­el-View-Con­troller&lt;/a&gt; (MVC) par­a­digm that many frame­works use.&lt;/p&gt;


                                Mod­els are objects rep­re­sent­ing busi­ness data, rules and logic.
                            

&lt;p&gt;Mod­els are used to rep­re­sent data and val­i­date data via a set of rules. For instance, Craft CMS has a &lt;a href="https://docs.craftcms.com/api/v3/craft-elements-user.html"&gt;User ele­ment&lt;/a&gt; (which is also a mod­el) that encap­su­lates all of the data need­ed to rep­re­sent a User in Craft CMS.&lt;/p&gt;

&lt;p&gt;It also has &lt;a href="https://github.com/craftcms/cms/blob/develop/src/elements/User.php#L670"&gt;val­i­da­tion rules&lt;/a&gt; for the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/**
 * @inheritdoc
 */
protected function defineRules(): array
{
    $rules = parent::defineRules();
    $rules[] = [['lastLoginDate', 'lastInvalidLoginDate', 'lockoutDate', 'lastPasswordChangeDate', 'verificationCodeIssuedDate'], DateTimeValidator::class];
    $rules[] = [['invalidLoginCount', 'photoId'], 'number', 'integerOnly' =&amp;gt; true];
    $rules[] = [['username', 'email', 'unverifiedEmail', 'firstName', 'lastName'], 'trim', 'skipOnEmpty' =&amp;gt; true];
    $rules[] = [['email', 'unverifiedEmail'], 'email'];
    $rules[] = [['email', 'password', 'unverifiedEmail'], 'string', 'max' =&amp;gt; 255];
    $rules[] = [['username', 'firstName', 'lastName', 'verificationCode'], 'string', 'max' =&amp;gt; 100];
    $rules[] = [['username', 'email'], 'required'];
    $rules[] = [['username'], UsernameValidator::class];
    $rules[] = [['lastLoginAttemptIp'], 'string', 'max' =&amp;gt; 45];

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



&lt;p&gt;If this looks more like con­fig than code to you, then you’d be right! Mod­el val­i­da­tion rules are essen­tial­ly a list of rules that the data must pass in order to be con­sid­ered valid.&lt;/p&gt;

&lt;p&gt;Yii2 has a base &lt;a href="https://www.yiiframework.com/doc/api/2.0/yii-validators-validator"&gt;Val­ida­tor class&lt;/a&gt; to help you write val­ida­tors, and ships with a whole bunch of use­ful &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/tutorial-core-validators"&gt;Core Val­ida­tors&lt;/a&gt; built-in that you can leverage.&lt;/p&gt;

&lt;p&gt;And we can see here that Craft CMS is doing just that in its craft\elements\User.php class. Any val­i­da­tion rule is an array:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Field&lt;/strong&gt;  — the mod­el field (aka attribute or object prop­er­ty) or array of mod­el fields to apply this val­i­da­tion rule to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Val­ida­tor&lt;/strong&gt;  — the val­ida­tor to use, which can be a Val­ida­tor class, an alias to a val­ida­tor class, &lt;a href="https://www.php.net/manual/en/language.types.callable.php"&gt;PHP Callable&lt;/a&gt;, or even an anony­mous func­tion for &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/input-validation#inline-validators"&gt;inline val­i­da­tion&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[params]&lt;/strong&gt; — depend­ing on the val­ida­tor, there may be addi­tion­al option­al para­me­ters you can define&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So in the above User Ele­ment exam­ple, the email &amp;amp; unverifiedEmail fields are using the built-in &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/tutorial-core-validators#email"&gt;email core val­ida­tor&lt;/a&gt; that Yii2 provides.&lt;/p&gt;

&lt;p&gt;The username has sev­er­al val­i­da­tion rules list­ed, which are applied in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/tutorial-core-validators#string"&gt;&lt;strong&gt;string&lt;/strong&gt;&lt;/a&gt; — This val­ida­tor checks if the input val­ue is a valid string with cer­tain length (100 in this case)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/tutorial-core-validators#required"&gt;required&lt;/a&gt;&lt;/strong&gt; — This val­ida­tor checks if the input val­ue is pro­vid­ed and not empty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/craftcms/cms/blob/develop/src/validators/UsernameValidator.php"&gt;User­nameVal­ida­tor&lt;/a&gt;&lt;/strong&gt; — This is a cus­tom val­ida­tor that P&amp;amp;T wrote to han­dle val­i­dat­ing the user­name field&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user­name field actu­al­ly gives us a fun lit­tle tan­gent we can go on, so let’s peek under the hood to see how sim­ple it can be to write a cus­tom validator.&lt;/p&gt;

&lt;p&gt;Here’s what the class looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * @link https://craftcms.com/
 * @copyright Copyright (c) Pixel &amp;amp; Tonic, Inc.
 * @license https://craftcms.github.io/license/
 */

namespace craft\validators;

use Craft;
use yii\validators\Validator;

/**
 * Class UsernameValidator.
 *
 * @author Pixel &amp;amp; Tonic, Inc. &amp;lt;support@pixelandtonic.com&amp;gt;
 * @since 3.0.0
 */
class UsernameValidator extends Validator
{
    /**
     * @inheritdoc
     */
    public function validateValue($value)
    {
        // Don't allow whitespace in the username
        if (preg_match('/\s+/', $value)) {
            return [Craft::t('app', '{attribute} cannot contain spaces.'), []];
        }

        return null;
    }
}

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



&lt;p&gt;At its sim­plest form, this is all a Val­ida­tor needs to imple­ment! Giv­en some passed in $value, return whether it pass­es val­i­da­tion or not.&lt;/p&gt;

&lt;p&gt;In this case, it’s just check­ing if it pass­es a reg­u­lar expres­sion (&lt;a href="https://regexr.com/"&gt;RegEx&lt;/a&gt;) test.&lt;/p&gt;

&lt;p&gt;And indeed, we can even sim­pli­fy this fur­ther, and get rid of the cus­tom val­ida­tor alto­geth­er by using the &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/tutorial-core-validators#match"&gt;match core val­ida­tor&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    $rules[] = [['username'], 'match', '/\s+/', 'not' =&amp;gt; true];

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



&lt;p&gt;Then we’re real­ly be using the plat­form, and get­ting rid of cus­tom code.&lt;/p&gt;


                                Being able to delete code is one of the most under­rat­ed joys of programming
                            

&lt;p&gt;But let’s return from our tan­gent, and see how we can lever­age these rules to our own advan­tage. Let’s say we have spe­cif­ic require­ments for our username and password fields.&lt;/p&gt;

&lt;p&gt;Well, we can eas­i­ly &lt;em&gt;extend&lt;/em&gt; the exist­ing mod­el val­i­da­tion rules for our User Ele­ment by lis­ten­ing for the User class trig­ger­ing the EVENT_DEFINE_RULES event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule;

use modules\sitemodule\rules\UserRules;

use craft\elements\User;
use craft\events\DefineRulesEvent;

// ...

class SiteModule extends Module
{
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        // Add in our custom rules for the User element validation
        Event::on(
            User::class,
            User::EVENT_DEFINE_RULES,
            static function(DefineRulesEvent $event) {
                foreach(UserRules::define() as $rule) {
                    $event-&amp;gt;rules[] = $rule;
                }
            });
    // ...
    }
}

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



&lt;p&gt;We’re call­ing our cus­tom class method UserRules::define() to return a list of rules we want to add, and then we’re adding them one by one to the $event-&amp;gt;rules&lt;/p&gt;

&lt;p&gt;Here’s what the UserRules class looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule\rules;

use Craft;

/**
 * @author nystudio107
 * @package SiteModule
 * @since 1.0.0
 */
class UserRules
{
    // Constants
    // =========================================================================

    const USERNAME_MIN_LENGTH = 5;
    const USERNAME_MAX_LENGTH = 15;
    const PASSWORD_MIN_LENGTH = 7;

    // Public Methods
    // =========================================================================

    /**
     * Return an array of Yii2 validator rules to be added to the User element
     * https://www.yiiframework.com/doc/guide/2.0/en/input-validation
     *
     * @return array
     */
    public static function define(): array
    {
        return [
            [
                'username',
                'string',
                'length' =&amp;gt; [self::USERNAME_MIN_LENGTH, self::USERNAME_MAX_LENGTH],
                'tooLong' =&amp;gt; Craft::t(
                    'site-module',
                    'Your username {max} characters or shorter.',
                    [
                        'min' =&amp;gt; self::USERNAME_MIN_LENGTH,
                        'max' =&amp;gt; self::USERNAME_MAX_LENGTH
                    ]
                ),
                'tooShort' =&amp;gt; Craft::t(
                    'site-module',
                    'Your username must {min} characters or longer.',
                    [
                        'min' =&amp;gt; self::USERNAME_MIN_LENGTH,
                        'max' =&amp;gt; self::USERNAME_MAX_LENGTH
                    ]
                ),
            ],
            [
                'password',
                'string',
                'min' =&amp;gt; self::PASSWORD_MIN_LENGTH,
                'tooShort' =&amp;gt; Craft::t(
                    'site-module',
                    'Your password must be at least {min} characters.',
                    ['min' =&amp;gt; self::PASSWORD_MIN_LENGTH]
                )
            ],
            [
                'password',
                'match',
                'pattern' =&amp;gt; '/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&amp;amp;\*])(?=.{7,})/',
                'message' =&amp;gt; Craft::t(
                    'site-module',
                    'Your password must contain at least one of each of the following: A number, a lower-case character, an upper-case character, and a special character'
                )
            ],
        ];
    }
}

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



&lt;p&gt;And then BOOM! Just like that we’ve extend­ed the User Ele­ment mod­el val­i­da­tion rules with our own cus­tom rules.&lt;/p&gt;

&lt;p&gt;We’re even giv­ing it the cus­tom message to dis­play if the password field does­n’t match, as well as the mes­sage to dis­play if the username field is tooLong or tooShort.&lt;/p&gt;

&lt;p&gt;Nice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JS1RH9zX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x684_crop_center-center_100_line/craft-cms-model-validation-rules-frontend.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JS1RH9zX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x684_crop_center-center_100_line/craft-cms-model-validation-rules-frontend.png" alt="Craft cms model validation rules frontend"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, we even get the dis­play of the val­i­da­tion errors ​“for free” on the fron­tend, with­out hav­ing to do any addi­tion­al work.&lt;/p&gt;


                                The less code you write, the few­er chances you have to intro­duce bugs
                            

&lt;p&gt;Bear in mind that while we’re show­ing the User Ele­ment as an exam­ple, we can do this for any mod­el that Craft uses.&lt;/p&gt;

&lt;p&gt;For instance, if you want to make the address field in &lt;a href="https://craftcms.com/commerce"&gt;Craft Com­merce&lt;/a&gt; required, this is your ticket!&lt;/p&gt;

&lt;h2&gt;
  
  
  Mod­els &amp;amp; Behaviors
&lt;/h2&gt;

&lt;p&gt;But what if we want to add some prop­er­ties or meth­ods to an exist­ing mod­el? Well, we can do that, too, via &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/concept-behaviors"&gt;Yii2 Behav­iors&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To extend our User Ele­ment with a cus­tom Behav­ior, we can lis­ten for the User class trig­ger­ing the EVENT_DEFINE_BEHAVIORS event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule;

use modules\sitemodule\behaviors\UserBehavior;

use craft\elements\User;
use craft\events\DefineBehaviorsEvent;

// ...

class SiteModule extends Module
{
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        // Add in our custom behavior for the User element
        Event::on(
            User::class,
            User::EVENT_DEFINE_BEHAVIORS,
            static function(DefineBehaviorsEvent $event) {
                $event-&amp;gt;behaviors['userBehavior'] = ['class' =&amp;gt; UserBehavior::class];
            });
    // ...
    }
}

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



&lt;p&gt;Here we just add our userBehavior by set­ting the $event-&amp;gt;behaviors['userBehavior'] to a cus­tom UserBehavior class we wrote that inher­its from the Yii2 &lt;a href="https://www.yiiframework.com/doc/api/2.0/yii-base-behavior"&gt;Behav­ior&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule\behaviors;

use craft\elements\User;

use yii\base\Behavior;

/**
 * @author nystudio107
 * @package SiteModule
 * @since 1.0.0
 */
class UserBehavior extends Behavior
{
    // Public Properties
    // =========================================================================

    // Public Methods
    // =========================================================================

    /**
     * @inheritDoc
     */
    public function events()
    {
        return [
            User::EVENT_BEFORE_SAVE =&amp;gt; 'beforeSave',
        ];
    }

    /**
     * Save last names in upper-case
     *
     * @param $event
     */
    public function beforeSave($event)
    {
        $this-&amp;gt;owner-&amp;gt;lastName = mb_strtoupper($this-&amp;gt;owner-&amp;gt;lastName);
    }

    /**
     * Return a friendly name with a smile
     *
     * @return string
     */
    public function getHappyName()
    {
        $name = $this-&amp;gt;owner-&amp;gt;getFriendlyName();

        return ':) ' . $name;
    }
}

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



&lt;p&gt;We’re using the events() method to define the &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/concept-behaviors#handling-component-events"&gt;Com­po­nent Events&lt;/a&gt; we want our behav­ior to lis­ten for.&lt;/p&gt;

&lt;p&gt;In our case, we’re lis­ten­ing for the EVENT_BEFORE_SAVE event, and we’re call­ing a new method we added called beforeSave.&lt;/p&gt;

&lt;p&gt;In the con­text of a behav­ior, $this-&amp;gt;owner refers to the Mod­el object that our behav­ior is attached to; in our case, that’s a User Element.&lt;/p&gt;

&lt;p&gt;So our beforeSave() method just upper-cas­es the User::$lastName prop­er­ty before sav­ing it. So every­one’s last name will be upper-case.&lt;/p&gt;

&lt;p&gt;Then we’ve added a getHappyName() method that prepends a smi­ley face to the User Ele­men­t’s name, so in our Twig tem­plates we can now do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{{ currentUser.getHappyName() }}

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



&lt;p&gt;Pret­ty slick, we just pig­gy­backed on the exist­ing Craft User Ele­ment func­tion­al­i­ty with­out hav­ing to do a while lot of work.&lt;/p&gt;

&lt;p&gt;In our Behav­ior, if we defined any addi­tion­al prop­er­ties, they’d be added to the User Ele­ment mod­el as well… which opens up a whole world of possibilities.&lt;/p&gt;

&lt;p&gt;In addi­tion to writ­ing our own cus­tom behav­iors, we can also lever­age oth­er built-in Behav­iors that Yii2 offers, and add them to our own Mod­els. My per­son­al favorite is the &lt;a href="https://www.yiiframework.com/doc/api/2.0/yii-behaviors-attributetypecastbehavior"&gt;Attrib­ut­e­Type­cast­Be­hav­ior&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Check out Zoltan’s arti­cle &lt;a href="https://medium.com/@drifter/extending-entries-with-yii-behaviors-in-craft-3-c06e33bd1f0"&gt;Extend­ing entries with Yii behav­iors in Craft 3&lt;/a&gt; for even more on behaviors.&lt;/p&gt;

&lt;p&gt;I’d also like to note what Behav­iors are &lt;strong&gt;not&lt;/strong&gt;. You can &lt;strong&gt;not&lt;/strong&gt; over­ride an exist­ing method with a Behav­ior. You might want over­ride an exist­ing method and replace it with your own code dynam­i­cal­ly… but Behav­iors can­not do that.&lt;/p&gt;

&lt;p&gt;Behav­iors can only &lt;em&gt;extend&lt;/em&gt;, not &lt;em&gt;replace&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap­ping Up
&lt;/h2&gt;

&lt;p&gt;In addi­tion to ​“use the plat­form”, when­ev­er we’re adding code, I think we should add as lit­tle as possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IHmbbfMB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x558_crop_center-center_82_line/code-with-precision-like-a-surgeon.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IHmbbfMB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x558_crop_center-center_82_line/code-with-precision-like-a-surgeon.jpg" alt="Code with precision like a surgeon"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, there are oth­er ways to add the func­tion­al­i­ty we’ve shown in this arti­cle, but the meth­ods dis­cussed here are sim­ple, and require less code.&lt;/p&gt;


                                Sim­ple is Good
                            

&lt;p&gt;When adding code to an exist­ing project or frame­work, you typ­i­cal­ly want to go in like a sur­geon, chang­ing as lit­tle as pos­si­ble to achieve the desired effect.&lt;/p&gt;

&lt;p&gt;Hap­py coding!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>craftcms</category>
      <category>craft3</category>
      <category>php</category>
    </item>
    <item>
      <title>An Annotated Docker Config for Frontend Web Development</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Sun, 22 Mar 2020 16:12:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin</link>
      <guid>https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin</guid>
      <description>&lt;h1&gt;
  
  
  An Annotated Docker Config for Frontend Web Development
&lt;/h1&gt;

&lt;h3&gt;
  
  
  A local devel­op­ment envi­ron­ment with Dock­er allows you to shrink-wrap the devops your project needs as con­fig, mak­ing onboard­ing frictionless
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4p30NTBR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/5189/an-annotated-docker-config-for-frontend-web-development.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4p30NTBR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/5189/an-annotated-docker-config-for-frontend-web-development.jpg" alt="An annotated docker config for frontend web development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.docker.com/"&gt;Dock­er&lt;/a&gt; is a tool for con­tainer­iz­ing your appli­ca­tions, which means that your appli­ca­tion is shrink-wrapped with the envi­ron­ment that it needs to run.&lt;/p&gt;

&lt;p&gt;This allows you to define the devops your appli­ca­tion needs in order to run as con­fig, which can then be eas­i­ly repli­cat­ed and reused.&lt;/p&gt;


                                The prin­ci­pals and approach dis­cussed in this arti­cle are universal.
                            

&lt;p&gt;While there are many uses for Dock­er, this arti­cle will focus on using Dock­er as a local envi­ron­ment for fron­tend web development.&lt;/p&gt;

&lt;p&gt;Although &lt;a href="https://craftcms.com/"&gt;Craft CMS&lt;/a&gt; is ref­er­enced in this arti­cle, Dock­er works well for any kind of web devel­op­ment with any kind of CMS or dev stack (Lar­avel, Node­JS, Rails, whatevs).&lt;/p&gt;

&lt;p&gt;The Dock­er con­fig used here is used in both the &lt;a href="https://github.com/nystudio107/devmode"&gt;dev​Mode​.fm GitHub repo&lt;/a&gt;, and in the &lt;a href="https://github.com/nystudio107/craft"&gt;nystudio107/​craft&lt;/a&gt; boil­er­plate Com­pos­er project if you want to see some ​“in the wild” examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Dock­er?
&lt;/h2&gt;

&lt;p&gt;If you’re doing fron­tend web devel­op­ment, you very like­ly already have some kind of a local devel­op­ment environment. &lt;/p&gt;


                                So why should you use Dock­er instead?
                            

&lt;p&gt;This is a very rea­son­able ques­tion to ask, because any kind of switch of tool­ing requires some upskilling, and some work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TSIjJIXO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-frontend-web-development.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TSIjJIXO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-frontend-web-development.jpg" alt="Docker containers frontend web development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve long been using &lt;a href="https://laravel.com/docs/6.x/homestead"&gt;Home­stead&lt;/a&gt;—which is real­ly just a cus­tom &lt;a href="https://www.vagrantup.com/"&gt;Vagrant&lt;/a&gt; box with some extras — as my local dev envi­ron­ment as dis­cussed in the &lt;a href="https://dev.to/gaijinity/local-development-with-vagrant-homestead-2kn7"&gt;Local Devel­op­ment with Vagrant / Home­stead&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;I’d cho­sen to use Home­stead because I want­ed a local dev envi­ron­ment that was deter­min­is­tic, dis­pos­able, and sep­a­rat­ed my devel­op­ment envi­ron­ment from my actu­al computer.&lt;/p&gt;

&lt;p&gt;Dock­er has all of these advan­tages, but also a much more light­weight approach. Here are the advan­tages of Dock­er for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each appli­ca­tion has exact­ly the envi­ron­ment it needs to run, includ­ing spe­cif­ic ver­sions of any of the plumb­ing need­ed to get it to work (PHP, MySQL, Post­gres, whatever)&lt;/li&gt;
&lt;li&gt;Onboard­ing oth­ers becomes triv­ial, all they need to do is &lt;a href="https://docs.docker.com/install/"&gt;install Dock­er&lt;/a&gt; and type docker-compose up and away they go&lt;/li&gt;
&lt;li&gt;Your devel­op­ment envi­ron­ment is entire­ly dis­pos­able; if some­thing goes wrong, you just delete it and fire up a new one&lt;/li&gt;
&lt;li&gt;Your local com­put­er is sep­a­rate from your devel­op­ment envi­ron­ment, so switch­ing com­put­ers is triv­ial, and you won’t run into issues where you hose your com­put­er or are stuck with con­flict­ing ver­sions of devops services&lt;/li&gt;
&lt;li&gt;The cost of try­ing dif­fer­ent ver­sions of var­i­ous ser­vices is low; just change a num­ber in a .yaml file, docker-compose up, and away you go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are oth­er advan­tages as well, but these are the more impor­tant ones for me.&lt;/p&gt;

&lt;p&gt;Addi­tion­al­ly, con­tainer­iz­ing your appli­ca­tion in local devel­op­ment is a great first step to using a con­tainer­ized deploy­ment process, and run­ning Dock­er in pro­duc­tion as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under­stand­ing Docker
&lt;/h2&gt;

&lt;p&gt;This arti­cle is not a com­pre­hen­sive tuto­r­i­al on Dock­er, but I will attempt to explain some of the more impor­tant, broad­er concepts.&lt;/p&gt;

&lt;p&gt;Dock­er has the notion of con­tain­ers, each of which run one or more ser­vices. You can think of each con­tain­er as a mini vir­tu­al machine (even though tech­ni­cal­ly, they are not).&lt;/p&gt;

&lt;p&gt;While you can run mul­ti­ple ser­vices in a sin­gle Dock­er con­tain­er, sep­a­rat­ing each ser­vice out into a sep­a­rate con­tain­er has many advantages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HU6WFOTo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-separate-services.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HU6WFOTo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-separate-services.jpg" alt="Docker containers separate services"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If &lt;a href="https://www.php.net/"&gt;PHP&lt;/a&gt;, &lt;a href="https://httpd.apache.org/"&gt;Apache&lt;/a&gt;, and &lt;a href="https://www.mysql.com/"&gt;MySQL&lt;/a&gt; are all in sep­a­rate con­tain­ers, they won’t affect each oth­er, and also can be more eas­i­ly swapped in and out.&lt;/p&gt;

&lt;p&gt;If you decide you want to use &lt;a href="https://www.nginx.com/"&gt;Nginx&lt;/a&gt; or &lt;a href="https://www.postgresql.org/"&gt;Post­gres&lt;/a&gt; instead, the decou­pling into sep­a­rate con­tain­ers makes it easy!&lt;/p&gt;

&lt;p&gt;Dock­er con­tain­ers are built from &lt;a href="https://stackify.com/docker-image-vs-container-everything-you-need-to-know/"&gt;Dock­er images&lt;/a&gt;, which can be thought of as a recipe for build­ing the con­tain­er, with all of the files and code need­ed to make it happen.&lt;/p&gt;


                                If a Dock­er image is the recipe, a Dock­er con­tain­er is the fin­ished result­ing meal.
                            

&lt;p&gt;Dock­er images almost always are lay­ered on top of oth­er exist­ing images that they extend FROM. For instance, you might have a base image from &lt;a href="https://hub.docker.com/_/ubuntu"&gt;Ubun­tu&lt;/a&gt; or &lt;a href="https://hub.docker.com/_/alpine"&gt;Alpine&lt;/a&gt; Lin­ux which pro­vide in the nec­es­sary oper­at­ing sys­tem ker­nel lay­er for oth­er process­es like Nginx to run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Rx7h7isn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x983_crop_center-center_100_line/docker-image-layers.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Rx7h7isn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x983_crop_center-center_100_line/docker-image-layers.png" alt="Docker image layers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This lay­er­ing works thanks to the &lt;a href="https://medium.com/@nagarwal/docker-containers-filesystem-demystified-b6ed8112a04a"&gt;Union file sys­tem&lt;/a&gt;, which han­dles com­pos­ing all the lay­ers of the cake togeth­er for you.&lt;/p&gt;

&lt;p&gt;We said ear­li­er that Dock­er is more light­weight than run­ning a full Vagrant VM, and it is… but unfor­tu­nate­ly, unless you’re run­ning Lin­ux there still is a vir­tu­al­iza­tion lay­er run­ning, which is &lt;a href="https://docs.docker.com/docker-for-mac/docker-toolbox/"&gt;Hyper­K­it&lt;/a&gt; for the Mac, and &lt;a href="https://docs.docker.com/docker-for-windows/install/"&gt;Hyper‑V&lt;/a&gt; for Windows.&lt;/p&gt;


                                Dock­er for Mac &lt;span&gt;&amp;amp;&lt;/span&gt; Win­dows still has a vir­tu­al­iza­tion lay­er, it’s just rel­a­tive­ly lightweight.
                            

&lt;p&gt;For­tu­nate­ly, you don’t need to be con­cerned with any of this, but the per­for­mance impli­ca­tions do inform some of the deci­sions we’ve made in the Dock­er con­fig pre­sent­ed here.&lt;/p&gt;

&lt;p&gt;For more infor­ma­tion on Dock­er, for that I’d high­ly rec­om­mend the &lt;a href="https://www.udemy.com/course/docker-mastery/"&gt;Dock­er Mas­tery&lt;/a&gt; course (if it’s not on sale now, don’t wor­ry, it will be at some point) and also the fol­low­ing &lt;a href="https://devmode.fm/"&gt;dev​Mode​.fm&lt;/a&gt; episodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devmode.fm/episodes/containerize-your-development-with-docker"&gt;Con­tainer­ize your Devel­op­ment with Docker!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and there are tons of oth­er excel­lent edu­ca­tion­al resources on Dock­er out there such as Matt Gray’s &lt;a href="https://dotall.com/sessions/craft-in-docker-everything-ive-learnt"&gt;Craft in Dock­er: Every­thing I’ve Learnt&lt;/a&gt; pre­sen­ta­tion, and his excel­lent &lt;a href="https://mattgrayisok.com/a-craft-cms-development-workflow-with-docker-part-1-local-development"&gt;A Craft CMS Devel­op­ment Work­flow With Dock­er&lt;/a&gt;series.&lt;/p&gt;

&lt;p&gt;In our arti­cle, we will focus on anno­tat­ing a real-world Dock­er con­fig that’s used in pro­duc­tion. We’ll dis­cuss var­i­ous Dock­er con­cepts as we go, but the pri­ma­ry goal here is doc­u­ment­ing a work­ing config.&lt;/p&gt;


                                This arti­cle is what I wished exist­ed when I start­ed learn­ing Docker
                            

&lt;p&gt;I learn best by look­ing at a work­ing exam­ple, and pick­ing it apart. If you do, too, let’s get going!&lt;/p&gt;

&lt;h2&gt;
  
  
  My Dock­er Direc­to­ry Structure
&lt;/h2&gt;

&lt;p&gt;This Dock­er set­up uses a direc­to­ry struc­ture that looks like this (don’t wor­ry, it’s not as com­plex as it seems, many of the Dock­er images here are for ref­er­ence only, and are actu­al­ly pre-built):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
├── cms
│ ├── composer.json
│ ├── config
│ ├── craft
│ ├── craft.bat
│ ├── example.env
│ ├── modules
│ ├── templates
│ └── web
├── docker-compose.yml
├── docker-config
│ ├── mariadb
│ │ └── Dockerfile
│ ├── nginx
│ │ ├── default.conf
│ │ └── Dockerfile
│ ├── php-dev-base
│ │ ├── Dockerfile
│ │ └── zzz-docker.conf
│ ├── php-dev-craft
│ │ └── Dockerfile
│ ├── postgres
│ │ └── Dockerfile
│ ├── redis
│ │ └── Dockerfile
│ ├── webpack-dev-base
│ │ └── Dockerfile
│ └── webpack-dev-craft
│ ├── Dockerfile
│ ├── package.json
│ ├── postcss.config.js
│ ├── tailwind.config.js
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ ├── webpack.prod.js
│ └── webpack.settings.js
├── scripts
│ ├── common
│ ├── docker_pull_db.sh
│ ├── docker_restore_db.sh
│ ├── example.env.sh
│ └── seed_db.sql
└── src
    ├── conf
    ├── css
    ├── img
    ├── js
    ├── php
    ├── templates -&amp;gt; ../cms/templates
    └── vue

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



&lt;p&gt;Here’s an expla­na­tion of what the top-lev­el direc­to­ries are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
cms — every­thing need­ed to run Craft CMS. The is the ​“app” of the project&lt;/li&gt;
&lt;li&gt;
docker-config — an indi­vid­ual direc­to­ry for each ser­vice that the Dock­er set­up uses, with a Dockerfile and oth­er ancil­lary con­fig files therein&lt;/li&gt;
&lt;li&gt;
scripts — helper shell scripts that do things like pull a remote or local data­base into the run­ning Dock­er con­tain­er. These are derived from the &lt;a href="https://github.com/nystudio107/craft-scripts"&gt;Craft-Scripts&lt;/a&gt; shell scripts&lt;/li&gt;
&lt;li&gt;
src — the fron­tend JavaScript, CSS, Vue, etc. source code that the project uses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each ser­vice is ref­er­enced in the docker-compose.yaml file, and defined in the Dockerfile that is in the cor­re­spond­ing direc­to­ry in the docker-config/ directory.&lt;/p&gt;

&lt;p&gt;It isn’t strict­ly nec­es­sary to have a sep­a­rate Dockerfile for each ser­vice, if they are just derived from a base image. But I like the con­sis­ten­cy, and ease of future expan­sion should some­thing cus­tom be nec­es­sary down the road.&lt;/p&gt;

&lt;p&gt;You’ll also notice that there are php-dev-base and php-dev-craft direc­to­ries, as well as webpack-dev-base and webpack-dev-craft direc­to­ries, and might be won­der­ing why they aren’t consolidated.&lt;/p&gt;

&lt;p&gt;The rea­son is that there’s a whole lot of the base set­up in both that just nev­er changes, so instead of rebuild­ing that each time, we can build it once and pub­lish the images up on Dock​er​Hub​.com as &lt;a href="https://hub.docker.com/repository/docker/nystudio107/php-dev-base"&gt;nys­tu­dio107/php-dev-base&lt;/a&gt; and &lt;a href="https://hub.docker.com/repository/docker/nystudio107/webpack-dev-base"&gt;nys­tu­dio107/web­pack-dev-base&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then we can lay­er any­thing spe­cif­ic about our project on top of these base images in the respec­tive -craft ser­vices. This saves us sig­nif­i­cant build­ing time, while keep­ing flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The docker-compose.yaml file
&lt;/h2&gt;

&lt;p&gt;While a docker-compose.yaml file isn’t required when using Dock­er, from a prac­ti­cal point of view, you’ll almost always use it. The docker-compose.yaml file allows you to define mul­ti­ple con­tain­ers for run­ning the ser­vices you need, and coor­di­nate start­ing them up and shut­ting them down in unison.&lt;/p&gt;

&lt;p&gt;Then all you need to do is run docker-compose up via the ter­mi­nal in a direc­to­ry that has a docker-compose.yaml file, and Dock­er will start up all of your con­tain­ers for you!&lt;/p&gt;

&lt;p&gt;Here’s an exam­ple of what that might look like, start­ing up your Dock­er containers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BCpOOn_g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x592_crop_center-center_100_line/docker-compose-up-terminal.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BCpOOn_g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x592_crop_center-center_100_line/docker-compose-up-terminal.png" alt="Docker compose up terminal"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s have a look at our docker-compose.yaml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
version: '3.7'

services:
  # nginx - web server
  nginx:
    build:
      context: .
      dockerfile: ./docker-config/nginx/Dockerfile
    env_file: &amp;amp;env
      - ./cms/.env
    links:
      - php
    ports:
      - "8000:80"
    volumes:
      - cpresources:/var/www/project/cms/web/cpresources
      - ./cms/web:/var/www/project/cms/web:cached
  # php - personal home page
  php:
    build:
      context: .
      dockerfile: ./docker-config/php-dev-craft/Dockerfile
    depends_on:
      - "mariadb"
      - "redis"
    env_file:
      *env
    expose:
      - "9000"
    links:
      - mariadb
      - redis
    volumes:
      - cpresources:/var/www/project/cms/web/cpresources
      - storage:/var/www/project/cms/storage
      - ./cms:/var/www/project/cms:cached
      - ./cms/vendor:/var/www/project/cms/vendor:delegated
      - ./cms/storage/logs:/var/www/project/cms/storage/logs:delegated
  # mariadb - database
  mariadb:
    build:
      context: .
      dockerfile: ./docker-config/mariadb/Dockerfile
    env_file:
      *env
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: project
      MYSQL_USER: project
      MYSQL_PASSWORD: project
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql
  # redis - key/value database for caching &amp;amp; php sessions
  redis:
    build:
      context: .
      dockerfile: ./docker-config/redis/Dockerfile
    expose:
      - "6379"
  # webpack - frontend build system
  webpack:
    build:
      context: .
      dockerfile: ./docker-config/webpack-dev-craft/Dockerfile
    env_file:
      *env
    ports:
      - "8080:8080"
    volumes:
      - ./docker-config/webpack-dev-craft:/var/www/project/docker-config/webpack-dev-craft:cached
      - ./docker-config/webpack-dev-craft/node_modules:/var/www/project/docker-config/webpack-dev-craft/node_modules:delegated
      - ./cms/web/dist:/var/www/project/cms/web/dist:delegated
      - ./src:/var/www/project/src:cached
      - ./cms/templates:/var/www/project/cms/templates:cached

volumes:
  db-data:
  cpresources:
  storage:

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



&lt;p&gt;This &lt;a href="https://yaml.org/"&gt;.yaml file&lt;/a&gt; has 3 top-lev­el keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
version — the ver­sion num­ber of the &lt;a href="https://docs.docker.com/compose/compose-file/"&gt;Dock­er Com­pose file,&lt;/a&gt; which cor­re­sponds to dif­fer­ent capa­bil­i­ties offered by dif­fer­ent ver­sions of the &lt;a href="https://docs.docker.com/engine/"&gt;Dock­er Engine&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
services — each ser­vice cor­re­sponds to a sep­a­rate Dock­er con­tain­er that is cre­at­ed using a sep­a­rate Dock­er image&lt;/li&gt;
&lt;li&gt;
volumes — &lt;a href="https://docs.docker.com/compose/compose-file/#volumes"&gt;named vol­umes&lt;/a&gt; that are mount­ed and can be shared amongst your Dock­er con­tain­ers (but not your host com­put­er), for stor­ing per­sis­tent data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll detail each ser­vice below, but there are a few inter­est­ing tid­bits to cov­er first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BUILD&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you’re cre­at­ing a Dock­er con­tain­er, you can either base it on an exist­ing image (either a local image or one pulled down from &lt;a href="https://DockerHub.com"&gt;Dock​er​Hub​.com&lt;/a&gt;), or you can build it local­ly via a Dockerfile.&lt;/p&gt;

&lt;p&gt;As men­tioned above, I chose the method­ol­o­gy that each ser­vice would be cre­at­ing as a build from a Dockerfile (all of which extend FROM an image up on &lt;a href="https://DockerHub.com"&gt;Dock​er​Hub​.com&lt;/a&gt;) to keep things consistent.&lt;/p&gt;

&lt;p&gt;This means that some of our Dockerfiles we use are noth­ing more than a sin­gle line, e.g.: FROM mariadb:10.3, but this set­up does allow for expansion.&lt;/p&gt;

&lt;p&gt;The two keys used for build are are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
context — this spec­i­fies where the work­ing direc­to­ry for the build should be, rel­a­tive to the docker-compose.yaml file. We have this set to . (the cur­rent direc­to­ry) for each service&lt;/li&gt;
&lt;li&gt;
dockerfile — this spec­i­fies a path to the Dockerfile to use to build the ser­vice Dock­er con­tain­er. Think of the Dockerfile as a local Dock­er image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the con­text is always the root direc­to­ry of the project, with the Dockerfile and any sup­port­ing files for each ser­vice are off in a sep­a­rate direc­to­ry. We do it this way to keep the paths con­sis­tent (always rel­a­tive to the project root) regard­less of the service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DEPENDS_ON&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This just lets you spec­i­fy what oth­er ser­vices this par­tic­u­lar ser­vice depends on; this allows you to ensure that oth­er con­tain­ers are up and run­ning before this con­tain­er starts up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ENV_FILE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The env_file set­ting spec­i­fies a path to your .env file for key/​value pairs that will be inject­ed into a Dock­er container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/docker/compose/issues/3702"&gt;Dock­er does not allow for quotes&lt;/a&gt; in its .env file, which is con­trary to how .env files work almost every­where else… so remove any quotes you have in your .env file.&lt;/p&gt;

&lt;p&gt;You’ll notice that for the nginx ser­vice, there’s a strange &amp;amp;env val­ue in the env_file set­ting, and for the oth­er ser­vices, the set­ting is *env. This is tak­ing advan­tage of &lt;a href="https://github.com/cyklo/Bukkit-OtherBlocks/wiki/Aliases-(advanced-YAML-usage)"&gt;YAML alias­es&lt;/a&gt;, so if we do change the .env file path, we only have to do it in one place.&lt;/p&gt;

&lt;p&gt;Doing it this way also ensures that all of the .env envi­ron­ment vari­ables are avail­able in every con­tain­er. For more on envi­ron­ment vari­ables, check out the &lt;a href="https://dev.to/gaijinity/flat-multi-environment-config-for-craft-cms-3-53h7-temp-slug-6416116"&gt;Flat Mul­ti-Envi­ron­ment Con­fig for Craft CMS 3&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Because it’s Dock­er that is inject­ing these .env envi­ron­ment vari­ables, if you change your .env file, you’ll need to restart your Dock­er containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LINKS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/compose/networking/#links"&gt;Links in Dock­er&lt;/a&gt; allow you to define extra alias­es by which a ser­vice is reach­able from anoth­er ser­vice. They are not required to enable ser­vices to com­mu­ni­cate, but I like being explic­it about it.&lt;/p&gt;

&lt;p&gt;The come into play when one con­tain­er needs to talk to anoth­er. For exam­ple, if you nor­mal­ly would com­mu­ni­cate with your data­base via the localhost sock­et, instead in our set­up you’d use the sock­et named mariadb.&lt;/p&gt;

&lt;p&gt;The key take-away is that when con­tain­ers need to talk to each oth­er, they are doing so over the inter­nal Dock­er net­work, and refer to each oth­er via their service or links name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PORTS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This spec­i­fies the port that should be exposed out­side of the con­tain­er, fol­lowed by the port that the con­tain­er uses inter­nal­ly. So for exam­ple, the nginx ser­vice has "8000:80", which means the exter­nal­ly acces­si­ble port for the Nginx web­serv­er is 8000, and the inter­nal port the ser­vice runs on is 80.&lt;/p&gt;

&lt;p&gt;If this sounds con­fus­ing, under­stand that &lt;a href="https://docs.docker.com/network/"&gt;Dock­er uses its own inter­nal net­work&lt;/a&gt; to allow con­tain­ers to talk to each oth­er, as well as the out­side world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VOL­UMES&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dock­er con­tain­ers run in their own lit­tle world, which is great for iso­la­tion pur­pos­es, but at some point you do need to share things from your ​“host” com­put­er with the Dock­er container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/compose/compose-file/#volumes"&gt;Dock­er vol­umes&lt;/a&gt; allow you to do this. You spec­i­fy either a named vol­ume or a path on your host, fol­lowed by the path where this vol­ume should be &lt;a href="https://docs.docker.com/storage/bind-mounts/"&gt;bind mount­ed&lt;/a&gt; in the Dock­er container.&lt;/p&gt;

&lt;p&gt;This is where per­for­mance prob­lems can hap­pen with Dock­er on the Mac and Win­dows. So we use some hints to &lt;a href="https://docs.docker.com/docker-for-mac/osxfs-caching/#tuning-with-consistent-cached-and-delegated-configurations"&gt;help with per­for­mance&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
consistent — per­fect con­sis­ten­cy (host and con­tain­er have an iden­ti­cal view of the mount at all times)&lt;/li&gt;
&lt;li&gt;
cached — the host’s view is author­i­ta­tive (per­mit delays before updates on the host appear in the container)&lt;/li&gt;
&lt;li&gt;
delegated — the container’s view is author­i­ta­tive (per­mit delays before updates on the con­tain­er appear in the host)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for things like node_modules/ and vendor/ we mark them as :del­e­gat­ed because while we want them shared, the con­tain­er is in con­trol of mod­i­fy­ing these volumes.&lt;/p&gt;

&lt;p&gt;Some Dock­er setups I’ve seen put these direc­to­ries into a named vol­ume, which means they are vis­i­ble only to the Dock­er containers.&lt;/p&gt;

&lt;p&gt;But the prob­lem is we lose out on our edi­tor auto-com­ple­tion, because our edi­tor has noth­ing to index.&lt;/p&gt;


                                This is a non-nego­tiable for me
                            

&lt;p&gt;See the &lt;a href="https://dev.to/gaijinity/auto-complete-craft-cms-3-apis-in-twig-with-phpstorm-5go9"&gt;Auto-Com­plete Craft CMS 3 APIs in Twig&lt;/a&gt; with Php­Storm arti­cle for details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ser­vice: Nginx
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.nginx.com/"&gt;Nginx&lt;/a&gt; is the web serv­er of choice for me, both in local dev and in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM nginx:1.16

COPY ./docker-config/nginx/default.conf /etc/nginx/conf.d/default.conf

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



&lt;p&gt;We’ve based the con­tain­er on the &lt;a href="https://hub.docker.com/_/nginx"&gt;nginx image&lt;/a&gt;, tagged at ver­sion 1.16&lt;/p&gt;

&lt;p&gt;The only mod­i­fi­ca­tion it makes is COPYing our default.conf file into place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
server {
    listen 80 default_server;
    root /var/www/project/cms/web;
    index index.html index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    access_log off;
    error_log /var/log/nginx/error.log error;

    sendfile off;
    ssi on;

    client_max_body_size 10m;

    gzip on;
    gzip_http_version 1.0;
    gzip_proxied any;
    gzip_min_length 500;
    gzip_disable "MSIE [1-6]\.";
    gzip_types text/plain text/xml text/css
                      text/comma-separated-values
                      text/javascript
                      application/x-javascript
                      application/javascript
                      application/atom+xml;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
        fastcgi_read_timeout 300;
    }

    location ~ /\.ht {
        deny all;
    }
}

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



&lt;p&gt;This is just a sim­ple Nginx con­fig that works well with Craft CMS. You can find more about Nginx con­figs for Craft CMS in the &lt;a href="https://github.com/nystudio107/nginx-craft"&gt;nginx-craft&lt;/a&gt; GitHub repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ser­vice: MariaDB
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mariadb.org/"&gt;Mari­aDB&lt;/a&gt; is a drop-in replace­ment for MySQL that I tend to use instead of MySQL itself. It was writ­ten by the orig­i­nal author of MySQL, and is bina­ry com­pat­i­ble with MySQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM mariadb:10.3

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



&lt;p&gt;We’ve based the con­tain­er on the &lt;a href="https://hub.docker.com/_/mariadb"&gt;mari­adb image&lt;/a&gt;, tagged at ver­sion 10.3&lt;/p&gt;

&lt;p&gt;There’s no mod­i­fi­ca­tion at all to the source image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ser­vice: Postgres
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/"&gt;Post­gres&lt;/a&gt; is a robust data­base that I am using more and more for Craft CMS projects. It’s not used in the docker-compose.yaml pre­sent­ed here, but I keep the con­fig­u­ra­tion around in case I want to use it.&lt;/p&gt;

&lt;p&gt;Post­gres is used in local dev and in pro­duc­tion on the &lt;a href="https://github.com/nystudio107/devmode"&gt;dev​Mode​.fm GitHub repo&lt;/a&gt;, if you want to see it implemented.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM postgres:12.2

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



&lt;p&gt;We’ve based the con­tain­er on the &lt;a href="https://hub.docker.com/_/postgres"&gt;post­gres image&lt;/a&gt;, tagged at ver­sion 12.2&lt;/p&gt;

&lt;p&gt;There’s no mod­i­fi­ca­tion at all to the source image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ser­vice: Redis
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://redis.io/"&gt;Redis&lt;/a&gt; is a key/​value pair data­base that I set all of my Craft CMS installs to use both as a &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/cms/config/app.php#L33"&gt;caching method&lt;/a&gt;, and as a &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/cms/config/app.php#L41"&gt;ses­sion han­dler&lt;/a&gt; for PHP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM redis:5.0

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



&lt;p&gt;We’ve based the con­tain­er on the &lt;a href="https://hub.docker.com/_/redis/"&gt;redis image&lt;/a&gt;, tagged at ver­sion 5.0&lt;/p&gt;

&lt;p&gt;There’s no mod­i­fi­ca­tion at all to the source image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ser­vice: PHP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.php.net/"&gt;PHP&lt;/a&gt; is the lan­guage that the Yii2 frame­work and Craft CMS itself is based on, so we need it in order to run our app.&lt;/p&gt;

&lt;p&gt;This ser­vice is com­posed of a base image that con­tains all of the pack­ages and PHP exten­sions we’ll always need to use, and then the project-spe­cif­ic image that con­tains what­ev­er addi­tion­al things are need­ed for our project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM php:7.3-fpm

# Install packages
RUN apt-get update \
    &amp;amp;&amp;amp; \
    # apt Debian packages
    apt-get install -y \
        apt-utils \
        autoconf \
        ca-certificates \
        curl \
        g++ \
        libbz2-dev \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        libpq-dev \
        libssl-dev \
        libicu-dev \
        libmagickwand-dev \
        libzip-dev \
        unzip \
        zip \
    &amp;amp;&amp;amp; \
    # pecl PHP extensions
    pecl install \
        imagick-3.4.4 \
        redis \
        xdebug-2.8.1 \
    &amp;amp;&amp;amp; \
    # Configure PHP extensions
    docker-php-ext-configure \
        gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    &amp;amp;&amp;amp; \
    # Install PHP extensions
    docker-php-ext-install \
        bcmath \
        bz2 \
        exif \
        ftp \
        gettext \
        gd \
        iconv \
        intl \
        mbstring \
        opcache \
        pdo \
        shmop \
        sockets \
        sysvmsg \
        sysvsem \
        sysvshm \
        zip \
    &amp;amp;&amp;amp; \
    # Enable PHP extensions
    docker-php-ext-enable \
        imagick \
        redis \
        xdebug

# Append our php.ini overrides to the end of the file
RUN echo "upload_max_filesize = 10M" &amp;gt; /usr/local/etc/php/php.ini &amp;amp;&amp;amp; \
    echo "post_max_size = 10M" &amp;gt;&amp;gt; /usr/local/etc/php/php.ini &amp;amp;&amp;amp; \
    echo "max_execution_time = 300" &amp;gt;&amp;gt; /usr/local/etc/php/php.ini &amp;amp;&amp;amp; \
    echo "memory_limit = 256M" &amp;gt;&amp;gt; /usr/local/etc/php/php.ini &amp;amp;&amp;amp; \
    echo "opcache.revalidate_freq = 0" &amp;gt;&amp;gt; /usr/local/etc/php/php.ini &amp;amp;&amp;amp; \
    echo "opcache.validate_timestamps = 1" &amp;gt;&amp;gt; /usr/local/etc/php/php.ini

# Copy the `zzz-docker.conf` file into place for php-fpm
COPY ./zzz-docker.conf /usr/local/etc/php-fpm.d/zzz-docker.conf

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



&lt;p&gt;We’ve based the con­tain­er on the &lt;a href="https://hub.docker.com/_/php"&gt;php image&lt;/a&gt;, tagged at ver­sion 7.3&lt;/p&gt;

&lt;p&gt;We’re then adding a bunch of &lt;a href="https://www.debian.org/"&gt;Debian&lt;/a&gt; pack­ages that we want avail­able for our &lt;a href="https://ubuntu.com/"&gt;Ubun­tu&lt;/a&gt; oper­at­ing sys­tem base, some debug­ging tools, as well as some PHP exten­sions that &lt;a href="https://docs.craftcms.com/v3/requirements.html#required-php-extensions"&gt;Craft CMS requires&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then we add some defaults to the php.ini, and copy into place the zzz-docker.conf file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[www]
pm.max_children = 10
pm.process_idle_timeout = 10s
pm.max_requests = 1000

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



&lt;p&gt;This just sets some defaults for php-fpm that make sense for local development.&lt;/p&gt;

&lt;p&gt;By itself, this image won’t do much for us, and in fact we don’t even spin up this image. But we’ve built this image, and made it avail­able as &lt;a href="https://hub.docker.com/repository/docker/nystudio107/php-dev-base"&gt;nys­tu­dio107/php-dev-base&lt;/a&gt; on DockerHub.&lt;/p&gt;

&lt;p&gt;Since it’s pre-built, we don’t have to build it every time, and can lay­er on top of this image any­thing project-spe­cif­ic via the php-dev-craft con­tain­er image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM nystudio107/php-dev-base

# Install packages
RUN apt-get update \
    &amp;amp;&amp;amp; \
    # apt Debian packages
    apt-get install -y \
        nano \
    &amp;amp;&amp;amp; \
    # Install PHP extensions
    docker-php-ext-install \
        pdo_mysql \
    &amp;amp;&amp;amp; \
    # Install Composer
    curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer

WORKDIR /var/www/project

# Copy over the directories/files php needs access to
COPY --chown=www-data:www-data ./cms/composer.* /var/www/project/cms/

# Create the storage directory and make it writeable by PHP
RUN mkdir -p /var/www/project/cms/storage &amp;amp;&amp;amp; \
    mkdir -p /var/www/project/cms/storage/runtime &amp;amp;&amp;amp; \
    chown -R www-data:www-data /var/www/project/cms/storage

# Create the cpresources directory and make it writeable by PHP
RUN mkdir -p /var/www/project/cms/web/cpresources &amp;amp;&amp;amp; \
    chown -R www-data:www-data /var/www/project/cms/web/cpresources

WORKDIR /var/www/project/cms

# Do a `composer install` without running any Composer scripts
# - If `composer.lock` is present, it will install what is in the lock file
# - If `composer.lock` is missing, it will update to the latest dependencies
# and create the `composer.lock` file
# Force permissions, update Craft, and start php-fpm
CMD if [! -f "./composer.lock"]; then \
        composer install --no-scripts --optimize-autoloader --no-interaction; \
    fi \
    &amp;amp;&amp;amp; \
    if [! -d ./vendor -o ! "$(ls -A ./vendor)"]; then \
        composer install --no-scripts --optimize-autoloader --no-interaction; \
    fi \
    &amp;amp;&amp;amp; \
    chown -R www-data:www-data /var/www/project/cms/vendor \
    &amp;amp;&amp;amp; \
    chown -R www-data:www-data /var/www/project/cms/storage \
    &amp;amp;&amp;amp; \
    composer craft-update \
    &amp;amp;&amp;amp; \
    php-fpm

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



&lt;p&gt;This is the image that we actu­al build into a con­tain­er, and use for our project. We install the nano edi­tor because I find it handy some­times, and we also install pdo_mysql so that PHP can con­nect to our Mari­aDB database.&lt;/p&gt;

&lt;p&gt;We do it this way so that if we want to cre­ate a Craft CMS project that uses Post­gres, we can just swap in the PDO exten­sion need­ed here.&lt;/p&gt;

&lt;p&gt;Then we make sure the var­i­ous storage/ and cpresources/ direc­to­ries are in place, with the right own­er­ship so that Craft will run properly.&lt;/p&gt;

&lt;p&gt;Then we do a bit of mag­ic to do a composer install, but only if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The composer.lock file does­n’t exist&lt;/li&gt;
&lt;li&gt;The vendor/ direc­to­ry does­n’t exist, or is empty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have to do the composer install as part of the Dock­er image CMD because the file sys­tem mounts aren’t in place until the CMD is run.&lt;/p&gt;

&lt;p&gt;This allows us to update our &lt;a href="https://getcomposer.org/"&gt;Com­pos­er&lt;/a&gt; depen­den­cies just by delet­ing the composer.lock file or the vendor/ direc­to­ry, and doing docker-compose up&lt;/p&gt;

&lt;p&gt;Sim­ple.&lt;/p&gt;

&lt;p&gt;The alter­na­tive is doing a docker exec -it craft_php_1 /bin/bash to open up a shell in our con­tain­er, and run­ning the com­mand man­u­al­ly. Which is fine, but a lit­tle con­vo­lut­ed for some.&lt;/p&gt;

&lt;p&gt;Then we make sure that the own­er­ship on impor­tant direc­to­ries is cor­rect, and we run the craft-update &lt;a href="https://getcomposer.org/doc/articles/scripts.md"&gt;Com­pos­er script&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
  "require": {
    "craftcms/cms": "^3.4.0",
    "vlucas/phpdotenv": "^3.4.0",
    "yiisoft/yii2-redis": "^2.0.6",
    "nystudio107/craft-imageoptimize": "^1.0.0",
    "nystudio107/craft-fastcgicachebust": "^1.0.0",
    "nystudio107/craft-minify": "^1.2.5",
    "nystudio107/craft-typogrify": "^1.1.4",
    "nystudio107/craft-retour": "^3.0.0",
    "nystudio107/craft-seomatic": "^3.2.0",
    "nystudio107/craft-webperf": "^1.0.0",
    "nystudio107/craft-twigpack": "^1.1.0",
    "nystudio107/dotenvy": "^1.1.0"
  },
  "autoload": {
    "psr-4": {
      "modules\\sitemodule\\": "modules/sitemodule/src/"
    }
  },
  "config": {
    "sort-packages": true,
    "optimize-autoloader": true,
    "platform": {
      "php": "7.0"
    }
  },
  "scripts": {
    "craft-update": [
      "@php craft migrate/all",
      "@php craft project-config/sync",
      "@php craft clear-caches/all"
    ],
    "post-root-package-install": [
      "@php -r \"file_exists('.env') || copy('example.env', '.env');\""
    ],
    "post-update-cmd": "@craft-update",
    "post-install-cmd": "@craft-update"
  }
}

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



&lt;p&gt;So the craft-update script auto­mat­i­cal­ly does the fol­low­ing when our con­tain­er starts up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All &lt;a href="https://docs.craftcms.com/v3/extend/migrations.html"&gt;migra­tions&lt;/a&gt; are run&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.craftcms.com/v3/project-config.html"&gt;Project Con­fig&lt;/a&gt; is sync’d&lt;/li&gt;
&lt;li&gt;All caches are cleared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start­ing from a clean slate like this is so help­ful in terms of avoid­ing sil­ly prob­lems like things being cached, not up to date, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ser­vice: webpack
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://webpack.js.org/"&gt;web­pack&lt;/a&gt; is the build tool that we use for build­ing the CSS, JavaScript, and oth­er parts of our application.&lt;/p&gt;

&lt;p&gt;The set­up used here is entire­ly based on the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; arti­cle, just with some set­tings tweaked.&lt;/p&gt;

&lt;p&gt;That means our web­pack build process runs entire­ly inside of a Dock­er con­tain­er, but we still get all of the &lt;a href="https://webpack.js.org/concepts/hot-module-replacement/"&gt;Hot Mod­ule Replace­ment&lt;/a&gt; good­ness for local development.&lt;/p&gt;

&lt;p&gt;This ser­vice is com­posed of a base image that con­tains node itself, all of the Debian pack­ages need­ed for head­less Chrome, the &lt;a href="https://www.npmjs.com/"&gt;npm pack­ages&lt;/a&gt; we’ll always need to use, and then the project-spe­cif­ic image that con­tains what­ev­er addi­tion­al things are need­ed for our project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM node:11

# Install packages for headless chrome
# https://medium.com/@ssmak/how-to-fix-puppetteer-error-while-loading-shared-libraries-libx11-xcb-so-1-c1918b75acc3
RUN apt-get update \
    &amp;amp;&amp;amp; \
    # apt Debian packages
    apt-get install -y \
        ca-certificates \
        fonts-liberation \
        gconf-service \
        libgl1-mesa-glx \
        libasound2 \
        libatk1.0-0 \
        libc6 \
        libcairo2 \
        libcups2 \
        libdbus-1-3 \
        libexpat1 \
        libfontconfig1 \
        libgcc1 \
        libgconf-2-4 \
        libgdk-pixbuf2.0-0 \
        libglib2.0-0 \
        libgtk-3-0 \
        libnspr4 \
        libpango-1.0-0 \
        libpangocairo-1.0-0 \
        libstdc++6 \
        libx11-6 \
        libx11-xcb1 \
        libxcb1 \
        libxcomposite1 \
        libxcursor1 \
        libxdamage1 \
        libxext6 \
        libxfixes3 \
        libxi6 \
        libxrandr2 \
        libxrender1 \
        libxss1 \
        libxtst6 \
        libappindicator1 \
        libnss3 \
        lsb-release \
        wget \
        xdg-utils

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



&lt;p&gt;We’ve based the con­tain­er on the &lt;a href="https://hub.docker.com/_/node/"&gt;node image&lt;/a&gt;, tagged at ver­sion 11&lt;/p&gt;

&lt;p&gt;We’re then adding a bunch of &lt;a href="https://www.debian.org/"&gt;Debian&lt;/a&gt; pack­ages that we need in order to get &lt;a href="https://medium.com/@ssmak/how-to-fix-puppetteer-error-while-loading-shared-libraries-libx11-xcb-so-1-c1918b75acc3"&gt;head­less Chrome work­ing&lt;/a&gt; (need­ed for Crit­i­cal CSS gen­er­a­tion), as well as oth­er libraries for the &lt;a href="https://sharp.pixelplumbing.com/"&gt;Sharp image library&lt;/a&gt; to work effectively.&lt;/p&gt;

&lt;p&gt;By itself, this image won’t do much for us, and in fact we don’t even spin up this image. But we’ve built this image, and made it avail­able as &lt;a href="https://hub.docker.com/repository/docker/nystudio107/webpack-dev-base"&gt;nys­tu­dio107/web­pack-dev-base&lt;/a&gt; on DockerHub.&lt;/p&gt;

&lt;p&gt;Since it’s pre-built, we don’t have to build it every time, and can lay­er on top of this image any­thing project-spe­cif­ic via the webpack-dev-craft con­tain­er image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
FROM nystudio107/webpack-dev-base

WORKDIR /var/www/project/docker-config/webpack-dev-craft/

# We'd normally use `npm ci` here, but by using `install`:
# - If `package-lock.json` is present, it will install what is in the lock file
# - If `package-lock.json` is missing, it will update to the latest dependencies
# and create the `package-lock-json` file
# Run our webpack build in debug mode
CMD if [! -f "./package-lock.json"]; then \
        npm install; \
    fi \
    &amp;amp;&amp;amp; \
    if [! -d "./node_modules" -o ! "$(ls -A ./node_modules)"]; then \
        npm install; \
    fi \
    &amp;amp;&amp;amp; \
    npm run debug

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



&lt;p&gt;Then, just like with the php-dev-craft image, we do a bit of mag­ic to do a npm install, but only if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The package-lock.json file does­n’t exist&lt;/li&gt;
&lt;li&gt;The node_modules/ direc­to­ry does­n’t exist, or is empty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have to do the npm install as part of the Dock­er image CMD because the file sys­tem mounts aren’t in place until the CMD is run.&lt;/p&gt;

&lt;p&gt;This allows us to update our &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt; depen­den­cies just by delet­ing the package-lock.json file or the node_modules/ direc­to­ry, and doing docker-compose up&lt;/p&gt;

&lt;p&gt;The alter­na­tive is doing a docker exec -it craft_webpack_1 /bin/bash to open up a shell in our con­tain­er, and run­ning the com­mand manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  All Aboard!
&lt;/h2&gt;

&lt;p&gt;Hope­ful­ly this anno­tat­ed Dock­er con­fig has been use­ful to you. If you use Craft CMS, you can dive in and start using it your­self; if you use some­thing else entire­ly, the con­cepts here should still be very salient for your project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zlJQQb_4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-local-development.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zlJQQb_4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-local-development.jpg" alt="Docker containers local development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think that Dock­er — or some oth­er con­cep­tu­al­ly sim­i­lar con­tainer­iza­tion strat­e­gy — is going to be an impor­tant tech­nol­o­gy going for­ward. So it’s time to jump on board.&lt;/p&gt;

&lt;p&gt;As men­tioned ear­li­er, the Dock­er con­fig used here is used in both the &lt;a href="https://github.com/nystudio107/devmode"&gt;dev​Mode​.fm GitHub repo&lt;/a&gt;, and in the &lt;a href="https://github.com/nystudio107/craft"&gt;nystudio107/​craft&lt;/a&gt; boil­er­plate Com­pos­er project if you want to see some ​“in the wild” examples.&lt;/p&gt;

&lt;p&gt;Hap­py containerizing!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Post-Mortem: Outbreak Database</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Tue, 03 Mar 2020 05:00:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/post-mortem-outbreak-database-315i</link>
      <guid>https://dev.to/gaijinity/post-mortem-outbreak-database-315i</guid>
      <description>&lt;h1&gt;
  
  
  Post-Mortem: Outbreak Database
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Mod­ern­iz­ing an aging cus­tom PHP web­site with Craft CMS for con­tent man­age­ment, and a hybrid Twig/Vue.js + Vuex + Axios + GraphQL on the frontend
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aTLBup0B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/disease-outbreak-database.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aTLBup0B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/disease-outbreak-database.jpg" alt="Disease outbreak database"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Relat­ed talk: &lt;a href="https://speakerdeck.com/nystudio107/solving-problems-with-modern-tooling"&gt;Solv­ing Prob­lems with Mod­ern Tooling&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was con­tact­ed to do over­flow work for a free­lancer who found him­self in the envi­able posi­tion of hav­ing too much work booked.&lt;/p&gt;

&lt;p&gt;The project was some­thing that will be famil­iar to most web devel­op­ers, which was to take an old web­site &lt;a href="http://outbreakdatabase.com/"&gt;Out​break​Data​base​.com&lt;/a&gt; and mod­ern­ize it.&lt;/p&gt;

&lt;p&gt;This arti­cle describes the high­er-lev­el deci­sions made while work­ing on the project; if you want to get into the tech­ni­cal imple­men­ta­tion, check out the &lt;a href="https://dev.to/gaijinity/using-the-craft-cms-8220-headless-8221-with-the-graphql-api-54ah-temp-slug-130738"&gt;Using the Craft CMS ​“head­less” with the GraphQL API&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N.B.:&lt;/strong&gt; While my role on the project is fin­ished, the project may or may not be live at the time of this writing.&lt;/p&gt;


                                The cus­tom-built Cake &lt;span&gt;PHP&lt;/span&gt; web­site was start­ing to show its age, both visu­al­ly and technologically.
                            

&lt;p&gt;The client want­ed a web­site that was eas­i­er for con­tent authors to main­tain the hygiene of the data in the out­break data­base, and the site just need­ed an over­all refresh to car­ry it for­ward for the next 10 years.&lt;/p&gt;

&lt;p&gt;The web­site describes itself thusly:&lt;/p&gt;


                                Out­break Data­base is a resource that pro­vides access to food poi­son­ing out­break data in one easy to search place, dat­ing back to &lt;span&gt;1993&lt;/span&gt;.
                            

&lt;p&gt;They just did­n’t want the web­site to &lt;em&gt;look&lt;/em&gt; like it dat­ed back to 1993.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ufn2bHAZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x777_crop_center-center_100_line/original-outbreak-database-website.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ufn2bHAZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x777_crop_center-center_100_line/original-outbreak-database-website.png" alt="Original outbreak database website"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ini­tial Handoff
&lt;/h2&gt;

&lt;p&gt;The design for the web­site was already done, and the less inter­est­ing (to me any­way) work of data migra­tion to &lt;a href="https://craftcms.com/"&gt;Craft CMS&lt;/a&gt; was done already as well.&lt;/p&gt;

&lt;p&gt;Bonus for me.&lt;/p&gt;

&lt;p&gt;I was giv­en access to the exist­ing site, a CSS file that was being used to style this project and sev­er­al oth­er ​“mini-site” projects for the client, and some Twig tem­plates that showed the mocked out design.&lt;/p&gt;

&lt;p&gt;The clients goals were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the out­break data­base eas­i­er to main­tain for the con­tent authors&lt;/li&gt;
&lt;li&gt;Make the fron­tend eas­i­er to use by researchers and journalists&lt;/li&gt;
&lt;li&gt;Mod­ern­ize the web­site underpinnings&lt;/li&gt;
&lt;li&gt;Poten­tial­ly pro­vide an API to allow oth­er par­ties to access the data­base directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oth­er than that, I was giv­en pret­ty much free rein to do what­ev­er I thought was best. Which is a lev­el of trust I real­ly enjoy in my rela­tion­ship with the orig­i­nal free­lance developer.&lt;/p&gt;

&lt;p&gt;Luck­i­ly for me, using Craft CMS as a back­end ensures that the first two bul­let points are already tak­en care of by Craft CMS’s excel­lent con­tent mod­el­ing &amp;amp; author­ing capabilities.&lt;/p&gt;

&lt;p&gt;As I do for any project I work on, I spend a bit of time upfront learn­ing about the client, their goals, etc. The nor­mal stuff.&lt;/p&gt;

&lt;p&gt;Then I sit down to think about what tech­nolo­gies and tech­niques I could apply to help them reach their goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL as an API
&lt;/h2&gt;

&lt;p&gt;While the actu­al design of the web­site was not in my con­trol, the tech­no­log­i­cal under­pin­nings of the web­site and the user expe­ri­ence def­i­nite­ly was.&lt;/p&gt;

&lt;p&gt;I want­ed to use GraphQL over the Ele­ment API not just because it was less work, but because it pro­vid­ed a self-doc­u­ment­ed, strict­ly typed API for us auto­mat­i­cal­ly. GraphQL is a doc­u­ment­ed, wide­ly embraced stan­dard, so plen­ty of learn­ing mate­ri­als are available.&lt;/p&gt;

&lt;p&gt;Since the client had a stat­ed inten­tion of want­i­ng to be able to pro­vide oth­ers access to the data­base, I imme­di­ate­ly thought of &lt;a href="https://graphql.org/"&gt;GraphQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was a nice, clean, mod­ern way to present stan­dard­ized access to data, that allows researchers to query for just the data that they are look­ing for. Since Pix­el &amp;amp; Ton­ic had recent­ly released a first-par­ty GraphQL imple­men­ta­tion for &lt;a href="https://craftcms.com/blog/craft-33"&gt;Craft CMS 3.3&lt;/a&gt;, it seemed like a lock.&lt;/p&gt;

&lt;p&gt;There was a rub, however.&lt;/p&gt;

&lt;p&gt;At the time, the GraphQL imple­men­ta­tion did­n’t sup­port query­ing based on cus­tom fields, which we need­ed for the faceted search. So we were left with the prospect of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writ­ing a cus­tom &lt;a href="https://github.com/craftcms/element-api"&gt;Ele­ment API&lt;/a&gt; implementation&lt;/li&gt;
&lt;li&gt;Using Mark Huot’s &lt;a href="https://plugins.craftcms.com/craftql"&gt;CraftQL plu­g­in&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;???&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So like any respon­si­ble devel­op­er, I went with ???. Which in this case meant fil­ing some issues for the Craft CMS devel­op­ers to see if the con­cerns could be addressed.&lt;/p&gt;

&lt;p&gt;For­tu­nate­ly, we weren’t the only devel­op­ers want­i­ng this func­tion­al­i­ty, so Andris rolled his sleeves up and got it imple­ment­ed in Craft CMS 3.4.&lt;/p&gt;

&lt;p&gt;We were in business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adopt­ing Vue + Vuex + Axios
&lt;/h2&gt;

&lt;p&gt;Since we’d already decid­ed on GraphQL as an API, I thought the best way to ensure we were build­ing out an API oth­ers could access would be to con­sume that API ourselves.&lt;/p&gt;

&lt;p&gt;So instead of using Craft’s built-in &lt;a href="https://docs.craftcms.com/v3/dev/element-queries/"&gt;Ele­ment Queries&lt;/a&gt; for access­ing data via Twig, I adopt­ed &lt;a href="https://vuejs.org/"&gt;Vue.js&lt;/a&gt; and &lt;a href="https://github.com/axios/axios"&gt;Axios&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’d use Vue to help make writ­ing the inter­ac­tive UI eas­i­er to do, and Axios to send along our GraphQL queries to the Craft CMS backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vuex.vuejs.org/"&gt;Vuex&lt;/a&gt; is a glob­al data store that we’d lever­age to stash the data fetched via Axios, and make it avail­able to all of our Vue.js components.&lt;/p&gt;

&lt;p&gt;Here’s what the orig­i­nal web­site UX was like for searching:&lt;/p&gt;

&lt;p&gt;So pret­ty typ­i­cal for an old­er web­site design: a form where you blind­ly enter search cri­te­ria, click the Search but­ton, and a results page shows up.&lt;/p&gt;

&lt;p&gt;If you make a mis­take, or don’t find what you want, you hit the back but­ton, and try again.&lt;/p&gt;

&lt;p&gt;The new design and UX hand­ed off to me looked visu­al­ly nicer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JZVJGLql--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x689_crop_center-center_100_line/updated-outbreak-database-design.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JZVJGLql--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x689_crop_center-center_100_line/updated-outbreak-database-design.png" alt="Updated outbreak database design"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While this looks bet­ter, it oper­at­ed much the same: enter your search cri­te­ria, click a but­ton, go to a search results page. Hit the back but­ton to try again if you don’t get what you want.&lt;/p&gt;

&lt;p&gt;I thought we could do bet­ter, and Vue.js + Vuex + Axios + GraphQL would make doing that easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing Bet­ter
&lt;/h2&gt;

&lt;p&gt;A great part of my sat­is­fac­tion work­ing on ren­o­vat­ing old­er sites is the goal of mak­ing the world just a lit­tle bit bet­ter. We don’t always hit the mark dead-on, but striv­ing to improve things is what moti­vates me.&lt;/p&gt;

&lt;p&gt;So here’s what we end­ed up with:&lt;/p&gt;

&lt;p&gt;First I elim­i­nat­ed the ​“search results page”; instead, the search results would be dis­played inter­ac­tive­ly right below the query. As soon as you start typ­ing, it starts search­ing (&lt;a href="https://www.geeksforgeeks.org/debouncing-in-javascript/"&gt;debounced&lt;/a&gt; of course), and a lit­tle spin­ner shows you so (thanks, &lt;a href="https://www.npmjs.com/package/vue-simple-spinner"&gt;vue-sim­ple-spin­ner&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Click­ing on the &lt;strong&gt;Search&lt;/strong&gt; but­ton or hit­ting the Return/​Enter key would smooth­ly auto­scroll (thanks, &lt;a href="https://www.npmjs.com/package/vue2-smooth-scroll"&gt;vue2-smooth-scroll&lt;/a&gt;) to view the search results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Zu_QB_gb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_768x335_crop_center-center_100_line/graphql-debounced-search.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zu_QB_gb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_768x335_crop_center-center_100_line/graphql-debounced-search.png" alt="Graphql debounced search"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think the UI should be reworked a bit to make this a lit­tle less bulky so we can see more of the search results, but already I think we have a nice improvement.&lt;/p&gt;

&lt;p&gt;Peo­ple can inter­ac­tive­ly see the results of their search query, and make adjust­ments as need­ed with­out hop­ping back and forth between pages.&lt;/p&gt;

&lt;p&gt;But we did­n’t want to lose the abil­i­ty of being able to copy a search result from the address bar, and send it to col­leagues. So a lit­tle mag­ic was done to update the address bar with a prop­er search?keywords= URL.&lt;/p&gt;

&lt;p&gt;Next up was to elim­i­nate some of the ​“I don’t know what to search for” prob­lem. Instead of pro­vid­ing just an emp­ty box where you type what cri­te­ria you want, we’d pro­vide an auto-com­plete lookup of avail­able choic­es (thanks, &lt;a href="https://www.npmjs.com/package/@trevoreyre/autocomplete-vue"&gt;@trevoreyre/autocomplete-vue&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qsNbj0Jl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_768x414_crop_center-center_100_line/graphql-autocomplete-search.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qsNbj0Jl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_768x414_crop_center-center_100_line/graphql-autocomplete-search.png" alt="Graphql autocomplete search"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think this helps great­ly with the UX, because researchers can just start typ­ing, and they’ll see a list of pos­si­ble things they can choose from.&lt;/p&gt;

&lt;p&gt;This also adds some trans­paren­cy to the data­base hygiene, and allows the con­tent authors to eas­i­ly see dupli­cat­ed data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CSS Problem
&lt;/h2&gt;

&lt;p&gt;When­ev­er I start on a new project, I great­ly look for­ward to refac­tor­ing the site to use &lt;a href="https://tailwindcss.com/"&gt;Tail­wind CSS&lt;/a&gt;. If you’re not on-board the Tail­wind express yet, do give it a look, I’ve yet to know of any­one who has used it, and moved back to a more tra­di­tion­al BEM approach.&lt;/p&gt;

&lt;p&gt;I’d be will­ing to use some pro-bono hours to do the refac­tor­ing myself if it isn’t includ­ed in the project. But in this case, the CSS was being used on a num­ber of sites to give them all a sim­i­lar look.&lt;/p&gt;

&lt;p&gt;So even if I did the CSS refac­tor­ing to Tail­wind CSS on my own time, it would­n’t mesh well with their goals of hav­ing one CSS file for mul­ti­ple sites.&lt;/p&gt;

&lt;p&gt;So I decid­ed to roll their CSS in as legacy/styles.css and use my nor­mal Tail­wind CSS + &lt;a href="https://purgecss.com/"&gt;PurgeC­SS&lt;/a&gt; set­up to to over­ride styles or add new styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of
 * Normalize.css and some additional base styles.
 */
 @import 'tailwindcss/base';

/**
 * This injects any component classes registered by plugins.
 *
 */
@import 'tailwindcss/components';

/**
 * Here we add custom component classes; stuff we want loaded
 * *before* the utilities so that the utilities can still
 * override them.
 *
 */
@import './components/global.pcss';
@import './components/typography.pcss';
@import './components/webfonts.pcss';

/**
 * Legacy CSS used for the project, rather than rewriting it in Tailwind
 */
@import './legacy/styles.css';

/**
 * Include styles for individual pages
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 */
@import './vendor.pcss';

/**
 * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 */
@import 'tailwindcss/utilities';

/**
 * Forced overrides of the legacy CSS
 */
@import './components/overrides.pcss';

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



&lt;p&gt;This gives me the best of both worlds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I can use Tail­wind CSS’s util­i­ty class­es for addi­tion­al styling or to over­ride the base CSS as needed&lt;/li&gt;
&lt;li&gt;The exist­ing lega­cy styles.css is import­ed whole­sale, so they can update it as they see fit&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hybrid Web­site
&lt;/h2&gt;

&lt;p&gt;This web­site is what I’d term a ​“hybrid” web­site, in that it uses both Twig and Vue to ren­der content. &lt;/p&gt;

&lt;p&gt;It was done this way for prac­ti­cal rea­sons. The project was already using Twig to ren­der pages, and the bud­get was­n’t there to redo the tool­ing to use &lt;a href="https://jamstack.org/"&gt;JAM­stack&lt;/a&gt; with some­thing like &lt;a href="https://gridsome.org/"&gt;Grid­some&lt;/a&gt;. The ben­e­fits of doing so were also dubi­ous in this case.&lt;/p&gt;

&lt;p&gt;So instead we dropped Vue.js into the mix just for the dynam­ic com­po­nents on the page. For exam­ple, this is what the home­page looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% extends "_layouts/generic-page-layout.twig" %}

{% block headLinks %}
    {{ parent() }}
{% endblock headLinks %}

{% block content %}
    &amp;lt;div class="section--grey-pattern section--grey-pattern-solid section--mobile-gutter-none"
         style="min-height: 648px;"
    &amp;gt;
        &amp;lt;div id="component-container"&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;&amp;lt;!-- /.section-/-grey-pattern --&amp;gt;
{% endblock %}

{% block subcontent %}
{% endblock %}

{# -- Any JavaScript that should be included before &amp;lt;/body&amp;gt; -- #}
{% block bodyJs %}
    {{ parent() }}
    {{ craft.twigpack.includeJsModule("home.js", true) }}
{% endblock bodyJs %}

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



&lt;p&gt;This is using the Twig tem­plate set­up described in the &lt;a href="https://dev.to/gaijinity/an-effective-twig-base-templating-setup-3mf9-temp-slug-3249301"&gt;An Effec­tive Twig Base Tem­plat­ing Set­up&lt;/a&gt; arti­cle, and the &amp;lt;div id="component-container"&amp;gt; is where the Vue instance mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Home page
import { OutbreakMixins } from '../mixins/outbreak.js';
import { createStore } from '../store/store.js';
import '@trevoreyre/autocomplete-vue/dist/style.css';

// App main
const main = async() =&amp;gt; {
    // Async load the vue module
    const [Vue, VueSmoothScroll] = await Promise.all([
        import(/* webpackChunkName: "vue" */ 'vue'),
        import(/* webpackChunkName: "vue" */ 'vue2-smooth-scroll'),
    ]);
    const store = await createStore(Vue.default);
    Vue.default.use(VueSmoothScroll.default);
    // Create our vue instance
    const vm = new Vue.default({
        render: (h) =&amp;gt; {
            return h('search-form');
        },
        mixins: [OutbreakMixins],
        store,
        components: {
            'search-form': () =&amp;gt; import(/* webpackChunkName: "searchform" */ '../../vue/SearchForm.vue'),
        },
    });

    return vm;
};

// Execute async function
main().then((vm) =&amp;gt; {
});

// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
    module.hot.accept();
}

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



&lt;p&gt;This means that our Vue com­po­nents are not ren­dered until Vue &amp;amp; our com­po­nents are loaded, exe­cut­ed, and mount­ed. How­ev­er the result­ing web­site still per­forms nicely:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ipV15f3i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x335_crop_center-center_100_line/outbreak-database-page-speed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ipV15f3i--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x335_crop_center-center_100_line/outbreak-database-page-speed.png" alt="Outbreak database page speed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So it was done this way in a nod to prac­ti­cal­i­ty, but should the client wish to jump to a full JAM­stack set­up in the future, we’re more than halfway home already.&lt;/p&gt;

&lt;p&gt;This tech­nique was described in the &lt;a href="https://nystudio107.com/blog/using-vuejs-2-0-with-craft-cms"&gt;Using Vue­JS 2.0 with Craft CMS&lt;/a&gt; and &lt;a href="https://dev.to/gaijinity/using-vuejs-graphql-to-make-practical-magic-1o2o"&gt;Using Vue­JS + GraphQL to make Prac­ti­cal Mag­ic&lt;/a&gt; arti­cles if you want to learn more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;No project is ever per­fect, espe­cial­ly soft­ware devel­op­ment projects. But I feel like the high­er lev­el deci­sions made helped to improve this project overall.&lt;/p&gt;

&lt;p&gt;It’s a good exam­ple of how pick­ing the right bits of tech­nol­o­gy can enable you to cre­ate an improved end result.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>graphql</category>
      <category>postmortem</category>
    </item>
    <item>
      <title>Flat Multi-Environment Config for Craft CMS 3</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Sat, 29 Feb 2020 15:29:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/flat-multi-environment-config-for-craft-cms-3-521n</link>
      <guid>https://dev.to/gaijinity/flat-multi-environment-config-for-craft-cms-3-521n</guid>
      <description>&lt;h1&gt;
  
  
  Flat Multi-Environment Config for Craft CMS 3
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Mul­ti-envi­ron­ment con­figs for Craft CMS are a mix of alias­es, envi­ron­ment vari­ables, and con­fig files. This arti­cle sorts it all out, and presents a flat con­fig file approach
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7qv8CzS1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/multiple-environment-configuration.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7qv8CzS1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/multiple-environment-configuration.jpg" alt="Multiple environment configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mul­ti-envi­ron­ment con­fig­u­ra­tion is a way to have your web­site or webapp do dif­fer­ent things depend­ing on where it is being served from. For instance, a typ­i­cal set­up might have the fol­low­ing environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
dev — your local devel­op­ment environment&lt;/li&gt;
&lt;li&gt;
staging — a stag­ing or User Accep­tance Test­ing (UAT) serv­er allow­ing stake­hold­ers to test&lt;/li&gt;
&lt;li&gt;
production — the live pro­duc­tion server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In each envi­ron­ment, you might want your project work­ing dif­fer­ent­ly. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debug­ging&lt;/strong&gt;  — in local dev you might want debug­ging tools enabled, but not in live production
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cre­den­tials&lt;/strong&gt;  — things like data­base cre­den­tials, API keys, etc. may be dif­fer­ent per environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track­ing&lt;/strong&gt;  — you prob­a­bly don’t want Google Ana­lyt­ics data in local dev, but you prob­a­bly do in live production
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are many oth­er behav­iors of set­tings that you might need or want to be dif­fer­ent depend­ing on where your project is being served from.&lt;/p&gt;

&lt;p&gt;Addi­tion­al­ly, you may have ​“secrets” that you &lt;a href="https://www.freecodecamp.org/news/how-to-securely-store-api-keys-4ff3ea19ebda/"&gt;don’t want stored in ver­sion con­trol&lt;/a&gt;, and you also don’t want stored in your database.&lt;/p&gt;

&lt;p&gt;Mul­ti-envi­ron­ment con­fig­u­ra­tion is for all of these things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter the .ENV file
&lt;/h2&gt;

&lt;p&gt;Craft CMS and a num­ber of oth­er sys­tems have adopt­ed the con­cept of a .env file which for stor­ing envi­ron­ment vari­ables and secrets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eclmO3ai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/storing-secrets-env-file.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eclmO3ai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/storing-secrets-env-file.jpg" alt="Storing secrets env file"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This .env file is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nev­er checked into source code con­trol such as Git&lt;/li&gt;
&lt;li&gt;Cre­at­ed man­u­al­ly in each envi­ron­ment where the project will run&lt;/li&gt;
&lt;li&gt;Stores both envi­ron­ment vari­ables and ​“secrets”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a sim­ple key/​value that looks some­thing like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=localhost
DB_USER=project
DB_PASSWORD=XXXX
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

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



&lt;p&gt;The val­ues can be quot­ed or not (and indeed need to be quot­ed if they con­tain spaces), but keep in mind that if you used Dock­er, it &lt;a href="https://github.com/docker/compose/issues/3702"&gt;does­n’t allow for quot­ed val­ues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can also add com­ments to your .env files by pro­ceed­ing a line with a # character.&lt;/p&gt;


                                Adding com­ments to your .env file is being nice to future-you
                            

&lt;p&gt;While there is some &lt;a href="https://blog.fortrabbit.com/how-to-keep-a-secret"&gt;debate over the effi­ca­cy&lt;/a&gt; of stor­ing secrets in this way, it’s become a com­mon­ly accept­ed prac­tice that is ​“good enough” for non-crit­i­cal purposes.&lt;/p&gt;

&lt;p&gt;Addi­tion­al­ly, this sep­a­ra­tion of envi­ron­ment vari­ables &amp;amp; secrets from code — and from the data­base — allows for the nat­ur­al use of more sophis­ti­cat­ed mea­sures should they be needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.heroku.com/"&gt;Heroku&lt;/a&gt;, &lt;a href="https://www.docker.com/"&gt;Dock­er&lt;/a&gt;, &lt;a href="https://buddy.works/"&gt;Buddy.works&lt;/a&gt;, &lt;a href="https://forge.laravel.com/"&gt;Forge&lt;/a&gt;, and many oth­er tools work direct­ly with .env files. &lt;/p&gt;

&lt;p&gt;Envi­ron­ment vari­ables can also be inject­ed direct­ly into the envi­ron­ment via the web­serv­er and oth­er tools, check out &lt;a href="https://github.com/nystudio107/dotenvy"&gt;Doten­vy&lt;/a&gt; for details on automat­ing that.&lt;/p&gt;

&lt;p&gt;It’s a good prac­tice to pro­vide an example.env file with each of your projects that con­tain­ers the boil­er­plate for the envi­ron­ment vari­ables your project uses, as well as default values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=localhost
DB_USER=project
DB_PASSWORD=REPLACE_ME
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

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



&lt;p&gt;The example.env file can and &lt;em&gt;should&lt;/em&gt; be checked into Git, just make sure it has noth­ing sen­si­tive in it such as passwords.&lt;/p&gt;

&lt;p&gt;This gives you a nice start­ing point that you can rename to .env when con­fig­ur­ing the project for a new envi­ron­ment. I use the &lt;a href="https://dev.to/fission/screaming-snake-case-43kj"&gt;scream­ing snake case&lt;/a&gt; con­stant REPLACE_ME to indi­cate non-default val­ues that need to be filled in on a per-envi­ron­ment basis.&lt;/p&gt;

&lt;p&gt;You’ll thank your­self the next time you go to set up the project, and so will oth­ers on your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Envi­ron­ment Vari­ables in Craft CMS
&lt;/h2&gt;

&lt;p&gt;In the con­text of Craft CMS, Pix­el &amp;amp; Ton­ic has the canon­i­cal con­fig­u­ra­tion infor­ma­tion in their &lt;a href="https://docs.craftcms.com/v3/config/environments.html"&gt;Envi­ron­men­tal Con­fig­u­ra­tion&lt;/a&gt; guide. How­ev­er, we’re going to go into it in-depth, and pro­vide a flex­i­ble ref­er­ence implementation.&lt;/p&gt;

&lt;p&gt;Craft CMS uses the &lt;a href="https://github.com/vlucas/phpdotenv"&gt;vlucas/​phpdotenv&lt;/a&gt; library for .env file han­dling. In fact, in the web/index.php we can see it being used thusly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Load dotenv?
if (class_exists('Dotenv\Dotenv') &amp;amp;&amp;amp; file_exists(CRAFT_BASE_PATH.'/.env')) {
    Dotenv\Dotenv::create(CRAFT_BASE_PATH)-&amp;gt;load();
}

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



&lt;p&gt;If the Dotenv class exists, will look for a .env file in the project direc­to­ry (set by the con­stant CRAFT_BASE_PATH) and try to load it.&lt;/p&gt;

&lt;p&gt;What this actu­al­ly does is it calls the PHP func­tion &lt;a href="https://www.php.net/manual/en/function.putenv.php"&gt;putenv()&lt;/a&gt; for each key/​value pair in your .env file, which sets those vari­ables in PHP’s $_ENV superglobal.&lt;/p&gt;

&lt;p&gt;The $_ENV super­glob­al con­tains vari­ables from the PHP run­time envi­ron­ment, and the $_SERVER super­glob­al con­tains vari­ables from the serv­er envi­ron­ment. The PHP func­tion &lt;a href="https://www.php.net/manual/en/function.getenv.php"&gt;getenv()&lt;/a&gt; reads vari­ables from both of them of these super­glob­als, and is how you can access your .env envi­ron­ment variables.&lt;/p&gt;


                                &lt;span&gt;“&lt;/span&gt;Super­glob­al” just means it’s a glob­al vari­able defined by &lt;span&gt;PHP&lt;/span&gt;, and avail­able in every script. It isn’t faster than a speed­ing bul­let or anything.
                            

&lt;p&gt;So if our .env file looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=localhost
DB_USER=project
DB_PASSWORD=XXXX
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

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



&lt;p&gt;Here’s what the the auto-com­plete drop­down looks like in the Craft CMS CP for the envi­ron­ment variables:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--f5cswQjz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x516_crop_center-center_100_line/craft-cms-environment-variables-autocomplete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--f5cswQjz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x516_crop_center-center_100_line/craft-cms-environment-variables-autocomplete.png" alt="Craft cms environment variables autocomplete"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We could get a val­ue from PHP like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
$database = getenv('DB_DATABASE');

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



&lt;p&gt;And we could get the same val­ue from Twig like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% set database = getenv('DB_DATABASE') %}

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



&lt;h2&gt;
  
  
  Alias­es in Craft CMS
&lt;/h2&gt;

&lt;p&gt;Craft CMS also has the con­cept of alias­es, which are actu­al­ly inher­it­ed from &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/concept-aliases"&gt;Yii2 alias­es&lt;/a&gt;.&lt;/p&gt;


                                Yii&lt;span&gt;2&lt;/span&gt; is the webapp frame­work that Craft &lt;span&gt;CMS&lt;/span&gt; is built on
                            

&lt;p&gt;Alias­es can some­times be con­fused with envi­ron­ment vari­ables, but they real­ly serve a dif­fer­ent pur­pose. You’ll use an alias when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The set­ting in ques­tion is a path&lt;/li&gt;
&lt;li&gt;The set­ting in ques­tion is a URL&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Could you use envi­ron­ment vari­ables in these cas­es? Sure. But with alias­es you can do things like have it resolve a path or URL that has a par­tial path in it (see below).&lt;/p&gt;

&lt;p&gt;You define alias­es in your config/general.php file in the aliases key, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/GeneralConfig.php.
 *
 * @see craft\config\GeneralConfig
 */

return [
    // Craft config settings from .env variables
    'aliases' =&amp;gt; [
        '@cloudfrontUrl' =&amp;gt; getenv('CLOUDFRONT_URL'),
        '@web' =&amp;gt; getenv('SITE_URL'),
        '@webroot' =&amp;gt; getenv('WEB_ROOT_PATH'),
    ],
];

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



&lt;p&gt;Note that we’re actu­al­ly set­ting alias­es from envi­ron­ment vari­ables! They actu­al­ly com­pli­ment each other.&lt;/p&gt;

&lt;p&gt;Both @web and @webroot are alias­es that Yii2 tries to set auto­mat­i­cal­ly for you. How­ev­er, you should always set them explic­it­ly (as shown above) to avoid &lt;a href="https://docs.craftcms.com/v3/sites.html#creating-a-site"&gt;poten­tial cache poi­son­ing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s how we can resolve an alias in PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
$path = Craft::getAlias('@webroot/assets');

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



&lt;p&gt;To resolve an alias from Twig:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% set path = alias('@webreoot/assets') %}

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



&lt;p&gt;This demon­strates what you can do with alias­es that you can­not do with envi­ron­ment vari­ables, which is pass in a par­tial path and have the alias resolve with that path added to it.&lt;/p&gt;

&lt;p&gt;You &lt;strong&gt;can­not&lt;/strong&gt; do this with envi­ron­ment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% set path = getenv('WEB_ROOT_PATH/assets') %}

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



&lt;p&gt;Sim­i­lar­ly, you &lt;strong&gt;can­not&lt;/strong&gt; put this in a CP set­ting in Craft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
$WEB_ROOT_PATH/assets

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



&lt;p&gt;Here’s what the the auto-com­plete drop­down looks like in the Craft CMS CP for aliases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L5SxSiMs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x516_crop_center-center_100_line/craft-cms-aliases-autocomplete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L5SxSiMs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x516_crop_center-center_100_line/craft-cms-aliases-autocomplete.png" alt="Craft cms aliases autocomplete"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  parseEnv() does both
&lt;/h2&gt;

&lt;p&gt;Since it’s com­mon­place that set­tings could be either alias­es or envi­ron­ment vari­ables (espe­cial­ly in CP set­tings), Craft CMS 3.1.0 intro­duced the con­ve­nience func­tion &lt;a href="https://docs.craftcms.com/v3/dev/functions.html#parseenv"&gt;parseEnv()&lt;/a&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch­es any envi­ron­ment vari­ables in the passed string&lt;/li&gt;
&lt;li&gt;Resolves any alias­es in the passed string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you can hap­pi­ly use it as a uni­ver­sal way to resolve both alias­es and envi­ron­ment variables.&lt;/p&gt;

&lt;p&gt;Here’s what it looks like in Twig:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% set path = parseEnv(someVariable) %}
{# This is equivalent to #}
{% set path = alias(getenv(someVariable)) %}

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



&lt;p&gt;Here’s what it looks like using parseEnv() via PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
$path = Craft::parseEnv($someVariable);
// This is equivalent to:
$path = Craft::getAlias(getenv($someVariable));

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



&lt;p&gt;The parseEnv() func­tion is a nice short­cut when you’re deal­ing with CP set­tings that could be alias­es, envi­ron­ment vari­ables, or both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Con­fig files in Craft CMS
&lt;/h2&gt;

&lt;p&gt;Craft CMS also has the con­cept of &lt;a href="https://docs.craftcms.com/v3/config/environments.html#config-files"&gt;con­fig files&lt;/a&gt;, stored in the config/​direc­to­ry. These can either be ​“flat” con­fig files that always return the same val­ues regard­less of environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// -- config/general.php --
return [
    'omitScriptNameInUrls' =&amp;gt; true,
    'devMode' =&amp;gt; true,
    'cpTrigger' =&amp;gt; 'secret-word',
];

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



&lt;p&gt;Or con­fig files can be multi-environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// -- config/general.php --
return [
    // Global settings
    '*' =&amp;gt; [
        'omitScriptNameInUrls' =&amp;gt; true,
    ],

    // Dev environment settings
    'dev' =&amp;gt; [
        'devMode' =&amp;gt; true,
    ],

    // Production environment settings
    'production' =&amp;gt; [
        'cpTrigger' =&amp;gt; 'secret-word',
    ],
];

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



&lt;p&gt;The &lt;em&gt;&lt;/em&gt; key is **required* for a con­fig file to be parsed as a mul­ti-envi­ron­ment con­fig file. If the * key is present, any set­tings in that sub-array are con­sid­ered glob­al settings.&lt;/p&gt;

&lt;p&gt;Oth­er keys in the array cor­re­spond with the CRAFT_ENVIRONMENT con­stant, which is set by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ENVIRONMENT vari­able in your .env, if present&lt;/li&gt;
&lt;li&gt;The incom­ing URL’s host­name otherwise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mul­ti-envi­ron­ment con­fig files are a car­ry-over from Craft 2, and con­tin­ue to be quite useful.&lt;/p&gt;


                                Flat is beautiful
                            

&lt;p&gt;How­ev­er, we’ve moved towards flat con­fig files com­bined with .env files. Let’s have a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real-world example
&lt;/h2&gt;

&lt;p&gt;For a real-world exam­ple of using flat con­fig files com­bined with envi­ron­ment vari­ables and alias­es, we’ll use the &lt;a href="https://github.com/nystudio107/devmode"&gt;OSS’d dev​Mode​.fm web­site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ONQU1U2q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/flat-is-beautiful.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ONQU1U2q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/flat-is-beautiful.jpg" alt="Flat is beautiful"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rea­son we’ve moved away from using mul­ti-envi­ron­ment con­fig files is sim­plic­i­ty. It takes less men­tal space to know that &lt;strong&gt;any&lt;/strong&gt; envi­ron­ment-spe­cif­ic set­tings or secrets are always com­ing from one place: the .env file.&lt;/p&gt;


                                Using flat con­fig files with envi­ron­ment vari­ables keeps all the per-envi­ron­ment set­tings in one place
                            

&lt;p&gt;This will save you time hav­ing to try to track down where a par­tic­u­lar con­fig set­ting is stored in each envi­ron­ment. It’s all in one place.&lt;/p&gt;

&lt;p&gt;Here’s what the example.env file looks like for dev​Mode​.fm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Craft general settings
ALLOW_UPDATES=1
ALLOW_ADMIN_CHANGES=1
BACKUP_ON_UPDATE=0
DEV_MODE=1
ENABLE_TEMPLATE_CACHING=0
ENVIRONMENT=local
IS_SYSTEM_LIVE=1
RUN_QUEUE_AUTOMATICALLY=1
SECURITY_KEY=FnKtqveecwgMavLwQnX2I-dqYjpwZMR6

# Craft database settings
DB_DRIVER=pgsql
DB_SERVER=postgres
DB_USER=project
DB_PASSWORD=REPLACE_ME
DB_DATABASE=project
DB_SCHEMA=public
DB_TABLE_PREFIX=
DB_PORT=5432

# URL &amp;amp; path settings
ASSETS_URL=http://localhost:8000/
SITE_URL=http://localhost:8000/
WEB_ROOT_PATH=/var/www/project/cms/web

# Craft &amp;amp; Plugin Licenses
LICENSE_KEY=
PLUGIN_IMAGEOPTIMIZE_LICENSE=
PLUGIN_RETOUR_LICENSE=
PLUGIN_SEOMATIC_LICENSE=
PLUGIN_TRANSCODER_LICENSE=
PLUGIN_WEBPERF_LICENSE=

# S3 settings
S3_KEY_ID=REPLACE_ME
S3_SECRET=REPLACE_ME
S3_BUCKET=devmode-bucket
S3_REGION=us-east-2
S3_SUBFOLDER=

# CloudFront settings
CLOUDFRONT_URL=https://dnzwsrj1eic0g.cloudfront.net
CLOUDFRONT_DISTRIBUTION_ID=E17SKV1U1OTZKW
CLOUDFRONT_PATH_PREFIX=

# Redis settings
REDIS_HOSTNAME=redis
REDIS_PORT=6379
REDIS_DEFAULT_DB=0
REDIS_CRAFT_DB=3

# webpack settings
PUBLIC_PATH=/dist/
DEVSERVER_PUBLIC=http://localhost:8080
DEVSERVER_HOST=0.0.0.0
DEVSERVER_POLL=0
DEVSERVER_PORT=8080
DEVSERVER_HTTPS=0

# Twigpack settings
TWIGPACK_DEV_SERVER_MANIFEST_PATH=http://webpack:8080/
TWIGPACK_DEV_SERVER_PUBLIC_PATH=http://webpack:8080/

# Disqus settings
DISQUS_PUBLIC_KEY=
DISQUS_SECRET_KEY=

# Google Analytics settings
GA_TRACKING_ID=UA-69117511-5

# FastCGI Cache Bust settings
FAST_CGI_CACHE_PATH=

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



&lt;p&gt;Because we’re using &lt;a href="https://docs.craftcms.com/v3/project-config.html"&gt;Project Con­fig&lt;/a&gt; to allow us to eas­i­ly deploy site changes across envi­ron­ments, we have to be mind­ful to put things like our Craft license key, plu­g­in license keys, and oth­er secrets into our .env file&lt;/p&gt;

&lt;p&gt;Oth­er­wise we’d end up with secrets checked into our git repo, which is not ide­al from a secu­ri­ty point of view.&lt;/p&gt;


                                While this .env file may look long, remem­ber that it’s con­sol­i­dat­ing all of the envi­ron­ment vari­ables in one place
                            

&lt;p&gt;Note also that the .env set­tings are log­i­cal­ly grouped, with comments.&lt;/p&gt;

&lt;p&gt;Let’s have a look at how we uti­lize these envi­ron­ment vari­ables in our config/general.php file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/GeneralConfig.php.
 *
 * @see craft\config\GeneralConfig
 */

return [
    // Craft config settings from .env variables
    'aliases' =&amp;gt; [
        '@assetsUrl' =&amp;gt; getenv('ASSETS_URL'),
        '@cloudfrontUrl' =&amp;gt; getenv('CLOUDFRONT_URL'),
        '@web' =&amp;gt; getenv('SITE_URL'),
        '@webroot' =&amp;gt; getenv('WEB_ROOT_PATH'),
    ],
    'allowUpdates' =&amp;gt; (bool)getenv('ALLOW_UPDATES'),
    'allowAdminChanges' =&amp;gt; (bool)getenv('ALLOW_ADMIN_CHANGES'),
    'backupOnUpdate' =&amp;gt; (bool)getenv('BACKUP_ON_UPDATE'),
    'devMode' =&amp;gt; (bool)getenv('DEV_MODE'),
    'enableTemplateCaching' =&amp;gt; (bool)getenv('ENABLE_TEMPLATE_CACHING'),
    'isSystemLive' =&amp;gt; (bool)getenv('IS_SYSTEM_LIVE'),
    'resourceBasePath' =&amp;gt; getenv('WEB_ROOT_PATH').'/cpresources',
    'runQueueAutomatically' =&amp;gt; (bool)getenv('RUN_QUEUE_AUTOMATICALLY'),
    'securityKey' =&amp;gt; getenv('SECURITY_KEY'),
    'siteUrl' =&amp;gt; getenv('SITE_URL'),
    // Craft config settings from constants
    'cacheDuration' =&amp;gt; false,
    'defaultSearchTermOptions' =&amp;gt; [
        'subLeft' =&amp;gt; true,
        'subRight' =&amp;gt; true,
    ],
    'defaultTokenDuration' =&amp;gt; 'P2W',
    'enableCsrfProtection' =&amp;gt; true,
    'errorTemplatePrefix' =&amp;gt; 'errors/',
    'generateTransformsBeforePageLoad' =&amp;gt; true,
    'maxCachedCloudImageSize' =&amp;gt; 3000,
    'maxUploadFileSize' =&amp;gt; '100M',
    'omitScriptNameInUrls' =&amp;gt; true,
    'useEmailAsUsername' =&amp;gt; true,
    'usePathInfo' =&amp;gt; true,
    'useProjectConfigFile' =&amp;gt; true,
];

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



&lt;p&gt;&lt;strong&gt;// Craft con­fig set­tings from .env variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The set­tings under this com­ment, includ­ing the aliases, are all set from .env envi­ron­ment vari­ables via getenv().&lt;/p&gt;

&lt;p&gt;Note that we’re explic­it­ly type­cast­ing the boolean val­ues with (bool) because they are set with either 0 (false) or 1 (true) in the .env file, because true and false are both strings. Nor­mal­ly this isn’t a prob­lem, but there can be edge cas­es with weak­ly typed lan­guages like PHP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;// Craft con­fig set­tings from constants&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The set­tings under this com­ment are set­tings that we typ­i­cal­ly want to adjust from their default, but we don’t need them to be dif­fer­ent on a per-envi­ron­ment basis.&lt;/p&gt;

&lt;p&gt;You can look up what the var­i­ous con­fig set­tings are on the Craft CMS &lt;a href="https://docs.craftcms.com/v3/config/config-settings.html"&gt;Gen­er­al Con­fig Set­tings&lt;/a&gt; page.&lt;/p&gt;

&lt;p&gt;Let’s have a look at the config/db.php file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Database Configuration
 *
 * All of your system's database connection settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/DbConfig.php.
 *
 * @see craft\config\DbConfig
 */

return [
    'driver' =&amp;gt; getenv('DB_DRIVER'),
    'server' =&amp;gt; getenv('DB_SERVER'),
    'user' =&amp;gt; getenv('DB_USER'),
    'password' =&amp;gt; getenv('DB_PASSWORD'),
    'database' =&amp;gt; getenv('DB_DATABASE'),
    'schema' =&amp;gt; getenv('DB_SCHEMA'),
    'tablePrefix' =&amp;gt; getenv('DB_TABLE_PREFIX'),
    'port' =&amp;gt; getenv('DB_PORT')
];

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



&lt;p&gt;These set­tings are all pret­ty straight­for­ward, we’re just read­ing in secrets or set­tings that may be dif­fer­ent per envi­ron­ment from .env envi­ron­ment vari­ables via getenv().&lt;/p&gt;

&lt;p&gt;Final­ly, let’s have a look at the config/app.php file that lets you con­fig­ure just about any aspect of the &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/structure-applications"&gt;Craft CMS webapp&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Yii Application Config
 *
 * Edit this file at your own risk!
 *
 * The array returned by this file will get merged with
 * vendor/craftcms/cms/src/config/app/main.php and [web|console].php, when
 * Craft's bootstrap script is defining the configuration for the entire
 * application.
 *
 * You can define custom modules and system components, and even override the
 * built-in system components.
 */

return [
    'modules' =&amp;gt; [
        'site-module' =&amp;gt; [
            'class' =&amp;gt; \modules\sitemodule\SiteModule::class,
        ],
    ],
    'bootstrap' =&amp;gt; ['site-module'],
    'components' =&amp;gt; [
        'deprecator' =&amp;gt; [
            'throwExceptions' =&amp;gt; YII_DEBUG,
        ],
        'redis' =&amp;gt; [
            'class' =&amp;gt; yii\redis\Connection::class,
            'hostname' =&amp;gt; getenv('REDIS_HOSTNAME'),
            'port' =&amp;gt; getenv('REDIS_PORT'),
            'database' =&amp;gt; getenv('REDIS_DEFAULT_DB'),
        ],
        'cache' =&amp;gt; [
            'class' =&amp;gt; yii\redis\Cache::class,
            'redis' =&amp;gt; [
                'hostname' =&amp;gt; getenv('REDIS_HOSTNAME'),
                'port' =&amp;gt; getenv('REDIS_PORT'),
                'database' =&amp;gt; getenv('REDIS_CRAFT_DB'),
            ],
        ],
        'session' =&amp;gt; [
            'class' =&amp;gt; \yii\redis\Session::class,
            'redis' =&amp;gt; [
                'hostname' =&amp;gt; getenv('REDIS_HOSTNAME'),
                'port' =&amp;gt; getenv('REDIS_PORT'),
                'database' =&amp;gt; getenv('REDIS_CRAFT_DB'),
            ],
            'as session' =&amp;gt; [
                'class' =&amp;gt; \craft\behaviors\SessionBehavior::class,
            ],
        ],
    ],
];

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



&lt;p&gt;Here we’re boot­strap­ping our Site Mod­ule as per the &lt;a href="https://dev.to/gaijinity/enhancing-a-craft-cms-3-website-with-a-custom-module-7k7"&gt;Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Then we’re con­fig­ur­ing the deprecator com­po­nent so that if devMode is enabled, dep­re­ca­tion errors that would nor­mal­ly be logged instead cause an excep­tion to be thrown.&lt;/p&gt;


                                This is play­ing Craft in &lt;span&gt;&lt;/span&gt;​&lt;span&gt;“&lt;/span&gt;hard” mode
                            

&lt;p&gt;This can be real­ly use­ful for track­ing down and fix­ing dep­re­ca­tion errors as they happen.&lt;/p&gt;

&lt;p&gt;Final­ly, we con­fig­ure &lt;a href="https://redis.io/"&gt;Redis&lt;/a&gt;, and use it as the Yii2 caching method, and more impor­tant­ly for PHP ses­sions. You can read more about set­ting up Redis in Matt Gray’s excel­lent &lt;a href="https://servd.host/blog/adding-redis-to-craft-cms"&gt;Adding Redis to Craft CMS&lt;/a&gt; article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mul­ti-site Mul­ti-Envi­ron­ment in Craft CMS
&lt;/h2&gt;

&lt;p&gt;Craft CMS has pow­er­ful &lt;a href="https://docs.craftcms.com/v3/sites.html"&gt;mul­ti-site&lt;/a&gt; baked in that allows you to cre­ate local­iza­tions of exist­ing sites, or sis­ter-sites all man­aged under one umbrella.&lt;/p&gt;

&lt;p&gt;In the con­text of a mul­ti-envi­ron­ment con­fig the siteUrl in your config/general.php changes from a string to an array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    'siteUrl' =&amp;gt; [
        'en' =&amp;gt; getenv('EN_SITE_URL'),
        'fr' =&amp;gt; getenv('FR_SITE_URL'),
    ],

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



&lt;p&gt;The key in the array is the lan­guage han­dle, and the val­ue is the siteUrl for that site.&lt;/p&gt;

&lt;p&gt;And your .env would have the cor­re­spond­ing URLs in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Site URLs
EN_SITE_URL=https://english-example.com/
FR_SITE_URL=https://french-example.com/

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



&lt;p&gt;You can have a sep­a­rate .env envi­ron­ment vari­able for each site as shown above, or if your sites will all have the same base URL, you can define an alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    'aliases' =&amp;gt; [
        '@baseSiteUrl' =&amp;gt; getenv('SITE_URL'),
    ],

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



&lt;p&gt;And then your siteUrl array would just look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    'siteUrl' =&amp;gt; [
        'en' =&amp;gt; '@baseSiteUrl/en',
        'fr' =&amp;gt; '@baseSiteUrl/fr',
    ],

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



&lt;p&gt;This makes it a lit­tle clean­er to set up and main­tain, and it’s few­er envi­ron­ment vari­ables you need to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wind­ing Down
&lt;/h2&gt;

&lt;p&gt;That about wraps it up our spelunk­ing into the world of mul­ti-envi­ron­ment con­figs in Craft CMS 3.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lti4_4Px--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x558_crop_center-center_82_line/winding-down.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lti4_4Px--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x558_crop_center-center_82_line/winding-down.jpg" alt="Winding down"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hope­ful­ly this in-depth explo­ration of how envi­ron­ment vari­ables work com­bined with real-world exam­ples have helped to give you a bet­ter under­stand­ing of how you can cre­ate a sol­id mul­ti-envi­ron­ment con­fig­u­ra­tion for Craft CMS 3.&lt;/p&gt;

&lt;p&gt;If you adopt some of the method­olo­gies dis­cussed here, you will reap the ben­e­fits of a proven setup.&lt;/p&gt;

&lt;p&gt;The approach pre­sent­ed here is also used in the &lt;a href="https://github.com/nystudio107/craft"&gt;nystudio107 Craft 3 CMS scaf­fold­ing project&lt;/a&gt;. Enjoy!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>craftcms</category>
      <category>config</category>
      <category>env</category>
    </item>
    <item>
      <title>Setting Up AWS S3 Buckets + CloudFront CDN for your Assets</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Thu, 23 Jan 2020 23:01:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/setting-up-aws-s3-buckets-cloudfront-cdn-for-your-assets-4h24</link>
      <guid>https://dev.to/gaijinity/setting-up-aws-s3-buckets-cloudfront-cdn-for-your-assets-4h24</guid>
      <description>&lt;h1&gt;
  
  
  Setting Up AWS S3 Buckets + CloudFront CDN for your Assets
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Using a cloud stor­age sys­tem like AWS S3 with a CDN dis­tri­b­u­tion can be a con­ve­nient and inex­pen­sive way to store your assets. Here’s how to set it up right.
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xz7EsnY5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/aws-s3-bucket-cloudfront-craft-cms.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xz7EsnY5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/aws-s3-bucket-cloudfront-craft-cms.jpg" alt="Aws s3 buckets cloudfront craft cms"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Assets like images, PDFs, and oth­er files are often an impor­tant part of the ​“con­tent” that a Con­tent Man­age­ment Sys­tem handles.&lt;/p&gt;

&lt;p&gt;Although this arti­cle was writ­ten with Craft CMS in mind, the vast major­i­ty of the arti­cle applies gener­i­cal­ly to any CMS or website.&lt;/p&gt;

&lt;p&gt;Craft CMS has some fan­tas­tic native han­dling of said assets, which by default are stored in fold­ers on your server.&lt;/p&gt;

&lt;p&gt;How­ev­er, it can be con­ve­nient to use a cloud-based stor­age sys­tem like Ama­zon Web Ser­vices (AWS) &lt;a href="https://aws.amazon.com/s3/"&gt;Sim­ple Stor­age Ser­vice (S3)&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’ll nev­er run out of disk space&lt;/li&gt;
&lt;li&gt;Your assets are inher­ent­ly backed up &amp;amp; stored off-site&lt;/li&gt;
&lt;li&gt;Your assets can go to long-term back­up in &lt;a href="https://aws.amazon.com/glacier/"&gt;AWS Glac­i­er&lt;/a&gt; easily&lt;/li&gt;
&lt;li&gt;You can lever­age a Con­tent Deliv­ery Net­work (CDN) in the form of &lt;a href="https://aws.amazon.com/cloudfront/"&gt;Cloud­front&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Elim­i­nates the need to sync assets between local dev, stag­ing, and pro­duc­tion environments&lt;/li&gt;
&lt;li&gt;It’s &lt;a href="https://aws.amazon.com/s3/pricing/"&gt;cheap&lt;/a&gt; (&lt;a href="https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&amp;amp;all-free-tier.sort-order=asc"&gt;or even free&lt;/a&gt;) &amp;amp; scalable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are oth­er advan­tages as well, but we’ll just assume you’re on-board, and get right into how to set it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set­ting up S3
&lt;/h2&gt;

&lt;p&gt;S3 stores files in ​“buck­ets”, and while you can serve assets direct­ly from an S3 buck­et, I’d &lt;strong&gt;strong­ly rec­om­mend against it&lt;/strong&gt;.&lt;/p&gt;


                                Don’t serve assets direct­ly from &lt;span&gt;S&lt;span&gt;3&lt;/span&gt;&lt;/span&gt; buckets
                            

&lt;p&gt;S3 buck­ets are intend­ed for asset stor­age, not serv­ing assets. It’ll work, but you’ll only be serv­ing them out of one geo­graph­ic region (wher­ev­er your S3 buck­et was cre­at­ed), and it real­ly was­n’t designed to for that.&lt;/p&gt;

&lt;p&gt;Instead, use AWS Cloud­Front as your Con­tent Deliv­ery Net­work (CDN) that actu­al­ly serves up your assets. &lt;/p&gt;

&lt;p&gt;Here’s what it looks like conceptually:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WxOGapiE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1012_crop_center-center_100_line/aws-s3-cloudfront-overview-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WxOGapiE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1012_crop_center-center_100_line/aws-s3-cloudfront-overview-diagram.png" alt="Aws s3 cloudfront overview diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is that when a per­son loads one of your web pages with an image on it, the image will point to a Cloud­Front URL.&lt;/p&gt;

&lt;p&gt;If that image is in the cache, it’ll return it from a CDN Edge serv­er that is geo­graph­i­cal­ly near the per­son load­ing the page. This makes it quick, with low latency.&lt;/p&gt;

&lt;p&gt;If the image isn’t in the cache, Cloud­Front pulls it from your S3 buck­et, returns it to the per­son, and prop­a­gates the image to the CDN Edge locations.&lt;/p&gt;


                                &lt;span&gt;S&lt;span&gt;3&lt;/span&gt;&lt;/span&gt; Buck­ets should­n’t be pub­licly accessible
                            

&lt;p&gt;The rule of thumb here is that S3 buck­ets aren’t pub­licly acces­si­ble. Instead, Cloud­Front is giv­en per­mis­sion to pull assets from the S3 buckets.&lt;/p&gt;

&lt;p&gt;That said, let’s get into set­ting it all up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Have an AWS account
&lt;/h2&gt;

&lt;p&gt;If you don’t already have an AWS account, you’re going to need one. Go to your &lt;a href="https://aws.amazon.com/console/"&gt;AWS Con­sole&lt;/a&gt; and either &lt;a href="https://console.aws.amazon.com/"&gt;log in&lt;/a&gt; to your account, or &lt;a href="https://portal.aws.amazon.com/gp/aws/developer/registration/index.html"&gt;cre­ate&lt;/a&gt; a new account.&lt;/p&gt;

&lt;p&gt;If you want the billing to go direct­ly to your clients, you can either cre­ate an AWS account for them, or use their exist­ing account.&lt;/p&gt;

&lt;p&gt;If you fac­tor in the rel­a­tive­ly low cost of S3 into your main­te­nance con­tracts or the like, you can just your account for all of your clients.&lt;/p&gt;

&lt;p&gt;If you don’t have one already, you should also &lt;a href="https://www.sweetprocess.com/procedures/_eG30mkvYDrfAmevj78A0i6E1GZE/add-an-administrator-to-your-amazon-aws-account/"&gt;set up an admin account&lt;/a&gt; for AWS now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Cre­ate an S3 Bucket
&lt;/h2&gt;

&lt;p&gt;Now that we have an AWS account, the first thing we’re going to do is set up our S3 buck­et. We’re going to use kebab-case for all of our AWS enti­ty names, and we’ll use the format:&lt;/p&gt;

&lt;p&gt;project-name + - + descriptor&lt;/p&gt;

&lt;p&gt;We’ll be set­ting up S3 + Cloud­Front for &lt;a href="https://devmode.fm/"&gt;dev​Mode​.fm&lt;/a&gt;, so the project name is devmode, and the buck­et name is devmode-bucket.&lt;/p&gt;

&lt;p&gt;In your AWS Con­sole, click on Ser­vices → S3 → &lt;strong&gt;+ Cre­ate Buck­et&lt;/strong&gt;. Fill in the name of the buck­et, and click through to the end (we won’t be chang­ing any set­tings from the default oth­er than the name):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--afONjFVM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x873_crop_center-center_100_line/aws-s3-create-bucket.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--afONjFVM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x873_crop_center-center_100_line/aws-s3-create-bucket.png" alt="Aws s3 create bucket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that for Per­mis­sions, it’s set to &lt;strong&gt;Block &lt;em&gt;all&lt;/em&gt; pub­lic access&lt;/strong&gt;. As dis­cussed above, our S3 buck­et will be pri­vate, and the Cloud­Front dis­tri­b­u­tion will be public.&lt;/p&gt;

&lt;p&gt;For most projects, I cre­ate one buck­et, and use sub-fold­ers in the buck­et for dif­fer­ent &lt;a href="https://docs.craftcms.com/v3/assets.html#volumes"&gt;Asset Vol­umes&lt;/a&gt;. You can cer­tain­ly also cre­ate more than one buck­et if that’ll work bet­ter for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Cre­ate a Cus­tom Policy
&lt;/h2&gt;

&lt;p&gt;Next we’ll set up a cus­tom &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create.html"&gt;Iden­ti­ty and Access Man­age­ment (IAM)&lt;/a&gt; pol­i­cy to con­trol access. IAM poli­cies can be attached to any AWS object, and they con­trol who can access what, with some fair­ly fine-grained permissions.&lt;/p&gt;

&lt;p&gt;We’re using a cus­tom pol­i­cy because we want to grant as lit­tle access as pos­si­ble, but still have it work correctly.&lt;/p&gt;


                                This isn’t a fan­cy watch. We don’t add com­pli­ca­tions just because they look cool
                            

&lt;p&gt;From your AWS Con­sole, click on Ser­vices → IAM → Poli­cies → &lt;strong&gt;Cre­ate Pol­i­cy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Click on the &lt;strong&gt;JSON&lt;/strong&gt; tab, and then paste this in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "acm:ListCertificates",
                "cloudfront:GetDistribution",
                "cloudfront:GetStreamingDistribution",
                "cloudfront:GetDistributionConfig",
                "cloudfront:ListDistributions",
                "cloudfront:ListCloudFrontOriginAccessIdentities",
                "cloudfront:CreateInvalidation",
                "cloudfront:GetInvalidation",
                "cloudfront:ListInvalidations",
                "elasticloadbalancing:DescribeLoadBalancers",
                "iam:ListServerCertificates",
                "sns:ListSubscriptionsByTopic",
                "sns:ListTopics",
                "waf:GetWebACL",
                "waf:ListWebACLs"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::REPLACE-WITH-BUCKET-NAME"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::REPLACE-WITH-BUCKET-NAME/*"
            ]
        }
    ]
}

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



&lt;p&gt;Make sure you replace REPLACE-WITH-BUCKET-NAME with the name of the buck­et we cre­at­ed in Step 2 (in our case devmode-bucket), in both places.&lt;/p&gt;

&lt;p&gt;Then click on &lt;strong&gt;Review Pol­i­cy&lt;/strong&gt; , and give your pol­i­cy a name (in our case devmode-policy) and descrip­tion, then click on &lt;strong&gt;Cre­ate Pol­i­cy&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--W42m8ieM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x842_crop_center-center_100_line/aws-s3-create-policy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W42m8ieM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x842_crop_center-center_100_line/aws-s3-create-policy.png" alt="Aws s3 create policy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Cre­ate a Group
&lt;/h2&gt;

&lt;p&gt;Instead of attach­ing the IAM pol­i­cy we cre­at­ed to a buck­et or a user, we’re going to cre­ate a group, and attach it to the group.&lt;/p&gt;


                                The &lt;span&gt;IAM&lt;/span&gt; pol­i­cy gets attached to the group
                            

&lt;p&gt;We’re doing this because it makes it triv­ial to move users in and out of the group, and the group is what is con­trol­ling access permissions.&lt;/p&gt;

&lt;p&gt;You also then won’t be out of luck if you some­how lose the user cre­den­tials, you can just cre­ate a new user and assign it to the group.&lt;/p&gt;

&lt;p&gt;From your AWS Con­sole, click on Ser­vices → IAM → Groups → &lt;strong&gt;Cre­ate New Group&lt;/strong&gt; , and give your group a name (in our case devmode-group).&lt;/p&gt;

&lt;p&gt;At the Attach Pol­i­cy screen, search for the pol­i­cy we cre­at­ed in Step 3, and check it and click &lt;strong&gt;Next Step&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7TvEjkeq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x777_crop_center-center_100_line/aws-s3-create-group.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7TvEjkeq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x777_crop_center-center_100_line/aws-s3-create-group.png" alt="Aws s3 create group"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click on Cre­ate Group to cre­ate your new group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Cre­ate a User
&lt;/h2&gt;

&lt;p&gt;Next we’re going to cre­ate a user, and assign it to the group we just cre­at­ed. The user we cre­ate will be a gener­ic project user, rather than an actu­al person.&lt;/p&gt;

&lt;p&gt;From your AWS Con­sole, click on Ser­vices → IAM → Users → &lt;strong&gt;Add user&lt;/strong&gt; , and give your user a name (in our case devmode-user). Also check only the &lt;strong&gt;Pro­gra­mat­ic access&lt;/strong&gt; check­box under Access type:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2gGSGJ2W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/aws-s3-add-user-details.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2gGSGJ2W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/aws-s3-add-user-details.png" alt="Aws s3 add user details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This ensures that using these cre­den­tials, there’s AWS Man­age­ment Con­sole access. For that, you’ll use your reg­u­lar admin account &amp;amp; credentials.&lt;/p&gt;

&lt;p&gt;Click on &lt;strong&gt;Next: Per­mis­sions&lt;/strong&gt; , then add the user to the group we cre­at­ed in Step 4:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0YPXSWUr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/aws-s3-add-user-permissions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0YPXSWUr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/aws-s3-add-user-permissions.png" alt="Aws s3 add user permissions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click on &lt;strong&gt;Next: Tags&lt;/strong&gt; (we don’t set any tags here), then click on &lt;strong&gt;Next: Review&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---Ijgs6qI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/aws-s3-add-user-review.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---Ijgs6qI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1020_crop_center-center_100_line/aws-s3-add-user-review.png" alt="Aws s3 add user review"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click on &lt;strong&gt;Cre­ate User&lt;/strong&gt;. You’ll be tak­en to a screen where you can see your &lt;strong&gt;Access key ID&lt;/strong&gt; and &lt;strong&gt;Secret access key&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RPWn-KVZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x478_crop_center-center_100_line/aws-s3-add-user-credentials.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RPWn-KVZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x478_crop_center-center_100_line/aws-s3-add-user-credentials.png" alt="Aws s3 add user credentials"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on &lt;strong&gt;Down­load .csv&lt;/strong&gt; to down­load a CSV file that has your cre­den­tials in it. You will need these cre­den­tials to access your S3 buck­et, and &lt;strong&gt;this is the only time you’ll be able to retrieve them&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Cre­ate a Cloud­Front Distribution
&lt;/h2&gt;

&lt;p&gt;Now we need to cre­ate a Cloud­Front Dis­tri­b­u­tion that will be act­ing as our CDN, and actu­al­ly deliv­er­ing our assets to the users who request them.&lt;/p&gt;

&lt;p&gt;From your AWS Con­sole, click on Ser­vices → Cloud­Front → &lt;strong&gt;Cre­ate Dis­tri­b­u­tion&lt;/strong&gt; , and click on the &lt;strong&gt;Get Start­ed&lt;/strong&gt; but­ton below the Web heading.&lt;/p&gt;

&lt;p&gt;Alter the fol­low­ing settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ori­gin Domain Name&lt;/strong&gt;  — choose the ori­gin for the S3 buck­et we cre­at­ed in Step 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict Buck­et Access&lt;/strong&gt;  — Yes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ori­gin Access Iden­ti­ty&lt;/strong&gt;  — Cre­ate a New Identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant Read Per­mis­sions on Buck­et&lt;/strong&gt;  — Yes, Update Buck­et Policy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View­er Pro­to­col Pol­i­cy&lt;/strong&gt;  — Redi­rect HTTP to HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowed HTTP Meth­ods&lt;/strong&gt;  — GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object Caching&lt;/strong&gt;  — Customize&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Min­i­mum TTL&lt;/strong&gt;  — 1296000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max­i­mum TTL&lt;/strong&gt;  — 31536000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default TTL&lt;/strong&gt;  — 1296000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Com­press Objects Auto­mat­i­cal­ly&lt;/strong&gt;  — Yes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a full screen­shot for the set­tings we’re using for dev​Mode​.fm:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T1-ZEJTH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x2903_crop_center-center_100_line/aws-cloudfront-create-distribution-settings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T1-ZEJTH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x2903_crop_center-center_100_line/aws-cloudfront-create-distribution-settings.png" alt="Aws cloudfront create distribution settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click on &lt;strong&gt;Cre­ate Dis­tri­b­u­tion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You’ll be tak­en to a strange ​“Cloud­Front Pri­vate Con­tent Get­ting Start­ed” page; this isn’t an error page, and there aren’t any more steps to take.&lt;/p&gt;

&lt;p&gt;We just need to grab a cou­ple of set­tings from our new­ly cre­at­ed Cloud­Front distribution.&lt;/p&gt;

&lt;p&gt;From your AWS Con­sole, click on Ser­vices → Cloud­Front → then click on the Cloud­Front dis­tri­b­u­tion we just created:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TODwpC0t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x810_crop_center-center_100_line/aws-cloudfront-settings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TODwpC0t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x810_crop_center-center_100_line/aws-cloudfront-settings.png" alt="Aws cloudfront settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re going to need the &lt;strong&gt;Dis­tri­b­u­tion ID&lt;/strong&gt; and &lt;strong&gt;Domain Name&lt;/strong&gt; set­tings, so copy them down somewhere.&lt;/p&gt;

&lt;p&gt;We’re all done with the AWS S3 + Cloud­Front setup!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Con­fig­ure your Asset Vol­umes in Craft CMS
&lt;/h2&gt;

&lt;p&gt;Next we need to con­fig­ure Craft CMS to use our new S3 buck­et set­up. If you’re using some­thing oth­er than Craft CMS, you’ll need to fill in the anal­o­gous settings.&lt;/p&gt;

&lt;p&gt;The key point to remem­ber is that the pub­lic URL will be our Cloud­Front dis­tri­b­u­tion URL (in our case &lt;a href="https://dnzwsrj1eic0g.cloudfront.net"&gt;https://dnzwsrj1eic0g.cloudfront.net&lt;/a&gt;); make sure you add the https:// pro­to­col to it.&lt;/p&gt;

&lt;p&gt;First, we’ll need to install the first-par­ty &lt;a href="https://plugins.craftcms.com/aws-s3"&gt;Ama­zon S3 plu­g­in&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next we’ll need to set up our Asset Vol­umes. I use &lt;a href="https://docs.craftcms.com/v3/config/environments.html"&gt;Envi­ron­ment Vari­ables&lt;/a&gt; in my .env file to store all of my secrets, so they aren’t in the data­base, and the don’t end up in Git via &lt;a href="https://docs.craftcms.com/v3/project-config.html"&gt;Project Con­fig&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# S3 settings
S3_KEY_ID=XXXXXXXXXX
S3_SECRET=XXXXXXXXXX
S3_BUCKET=devmode-bucket
S3_REGION=us-east-2

# CloudFront settings
CLOUDFRONT_URL=https://dnzwsrj1eic0g.cloudfront.net
CLOUDFRONT_DISTRIBUTION_ID=E17SKV1U1OTZKW
CLOUDFRONT_PATH_PREFIX=

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



&lt;p&gt;In the Craft CMS CP, go to Set­tings → Assets → &lt;strong&gt;New vol­ume&lt;/strong&gt; , and fill in the vol­ume set­tings as shown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--q5zTKOWx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x2034_crop_center-center_100_line/aws-s3-cloudfront-asset-volume-settings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--q5zTKOWx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x2034_crop_center-center_100_line/aws-s3-cloudfront-asset-volume-settings.png" alt="Aws s3 cloudfront asset volume settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that &lt;strong&gt;Buck­et&lt;/strong&gt; has been set to Man­u­al so we can use our envi­ron­ment vari­ables, and we’ve man­u­al­ly added episodes to &lt;strong&gt;Sub­fold­er&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Also note that we have turned &lt;strong&gt;Make Uploads Pub­lic&lt;/strong&gt; OFF. If you enable it, it will set a man­u­al ACL on your S3 assets to allow pub­lic access, which &lt;strong&gt;isn’t&lt;/strong&gt; what we want. We want pri­vate S3 assets with pub­lic access through Cloud­Front only&lt;/p&gt;

&lt;p&gt;If you’re using Project Con­fig, your project.yaml will end up look­ing like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
  e69c8edb-d562-4367-9a05-91a6fd2c7d99:
    name: 'Devmode Episodes'
    handle: devmodeEpisodes
    type: craft\awss3\Volume
    hasUrls: true
    url: $CLOUDFRONT_URL
    settings:
      subfolder: episodes
      keyId: $S3_KEY_ID
      secret: $S3_SECRET
      bucketSelectionMode: manual
      bucket: $S3_BUCKET
      region: $S3_REGION
      expires: '3 months'
      makeUploadsPublic: ''
      storageClass: ''
      cfDistributionId: $CLOUDFRONT_DISTRIBUTION_ID
      cfPrefix: $CLOUDFRONT_PATH_PREFIX
      autoFocalPoint: ''
    sortOrder: 4

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



&lt;p&gt;Look, mah, no secrets!&lt;/p&gt;

&lt;p&gt;Then we can upload an image to our new Asset Volume:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Dg4uFQpX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x825_crop_center-center_100_line/aws-s3-cloudfront-asset-volume-image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Dg4uFQpX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x825_crop_center-center_100_line/aws-s3-cloudfront-asset-volume-image.png" alt="Aws s3 cloudfront asset volume image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And we can ver­i­fy that the image is indeed com­ing from our Cloud­Front dis­tri­b­u­tion URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tLFtmtMH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x980_crop_center-center_100_line/aws-s3-cloudfront-asset-volume-url.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tLFtmtMH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x980_crop_center-center_100_line/aws-s3-cloudfront-asset-volume-url.png" alt="Aws s3 cloudfront asset volume url"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fill your Buckets
&lt;/h2&gt;

&lt;p&gt;That’s all you need to start enjoy­ing the ben­e­fits of cloud stor­age in S3, and Cloud­Front as a glob­al Con­tent Deliv­ery Network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MO2Gx0G5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/aws-s3-bucket-of-beer.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MO2Gx0G5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/aws-s3-bucket-of-beer.jpg" alt="Aws s3 bucket of beer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This whole man­u­al set­up can be auto­mat­ed via an &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html"&gt;AWS Cloud­For­ma­tion stack…&lt;/a&gt; but that’s left as an exer­cise for the read­er (or maybe anoth­er article).&lt;/p&gt;

&lt;p&gt;Thanks to all around great guy Jonathan Melville of &lt;a href="https://codemdd.io/"&gt;CodeMDD​.io&lt;/a&gt; for this help with this article.&lt;/p&gt;

&lt;p&gt;Hap­py uploading!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>images</category>
      <category>aws</category>
      <category>s3</category>
    </item>
    <item>
      <title>Using the Craft CMS "headless" with the GraphQL API</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Mon, 20 Jan 2020 05:00:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/using-the-craft-cms-headless-with-the-graphql-api-1gip</link>
      <guid>https://dev.to/gaijinity/using-the-craft-cms-headless-with-the-graphql-api-1gip</guid>
      <description>&lt;h1&gt;
  
  
  Using the Craft CMS “headless” with the GraphQL API
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Craft CMS 3.3 added a GraphQL lay­er that gives your web­site a for­mal­ized, struc­tured API out of the box. Here’s how to use GraphQL + Craft CMS as a ​“head­less” CMS
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wUnkeBXQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/craft-cms-graphql-database.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wUnkeBXQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/craft-cms-graphql-database.jpg" alt="Craft cms graphql database"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL &amp;amp; Craft CMS
&lt;/h2&gt;

&lt;p&gt;This arti­cle explores cod­ing pat­terns you might find use­ful when work­ing with Craft CMS’s GraphQL API via real-world exam­ples, and also why you might want to use GraphQL to begin with.&lt;/p&gt;

&lt;p&gt;GraphQL describes itself with the fol­low­ing pithy tagline:&lt;/p&gt;


                                A query lan­guage for your &lt;span&gt;API&lt;/span&gt;
                            

&lt;p&gt;And that’s exact­ly what it is. It’s a neu­tral lay­er that sits on top of what­ev­er API lay­er you have, from data­bas­es like MySQL, Post­gres, Mon­go, etc. to cus­tom built solutions.&lt;/p&gt;

&lt;p&gt;In August 2019, Pix­el &amp;amp; Ton­ic added GraphQL to &lt;a href="https://craftcms.com/blog/craft-33"&gt;Craft CMS 3.3&lt;/a&gt;, and vast­ly improved it in Craft CMS 3.4.&lt;/p&gt;

&lt;p&gt;Pri­or to this, we relied on Mark Huot’s &lt;a href="https://plugins.craftcms.com/craftql"&gt;CraftQL plu­g­in&lt;/a&gt; for GraphQL, as dis­cussed in the &lt;a href="https://dev.to/gaijinity/using-vuejs-graphql-to-make-practical-magic-1520-temp-slug-9813575"&gt;Using Vue­JS + GraphQL to make Prac­ti­cal Mag­ic&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Should you use Mark’s CraftQL plu­g­in or should you use Craft CMS’s first-par­ty GraphQL implementation?&lt;/p&gt;

&lt;p&gt;Unless you need muta­tions (the abil­i­ty to change data in the Craft CMS back­end via GraphQL), using the Craft CMS first-par­ty GraphQL imple­men­ta­tion in Craft 3.3 or lat­er is the way to go.&lt;/p&gt;

&lt;p&gt;Craft CMS’s first-par­ty GraphQL API does­n’t cur­rent­ly sup­port muta­tions, but it does offer a robust, per­for­mant data source for head­less Craft CMS.&lt;/p&gt;

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

&lt;p&gt;As a web devel­op­er, you’d use GraphQL if you were writ­ing a fron­tend that is sep­a­rate from the back­end. You’d be using Craft CMS as a ​“head­less” CMS for its excel­lent con­tent author­ing experience.&lt;/p&gt;

&lt;p&gt;Per­haps you’re writ­ing the fron­tend in a frame­work like &lt;a href="https://reactjs.org/"&gt;React&lt;/a&gt;, &lt;a href="https://vuejs.org/"&gt;Vue.js&lt;/a&gt;, &lt;a href="https://svelte.dev/"&gt;Svelte&lt;/a&gt;, or one of the frame­works that lay­ers on top of them like &lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt;, &lt;a href="https://nuxtjs.org/"&gt;Nuxt.js&lt;/a&gt;, &lt;a href="https://www.gatsbyjs.org/"&gt;Gats­by&lt;/a&gt;, &lt;a href="https://gridsome.org/"&gt;Grid­some&lt;/a&gt;, or &lt;a href="https://sapper.svelte.dev/"&gt;Sap­per&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Or per­haps you’re jump­ing on the &lt;a href="https://jamstack.org/"&gt;JAM­stack&lt;/a&gt; band­wag­on, and just need a self-host­ed CMS like Craft CMS to man­age your content.&lt;/p&gt;


                                If you need a way to get data out of Craft &lt;span&gt;CMS&lt;/span&gt;, GraphQL is a great way to do it
                            

&lt;p&gt;Or maybe the project has an iPhone app that needs to com­mu­ni­cate with a con­tent man­age­ment sys­tem on the back­end for data.&lt;/p&gt;

&lt;p&gt;In those cas­es, you don’t have access to &lt;a href="https://twig.symfony.com/"&gt;Twig&lt;/a&gt; or the &lt;a href="https://docs.craftcms.com/v3/dev/element-queries/"&gt;Ele­ment Queries&lt;/a&gt; to inter­act with Craft CMS and your data, but you still want to reap the ben­e­fits of Craft CMS’s won­der­ful &amp;amp; flex­i­ble con­tent author­ing experience.&lt;/p&gt;

&lt;p&gt;You could write a cus­tom API for Craft CMS using the &lt;a href="https://github.com/craftcms/element-api"&gt;Ele­ment API&lt;/a&gt;, but that can be a sig­nif­i­cant amount of work, and you’ll end up with some­thing bou­tique, rather than an indus­try standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter the GraphQL
&lt;/h2&gt;

&lt;p&gt;This is where GraphQL for Craft CMS excels. By virtue of cre­at­ing your con­tent mod­els in Craft CMS, you auto­mat­i­cal­ly get a GraphQL API to access it, with no extra work on your part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sdPy4xUH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/enter-the-dragon-graphql.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sdPy4xUH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/enter-the-dragon-graphql.jpg" alt="Enter the dragon graphql"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s also self-doc­u­ment­ing, and strict­ly typed. You don’t need to define the &lt;a href="https://graphql.org/learn/schema/"&gt;GraphQL schemas&lt;/a&gt;, because you’ve already implic­it­ly done that in Craft.&lt;/p&gt;

&lt;p&gt;It just works.&lt;/p&gt;

&lt;p&gt;You just need to be using Craft CMS ​“Pro” edi­tion, and you’ll see GraphQL in your CP, and you can imme­di­ate­ly start explor­ing the GraphQL API using the explor­er (which is pro­vid­ed by a fron­tend tool called &lt;a href="https://github.com/graphql/graphiql"&gt;GraphiQL&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rDmRmuf3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x757_crop_center-center_100_line/craft-cms-graphql-explorer-graphiql%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rDmRmuf3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x757_crop_center-center_100_line/craft-cms-graphql-explorer-graphiql%402x.png" alt="Craft cms graphql explorer graphiql 2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While it’s beyond the scope of this arti­cle to teach you GraphQL (there are many &lt;a href="https://www.google.com/search?q=learning+graphql&amp;amp;oq=learning+graphql"&gt;great resources online&lt;/a&gt; for that), there are a few things to note here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the upper-left pane, is the GraphQL query spec­i­fy­ing what we’re searching&lt;/li&gt;
&lt;li&gt;In the low­er-left pane are vari­ables we pass into the query, in JSON format&lt;/li&gt;
&lt;li&gt;The mid­dle pane is the data returned from the GraphQL end­point as a result of our query&lt;/li&gt;
&lt;li&gt;To the right pane is a search­able, doc­u­ment­ed schema reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fun thing about using this GraphQL explor­er is that you can learn by just pok­ing around.&lt;/p&gt;

&lt;p&gt;You can press &lt;strong&gt;Option-Space­bar&lt;/strong&gt; at any time for a list of auto-com­plet­ed items that can appear in any giv­en con­text. You can also use the Doc­u­men­ta­tion Explor­er to see your entire schema.&lt;/p&gt;

&lt;p&gt;Com­bined with the offi­cial &lt;a href="https://docs.craftcms.com/v3/graphql.html"&gt;Craft CMS GraphQL API doc­u­men­ta­tion&lt;/a&gt;, you can get pret­ty far just by explor­ing around in the GraphQL Explorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way of the GraphQL
&lt;/h2&gt;

&lt;p&gt;Before we dive into some queries in JavaScript, I have a few tips I’d like to pass along to you to make your life with GraphQL easier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--B1B4elO9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/bruce-lee-nunchucks-game-of-death-graphql.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B1B4elO9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/bruce-lee-nunchucks-game-of-death-graphql.jpg" alt="Bruce lee nunchucks game of death graphql"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One nice fea­ture of the Craft CMS imple­men­ta­tion of GraphQL is that it has a caching lay­er for your queries. While this is nor­mal­ly great for per­for­mance rea­sons, when you’re devel­op­ing a site, you real­ly want it off to avoid poten­tial­ly deal­ing with cached results.&lt;/p&gt;

&lt;p&gt;You can do this via the enableGraphQlCaching set­ting in your config/general.php file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/GeneralConfig.php.
 *
 * @see \craft\config\GeneralConfig
 */

return [
    // Craft config settings from .env variables
    'enableGraphQlCaching' =&amp;gt; (bool)getenv('ENABLE_GQL_CACHING'),
];

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



&lt;p&gt;Here I set it to an &lt;a href="https://docs.craftcms.com/v3/config/environments"&gt;Envi­ron­ment Vari­able&lt;/a&gt;, but you can set it to true or false direct­ly if you prefer.&lt;/p&gt;

&lt;p&gt;Craft CMS also has a &lt;a href="https://docs.craftcms.com/v3/config/config-settings.html#headlessmode"&gt;headless­Mode&lt;/a&gt; set­ting. This is intend­ed for sit­u­a­tions where Craft CMS will nev­er be used to ren­der any con­tent on the frontend.&lt;/p&gt;

&lt;p&gt;Due to lim­i­ta­tions in how this works in Craft CMS 3.3, I rec­om­mend leav­ing it set to false, since ele­ments lack URIs in the 3.3 imple­men­ta­tion of headlessMode. This caus­es many plu­g­ins to not work prop­er­ly, and oth­er unde­sired behavior.&lt;/p&gt;

&lt;p&gt;How­ev­er, Pix­el &amp;amp; Ton­ic has &lt;a href="https://github.com/craftcms/cms/issues/4520#issuecomment-531040417"&gt;made improve­ments&lt;/a&gt; to how the headlessMode set­ting works in Craft CMS 3.4, so you can set it to true if you want Craft CMS to nev­er ren­der con­tent, and sim­ply be a data/​API provider via GraphQL.&lt;/p&gt;

&lt;p&gt;Some­thing many peo­ple aren’t aware of is that you can use GraphQL in Twig if you want to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# Set the GraphQL query #}
{% set query %}
query organismsQuery($needle: String!)
{
    entries(section: "organisms", search: $needle, orderBy: "title") {
        title,
        slug,
        id
    }
}
{% endset %}

{# Set the variables to pass into the query #}
{% set variables = {
    'needle': 'Botulism'
} %}

{# Query the data via Twig! #}
{% set data = gql(query, variables) %}

{% dd data %}

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



&lt;p&gt;The gql() Twig fil­ter was added in &lt;a href="https://github.com/craftcms/cms/blob/develop/CHANGELOG-v3.md#added-5"&gt;Craft CMS 3.3.12&lt;/a&gt;. Why would you ever want to do such a thing?&lt;/p&gt;

&lt;p&gt;Usu­al­ly, you would­n’t. You’d just use &lt;a href="https://docs.craftcms.com/v3/dev/element-queries/"&gt;Ele­ment Queries&lt;/a&gt; instead… but it can be a nice way to exper­i­ment and play around with queries in a familiar/​comfortable environment.&lt;/p&gt;

&lt;p&gt;In fact, it ham­mers home a very impor­tant con­cep­tu­al point with GraphQL in Craft CMS:&lt;/p&gt;


                                In Craft &lt;span&gt;CMS&lt;/span&gt;, GraphQL queries map to Ele­ment Queries
                            

&lt;p&gt;GraphQL is a thin-ish lay­er in Craft CMS that han­dles map­ping from a GraphQL queries to Ele­ment Queries. It works in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The GraphQL schema is derived from the under­ly­ing Craft CMS Sec­tions, Ele­ment con­tent mod­els, etc.&lt;/li&gt;
&lt;li&gt;GraphQL queries are inter­nal­ly mapped to Ele­ment Queries, which are then executed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rea­son this is an impor­tant con­cept is that if you want to fig­ure out how to do some­thing in GraphQL, &lt;strong&gt;fig­ure out how to do it via an Ele­ment Query first, and then work your way back­wards&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So it can then be handy to use gql() in Twig, because you can write your Ele­ment Query as you nor­mal­ly would. Get it work­ing the way you want it to, and then con­struct &amp;amp; test the anal­o­gous GraphQL query in the same place.&lt;/p&gt;

&lt;p&gt;Final­ly, if you’re using Php­Storm, check out the &lt;a href="https://dev.to/gaijinity/graphql-schema-auto-completion-with-phpstorm-5ekm-temp-slug-8314156"&gt;GraphQL Schema Auto-Com­ple­tion with Php­Storm&lt;/a&gt; arti­cle for bliss­ful auto-com­plete query writ­ing in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Con­troller of Fury
&lt;/h2&gt;

&lt;p&gt;I men­tioned ear­li­er that you get a GraphQL API with­out hav­ing to do any work with Craft CMS. While this is true, hav­ing a lit­tle &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/structure-controllers"&gt;cus­tom con­troller&lt;/a&gt; as part of your site mod­ule can be helpful.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GSKj1t_V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/4937/bruce-lee-firsts-of-fury-graphql.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GSKj1t_V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/4937/bruce-lee-firsts-of-fury-graphql.jpg" alt="Bruce lee firsts of fury graphql"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I always have a site mod­ule as part of my Craft CMS setups, as dis­cussed in the &lt;a href="https://dev.to/gaijinity/enhancing-a-craft-cms-3-website-with-a-custom-module-33g1-temp-slug-8101978"&gt;Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;You always start with no cus­tom code, but inevitably you end up need­ing a lit­tle piece here or there. And if you have a mod­ule in place already, it makes adding this in real­ly easy.&lt;/p&gt;

&lt;p&gt;In our case, we want a cus­tom con­troller that our fron­tend can ping to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
actionGetCsrf() — get the cur­rent &lt;a href="https://www.yiiframework.com/doc/guide/2.0/en/security-best-practices#avoiding-csrf"&gt;CSRF token&lt;/a&gt; to val­i­date POST request submissions&lt;/li&gt;
&lt;li&gt;
actionGetGqlToken() — get a par­tic­u­lar GraphQL token for access permissions&lt;/li&gt;
&lt;li&gt;
actionGetFieldOptions() — get the options in a Drop­down field in Craft CMS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;POST requests are used to send data from the web­site along to an end­point. While we could use GET for sim­ple requests like actionGetGqlToken(), we need to send data to oth­er end­points like actionGetFieldOptions(), so we might as well use POST for all requests.&lt;/p&gt;

&lt;p&gt;Here’s what our cus­tom con­troller looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
/**
 * Site module for Craft CMS 3.x
 *
 * An example module for Craft CMS 3 that lets you enhance your websites with a
 * custom site module
 *
 * @link https://nystudio107.com/
 * @copyright Copyright (c) 2019 nystudio107
 */

namespace modules\sitemodule\controllers;

use Craft;
use craft\web\Controller;

use yii\web\Response;

/**
 * @author nystudio107
 * @package SiteModule
 * @since 1.0.0
 */
class SiteController extends Controller
{
    // Constants
    // =========================================================================

    const GQL_TOKEN_NAME = 'Site GQL Token';

    // Protected Properties
    // =========================================================================

    protected $allowAnonymous = [
        'get-csrf',
        'get-gql-token',
        'get-field-options',
    ];

    // Public Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    public function beforeAction($action): bool
    {
        // Disable CSRF validation for get-csrf POST requests
        if ($action-&amp;gt;id === 'get-csrf') {
            $this-&amp;gt;enableCsrfValidation = false;
        }

        return parent::beforeAction($action);
    }

    /**
     * @return Response
     */
    public function actionGetCsrf(): Response
    {
        return $this-&amp;gt;asJson([
            'name' =&amp;gt; Craft::$app-&amp;gt;getConfig()-&amp;gt;getGeneral()-&amp;gt;csrfTokenName,
            'value' =&amp;gt; Craft::$app-&amp;gt;getRequest()-&amp;gt;getCsrfToken(),
        ]);
    }

    /**
     * @return Response
     */
    public function actionGetGqlToken(): Response
    {
        $result = null;
        $tokens = Craft::$app-&amp;gt;getGql()-&amp;gt;getTokens();
        foreach ($tokens as $token) {
            if ($token-&amp;gt;name === self::GQL_TOKEN_NAME) {
                $result = $token-&amp;gt;accessToken;
            }
        }

        return $this-&amp;gt;asJson([
            'token' =&amp;gt; $result,
        ]);
    }

    /**
     * Return all of the field options from the passed in array of $fieldHandles
     *
     * @return Response
     */
    public function actionGetFieldOptions(): Response
    {
        $result = [];
        $request = Craft::$app-&amp;gt;getRequest();
        $fieldHandles = $request-&amp;gt;getBodyParam('fieldHandles');
        foreach ($fieldHandles as $fieldHandle) {
            $field = Craft::$app-&amp;gt;getFields()-&amp;gt;getFieldByHandle($fieldHandle);
            if ($field) {
                $result[$fieldHandle] = $field-&amp;gt;options;
            }
        }

        return $this-&amp;gt;asJson($result);
    }
}

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



&lt;p&gt;While this cus­tom con­troller isn’t nec­es­sary for using your GraphQL end­point, it does make the expe­ri­ence a bit nicer. We grab the CSRF token, and send that CSRF token along with fur­ther requests, to allow Yii2 to val­i­date the data submissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schemas &amp;amp; Tokens
&lt;/h2&gt;

&lt;p&gt;GraphQL in Craft CMS has a con­cept of Schemas and Tokens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schemas&lt;/strong&gt;  — schemas you can think of these as per­mis­sions, in a way, in that they define what parts of the under­ly­ing CraftCMS con­tent you want exposed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9Yo18XrL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x759_crop_center-center_100_line/craft-cms-graphql-schemas%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9Yo18XrL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x759_crop_center-center_100_line/craft-cms-graphql-schemas%402x.png" alt="Craft cms graphql schemas 2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens&lt;/strong&gt;  — tokens are they ​“keys” that you pass along with your GraphQL queries, and they link to a schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Wx_8CUPD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x759_crop_center-center_100_line/craft-cms-graphql-tokens%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Wx_8CUPD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x759_crop_center-center_100_line/craft-cms-graphql-tokens%402x.png" alt="Craft cms graphql tokens 2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what I do is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the CSRF token for the cur­rent session&lt;/li&gt;
&lt;li&gt;Use that CSRF to obtain a spe­cif­ic GraphQL token used for API access&lt;/li&gt;
&lt;li&gt;Use that GraphQL token in all GraphQL request to the endpoint&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In many cas­es, you won’t need to do this because you’ll just have one Pub­lic Schema that defines your GraphQL API. But if you want to poten­tial­ly have vary­ing lev­els of access, you’d cre­at­ed mul­ti­ple tokens and use them to what schema you can use.&lt;/p&gt;

&lt;p&gt;If you just want to use the spe­cial Pub­lic Schema, you don’t need any token at all. Just don’t send a bear­er token down with your GraphQL queries, and it’ll just use what­ev­er schema is defined in Pub­lic Schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N.B.:&lt;/strong&gt; In our case, we want pub­lic access to do the data. The CRSF and GraphQL tokens are just for light­weight auth, it is not treat­ed as a locked down ​“secret” for auth’ing. For that you’d want some­thing like &lt;a href="https://jwt.io/"&gt;JSON Web Tokens&lt;/a&gt; (JWT), check out the &lt;a href="https://plugins.craftcms.com/craft-jwt-auth"&gt;Craft JWT Auth&lt;/a&gt; plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Con­troller &amp;amp; GraphQL XHRs
&lt;/h2&gt;

&lt;p&gt;Let’s get into some JavaScript that we use to com­mu­ni­cate with our cus­tom con­troller end­points, and also the Craft CMS GraphQL endpoint.&lt;/p&gt;

&lt;p&gt;First here is a lit­tle xhr.js helper to con­fig­ure and exe­cute gener­ic XHRs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Configure the XHR api endpoint
export const configureXhrApi = (url) =&amp;gt; ({
    baseURL: url,
    headers: {
        'X-Requested-With': 'XMLHttpRequest'
    }
});

// Execute an XHR to our api endpoint
export const executeXhr = async(api, variables, callback) =&amp;gt; {
    // Execute the XHR
    try {
        const response = await api.post('', variables);
        if (response.data) {
            callback(response.data);
        }
    } catch (error) {
        console.error(error);
    }
};

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



&lt;p&gt;The X-Requested-With: XMLHttpRequest head­er lets Craft CMS know that this is an ​“AJAX” request, which can affect how the request is processed.&lt;/p&gt;

&lt;p&gt;Here’s how we get the CSRF from our cus­tom con­troller endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import axios from 'axios';
import { configureXhrApi, executeXhr } from '../utils/xhr.js';

const CSRF_ENDPOINT = '/actions/site-module/site/get-csrf';

// Fetch &amp;amp; commit the CSRF token
export const getCsrf = async({commit, state}) =&amp;gt; {
    const api = axios.create(configureXhrApi(CSRF_ENDPOINT));
    let variables = {
    };
    // Execute the XHR
    await executeXhr(api, variables, (data) =&amp;gt; {
        commit('setCsrf', data);
    });
};

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



&lt;p&gt;The above func­tion is a &lt;a href="https://vuex.vuejs.org/guide/actions.html"&gt;Vuex Action&lt;/a&gt;, but Vuex is just used as a con­ve­nient place to store data. It’s not impor­tant that you use Vuex your­self to get some­thing out of the exam­ples pre­sent­ed here.&lt;/p&gt;

&lt;p&gt;The code is just cre­at­ing an &lt;a href="https://github.com/axios/axios"&gt;Axios&lt;/a&gt; instance, exe­cut­ing the XHR, and stash­ing the returned CSRF data for future use.&lt;/p&gt;

&lt;p&gt;We just use Axios here as opposed to some­thing like &lt;a href="https://www.apollographql.com/docs/react/"&gt;Apol­lo Client&lt;/a&gt; just because all we real­ly need is a light­weight way to com­mu­ni­cate with­out GraphQL end­point. We could also use the built-in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"&gt;Fetch API&lt;/a&gt;, but Axios han­dles some edge-case sit­u­a­tions and brows­er com­pat­i­bil­i­ty for us.&lt;/p&gt;

&lt;p&gt;Then this is how we get the GraphQL token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

// Fetch &amp;amp; commit the GraphQL token
export const getGqlToken = async({commit, state}) =&amp;gt; {
    const api = axios.create(configureXhrApi(TOKEN_ENDPOINT));
    let variables = {
        ...(state.csrf &amp;amp;&amp;amp; { [state.csrf.name]: state.csrf.value }),
    };
    // Execute the XHR
    await executeXhr(api, variables, (data) =&amp;gt; {
        commit('setGqlToken', data);
    });
};

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



&lt;p&gt;Once again, this is a Vuex Action, but it does­n’t have to be. The impor­tant part is that it’s using the CSRF we obtained ear­li­er, and send­ing that along with a request for our GraphQL token, which it then saves for future use.&lt;/p&gt;

&lt;p&gt;The funky ...(state.csrf &amp;amp;&amp;amp; { [state.csrf.name]: state.csrf.value }), line deserves some expla­na­tion. It’s using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax"&gt;JavaScript Spread Syn­tax&lt;/a&gt; to add a key/​value pair to the variables object.&lt;/p&gt;

&lt;p&gt;But it’s doing it in a con­di­tion­al way, using the behav­ior of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators"&gt;JavaScript &amp;amp;&amp;amp; Log­i­cal Oper­a­tor&lt;/a&gt; to only spread the { [state.csrf.name]: state.csrf.value } object into variables only if state.csrf is truthy.&lt;/p&gt;

&lt;p&gt;This works because JavaScript will return the object if state.csrf is truthy, and if not, it’ll return state.csrf itself, and the … spread syn­tax is smart enough to know not to spread null, so it spread noth­ing into variables.&lt;/p&gt;

&lt;p&gt;Final­ly we have some helper JavaScript in gql.js (very sim­i­lar to the xhr.js above) which con­fig­ures and cre­ates an Axios instance for query­ing Craft’s GraphQL endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Configure the GraphQL api endpoint
export const configureGqlApi = (url, token) =&amp;gt; ({
    baseURL: url,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
        ...(token &amp;amp;&amp;amp; { 'Authorization': `Bearer ${token}` }),
    }
});

// Execute a GraphQL query by sending an XHR to our api endpoint
export const executeGqlQuery = async(api, query, variables, callback) =&amp;gt; {
    // Execute the GQL query
    try {
        const response = await api.post('', {
            query: query,
            variables: variables
        });
        if (callback &amp;amp;&amp;amp; response.data.data) {
            callback(response.data.data);
        }
        // Log any errors
        if (response.data.errors) {
            console.error(response.data.errors);
        }
    } catch (error) {
        console.error(error);
    }
};

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



&lt;p&gt;This just adds an addi­tion­al a &lt;a href="https://www.oauth.com/oauth2-servers/differences-between-oauth-1-2/bearer-tokens/"&gt;Bear­er Token&lt;/a&gt; to the head­er which Craft CMS will use to link to the schema that the request will have per­mis­sion to access, and it adds some error log­ging spe­cif­ic to the GraphQL implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Sim­ple Query
&lt;/h2&gt;

&lt;p&gt;Let’s look at a sim­ple query that uses the infra­struc­ture we’ve described above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
export const organismsQuery =
    `
query organismsQuery($needle: String!)
{
    entries(section: "organisms", search: $needle, orderBy: "title") {
        title,
        slug,
        id
    }
}
`;

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



&lt;p&gt;This is the same query we looked at in the GraphQL Explor­er pre­vi­ous­ly, which is exe­cut­ed as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

const GRAPHQL_ENDPOINT = '/api';

// Fetch &amp;amp; commit the Organisms array from a GraphQL query
export const getOrganisms = async({commit, state}) =&amp;gt; {
    const token = state.gqlToken ? state.gqlToken.token : null;
    const api = axios.create(configureGqlApi(GRAPHQL_ENDPOINT, token));
    let variables = {
        needle: 'useInSearchForm:*'
    };
    // Execute the GQL query
    await executeGqlQuery(api, organismsQuery, variables, (data) =&amp;gt; {
        if (data.entries) {
            commit('setOrganisms', data.entries);
        }
    });
};

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



&lt;p&gt;This is once again a Vuex Action, which uses the GraphQL token we’ve already obtained, and pass­es in useInSearchForm:* as a search para­me­ter in needle.&lt;/p&gt;

&lt;p&gt;This is look­ing for a lightswitch field in Craft CMS with the han­dle useInSearchForm, so that it’ll only return entries from the organisms sec­tion that have that lightswitch on.&lt;/p&gt;

&lt;p&gt;This returns data that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[
    {
        "title": "Bacillus cereus",
        "slug": "bacillus-cereus",
        "id": "10226"
    },
    {
        "title": "Bacterial toxin",
        "slug": "bacterial-toxin",
        "id": "14472"
    },
]

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



&lt;h2&gt;
  
  
  Query­ing a Sin­gle Thing
&lt;/h2&gt;

&lt;p&gt;Nor­mal­ly all of your GraphQL queries will return an array of data, even if you’re only ask­ing for one thing. While this con­sis­ten­cy is nice, there are times when you real­ly only ever want one thing returned.&lt;/p&gt;

&lt;p&gt;In Craft CMS 3.4, sin­gu­lar ver­sions of all of the queries were made available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
assets() → asset()
&lt;/li&gt;
&lt;li&gt;
categories() → category()
&lt;/li&gt;
&lt;li&gt;
entries() → entry()
&lt;/li&gt;
&lt;li&gt;
globalSets() → globalSet()
&lt;/li&gt;
&lt;li&gt;
tags() → tag()
&lt;/li&gt;
&lt;li&gt;
users() → user()
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is anal­o­gous to the .all() → .one() meth­ods used on Ele­ment Queries.&lt;/p&gt;

&lt;p&gt;So we can lever­age this for our queries where we only ever want one thing returned, so we don’t have to do ugly [0] array syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
export const outbreakDetailQuery =
    `
query outbreakDetailQuery($slug: [String])
{
    entry(section: "outbreaks", slug: $slug) {
        title,
        url,
        slug,
        ...on outbreaks_outbreaks_Entry {
            summary,
            beginningDate,
            states,
            country,
            hasTest,
            testResults,
            productSubjectToRecall,
            totalIll,
            numberIllByCaseDefinitionKnown,
            probableIll,
            possibleIll,
            confirmedIll,
            hospitalized,
            numberHospitalized,
            anyDeaths,
            numberOfDeaths,
            recallLinks {
                col1
                url
            },
            reportReferenceLinks {
                col1
                url
            },
            brands {
                title
            },
            locations {
                title
            },
            organisms {
                title
            },
            tags {
                title
            },
            vehicles {
                title
            }
        }
    }
}
`;

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



&lt;p&gt;This rather large query exem­pli­fies the GraphQL mind­set, which is that you need to spec­i­fy &lt;strong&gt;all&lt;/strong&gt; of the data that you want returned. This is unlike Ele­ment Queries, where by default you get all of the data back.&lt;/p&gt;

&lt;p&gt;Here’s what it looks like exe­cut­ing this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

const GRAPHQL_ENDPOINT = '/api';

// Fetch &amp;amp; commit an Outbreak detail from a GraphQL query
export const getOutbreakDetail = async({commit, state}) =&amp;gt; {
    const token = state.gqlToken ? state.gqlToken.token : null;
    const outbreakSlug = state.outbreakSlug || null;
    const api = axios.create(configureGqlApi(GRAPHQL_ENDPOINT, token));
    let variables = {
        slug: outbreakSlug
    };
    // Execute the GQL query
    await executeGqlQuery(api, outbreakDetailQuery, variables, (data) =&amp;gt; {
        if (data.entry) {
            commit('setOutbreakDetail', data.entry);
        }
    });
};

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



&lt;p&gt;This code is almost exact­ly iden­ti­cal to the code we used to for getOrganisms(), except that we pass in a slug to look for in our entry() query rather than a search parameter.&lt;/p&gt;

&lt;p&gt;Here’s what the data returned from this query looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
  "title": "2017 Multistate Outbreak of Salmonella Infantis Linked Mangoes (Suspected)",
  "url": "http://outbreakdatabase.test/outbreaks/2017-multistate-outbreak-of-salmonella-infantis-linked-mangoes-suspected",
  "slug": "2017-multistate-outbreak-of-salmonella-infantis-linked-mangoes-suspected",
  "summary": "In the summer of 2017 federal, state and local health officials investigated an outbreak of Salmonella Infantis. The suspected vehicle of transmission was mangoes. Forty eight outbreak associated cases were reported by 14 states. Reported and estimated illness onset dates ranged from June 30, 2017 to August 31, 2017. Among 42 cases with available information, there were 15 reported hospitalizations. No deaths were reported. \nThe cluster investigation was assigned CDC 1708MLJFX-1.",
  "beginningDate": "2017-06-01T07:00:00+00:00",
  "states": [
    "CA",
    "IL",
    "IN",
    "MI",
    "MN",
    "NJ",
    "NY",
    "NC",
    "OH",
    "OK",
    "TX",
    "WA",
    "WI"
  ],
  "country": "us",
  "hasTest": "yes",
  "testResults": "JFXX01.0010",
  "productSubjectToRecall": "yes",
  "totalIll": 48,
  "numberIllByCaseDefinitionKnown": "yes",
  "probableIll": null,
  "possibleIll": null,
  "confirmedIll": 48,
  "hospitalized": "yes",
  "numberHospitalized": 15,
  "anyDeaths": "",
  "numberOfDeaths": null,
  "recallLinks": [
    {
      "col1": "",
      "url": ""
    }
  ],
  "reportReferenceLinks": [
    {
      "col1": "",
      "url": ""
    }
  ],
  "brands": [],
  "locations": [
    {
      "title": "Retail"
    }
  ],
  "organisms": [
    {
      "title": "Salmonella"
    }
  ],
  "tags": [
    {
      "title": "salmonellosis"
    },
    {
      "title": "salmonella"
    },
    {
      "title": "mangoes"
    },
    {
      "title": "infantis"
    }
  ],
  "vehicles": [
    {
      "title": "Mangoes"
    }
  ]
}

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



&lt;h2&gt;
  
  
  Dynam­ic Para­me­ter Queries
&lt;/h2&gt;

&lt;p&gt;But what if we’re doing some­thing more com­pli­cat­ed, like a faceted search, where we want to nar­row down the search cri­te­ria based on a series of option­al search para­me­ters from the user such as shown in this video:&lt;/p&gt;

&lt;p&gt;Pri­or to Craft CMS 3.4, you could­n’t do this via the GraphQL API. But Craft CMS 3.4 added two key fea­tures to make it possible&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s now pos­si­ble to query for ele­ments by their cus­tom field val­ues via GraphQL. (&lt;a href="https://github.com/craftcms/cms/issues/5208"&gt;#5208&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;It’s now pos­si­ble to fil­ter ele­ment query results by their relat­ed ele­ments using rela­tion­al fields’ ele­ment query params (e.g. publisher(100) rather than relatedTo({targetElement: 100, field: 'publisher'})). (&lt;a href="https://github.com/craftcms/cms/issues/5200"&gt;#5200&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are both huge in terms of mak­ing the GraphQL API flex­i­ble, so hat’s off to Andris for mak­ing it happen.&lt;/p&gt;

&lt;p&gt;This allows us to con­struct a dynam­ic query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
export const outbreaksQuery = (additionalParams) =&amp;gt; {
    let queryAdditionalParams = '';
    let entryAdditionalParams = '';
    additionalParams.forEach((item) =&amp;gt; {
        queryAdditionalParams += `, $${item.fieldName}: [QueryArgument]`;
        entryAdditionalParams += `, ${item.fieldName}: $${item.fieldName}`;
    });
    return `
    query outbreaksQuery($needle: String!${queryAdditionalParams})
    {
        entries(section: "outbreaks", search: $needle${entryAdditionalParams}) {
            title,
            url,
            slug,
            ...on outbreaks_outbreaks_Entry {
                summary,
                beginningDate,
                vehicles {
                    title
                },
                organisms {
                    title
                },
                tags {
                    title
                }
            }
        }
    }
    `;
};

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



&lt;p&gt;Note that this is a func­tion, not a sim­ple returned &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals"&gt;tem­plate lit­er­al&lt;/a&gt; as in the pre­vi­ous examples.&lt;/p&gt;

&lt;p&gt;This allows us to dynam­i­cal­ly add para­me­ters to our query based on what­ev­er has been pushed into the additionalParams array.&lt;/p&gt;

&lt;p&gt;So the base query looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
query outbreaksQuery($needle: String!)
{
    entries(section: "outbreaks", search: $needle) {
        title,
        url,
        slug,
        ...on outbreaks_outbreaks_Entry {
            summary,
            beginningDate,
            vehicles {
                title
            },
            organisms {
                title
            },
            tags {
                title
            }
        }
    }
}

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



&lt;p&gt;…and the vari­ables we pass in look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
    needle: "vomit"
}

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



&lt;p&gt;If the user then also adds in an organ­ism to search on (which in our case is a relat­ed entry in the Craft CMS back­end), the query will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
query outbreaksQuery($needle: String!, $organisms: [QueryArgument])
{
    entries(section: "outbreaks", search: $needle, organisms: $organisms) {
        title,
        url,
        slug,
        ...on outbreaks_outbreaks_Entry {
            summary,
            beginningDate,
            vehicles {
                title
            },
            organisms {
                title
            },
            tags {
                title
            }
        }
    }
}

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



&lt;p&gt;…and the vari­ables we pass in look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
    needle: "vomit",
    organisms: "4425"
}

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



&lt;p&gt;The 4425 num­ber is the ele­ment ID of the relat­ed organ­ism entry in the Organ­isms sec­tion. It could also be an array of IDs, or any­thing else you’d nor­mal­ly pass in as a rela­tion­al field­’s ele­ment query params.&lt;/p&gt;

&lt;p&gt;This tech­nique works for an arbi­trary num­ber of addi­tion­al faceted search cri­te­ria, which gives the user quite a bit of pow­er in the search form.&lt;/p&gt;

&lt;p&gt;Here’s the Vuex Action code that imple­ments it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

const GRAPHQL_ENDPOINT = '/api';

// Push additional params
const pushAdditionalParam = (state, dataFieldName, dbFieldName, additionalParams) =&amp;gt; {
    let fieldValue = state.searchForm ? state.searchForm[dataFieldName] || '' : '';
    if (fieldValue.length) {
        // Special case for fields
        if (dbFieldName === 'states') {
            // As per https://docs.craftcms.com/v3/checkboxes-fields.html#querying-elements-with-checkboxes-fields
            fieldValue = `*"${fieldValue}"*`
        }
        additionalParams.push({
            fieldName: dbFieldName,
            fieldValue: fieldValue,
        });
    }
};

// Fetch &amp;amp; commit the Outbreaks array from a GraphQL query
export const getOutbreaks = async({commit, state}) =&amp;gt; {
    const token = state.gqlToken ? state.gqlToken.token : null;
    const keywords = state.searchForm ? state.searchForm.keywords || '*' : '*';
    // Construct the additional parameters
    let additionalParams = [];
    for (const [key, value] of Object.entries(additionalParamFields)) {
        pushAdditionalParam(state, key, value, additionalParams);
    }
    // Configure out API endpoint
    const api = axios.create(configureGqlApi(GRAPHQL_ENDPOINT, token))
    // Construct the variables object
    let variables = {
        needle: keywords,
    };
    additionalParams.forEach((item) =&amp;gt; {
        variables[item.fieldName] = item.fieldValue;
    });
    // Execute the GQL query
    await executeGqlQuery(api, outbreaksQuery(additionalParams), variables, (data) =&amp;gt; {
        if (data.entries) {
            commit('setOutbreaks', data.entries);
        }
    });
};

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



&lt;p&gt;This pushAdditionalParam() func­tion just does a lit­tle mag­ic to map from the name of a prop­er­ty in our Vue com­po­nent to the name of the field in the database.&lt;/p&gt;

&lt;p&gt;Then it spe­cial-cas­es for the states field, which is Check­box­es field in Craft CMS. To query for an item in a Check­box­es field, you need to use the for­mat '&lt;em&gt;"foo"&lt;/em&gt;'.&lt;/p&gt;

&lt;p&gt;As per what we dis­cussed pre­vi­ous­ly, we fig­ured this out by &lt;a href="https://docs.craftcms.com/v3/checkboxes-fields.html#templating"&gt;look­ing up how to do it in an Ele­ment Query&lt;/a&gt;, and then using that in our GraphQL query.&lt;/p&gt;

&lt;p&gt;Then the getOutbreaks() func­tion then just dynam­i­cal­ly builds the additionalParams array and variables object, which then is passed along to our query.&lt;/p&gt;

&lt;p&gt;The data returned from this query looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
[
  {
    "title": "1997 Botulism After Consumption of Home-Pickled Eggs, Illinois",
    "url": "http://outbreakdatabase.test/outbreaks/1997-botulism-after-consumption-of-home-pickled-eggs-illinois",
    "slug": "1997-botulism-after-consumption-of-home-pickled-eggs-illinois",
    "summary": "On November 23, 1997, a previously healthy man became nauseated, vomited, and complained of abdominal pain. During the next 2 days, he developed double vision, difficult movement of joints, and respiratory impairment. He was hospitalized and placed on mechanical ventilation. His physical examination confirmed multiple cranial nerve abnormalities, including extraocular motor palsy and diffuse flaccid paralysis. Botulism was diagnosed, and antibotulinum toxin was administered. His blood serum demonstrated the presence of type B botulinum toxin. A food history revealed no exposures to home-canned products; however, the patient had eaten pickled eggs that he had prepared seven days before his symptoms began. The patient recovered after a prolonged period of supportive care. \n\nThe pickled eggs were prepared using a recipe that consisted of hard-boiled eggs, commercially prepared beets and hot peppers, and vinegar. The intact hard-boiled eggs were peeled and punctured with toothpicks then combined with the other ingredients in a glass jar that closed with a metal screw-on lid. The mixture was stored at room temperature and occasionally was exposed to sunlight. \n\nCultures revealed Clostridium botulinum type B, and type B toxin was detected in samples of the pickled egg mixture, the pickling liquid, beets, and egg yolk. The commercially sold peppers contained no detectable toxin or Clostridium botulinum bacteria. Beets from the original commercial containers were not available. The pH of the pickling liquid was 3.5 (i.e., adequate to prevent C. botulinum germination and toxin formation, however, the pH of egg yolk, although not tested for the investigation, is normally 6.8., conducive for bacteria growth and toxin production. Inadequate acidification and lack of refrigeration enhanced the risk of bacterial contamination of this home-prepared food.",
    "beginningDate": "1997-11-01T08:00:00+00:00",
    "vehicles": [
      {
        "title": "Eggs, Pickled eggs"
      }
    ],
    "organisms": [
      {
        "title": "Botulism"
      }
    ],
    "tags": [
      {
        "title": "c.+botulinum"
      },
      {
        "title": "clostridium+botulinum"
      },
      {
        "title": "c.+bot."
      }
    ]
  }
]

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



&lt;h2&gt;
  
  
  Tap­ping Out
&lt;/h2&gt;

&lt;p&gt;Hope­ful­ly these real-world exam­ples of using Craft CMS’s GraphQL API in ​“head­less” CMS setups has been help­ful to you!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KAuvn6Us--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/bruce-lee-sunset-graphql.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KAuvn6Us--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/bruce-lee-sunset-graphql.jpg" alt="Bruce lee sunset graphql"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Craft CMS is an excel­lent choice if you want to com­bine a great con­tent author­ing expe­ri­ence on the back­end with a ​“head­less” CMS that serves up data to a fron­tend via a GraphQL API.&lt;/p&gt;

&lt;p&gt;If you’re a plu­g­in or cus­tom mod­ule devel­op­er, there are also some oth­er nice new fea­tures in Craft CMS 3.4 for GraphQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plu­g­ins can now mod­i­fy the GraphQL schema via craft\gql\TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS
&lt;/li&gt;
&lt;li&gt;Plu­g­ins can now mod­i­fy the GraphQL per­mis­sions via craft\services\Gql::EVENT_REGISTER_GQL_PERMISSIONS
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows for great flex­i­bil­i­ty in terms of extend­ing the exist­ing Craft CMS GraphQL API.&lt;/p&gt;

&lt;p&gt;Hap­py querying!&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>craftcms</category>
      <category>graphql</category>
      <category>frontend</category>
    </item>
    <item>
      <title>An Effective Twig Base Templating Setup</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Thu, 21 Nov 2019 01:29:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/an-effective-twig-base-templating-setup-2h2b</link>
      <guid>https://dev.to/gaijinity/an-effective-twig-base-templating-setup-2h2b</guid>
      <description>&lt;h1&gt;
  
  
  An Effective Twig Base Templating Setup
&lt;/h1&gt;

&lt;h3&gt;
  
  
  A good base tem­plat­ing set­up for your Craft CMS Twig tem­plates pro­vides a sta­ble, sol­id foun­da­tion on which to build your projects
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pc4xtkBq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/solid-twig-templating-layer.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pc4xtkBq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/solid-twig-templating-layer.jpg" alt="Solid twig templating layer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twig.symfony.com/"&gt;Twig&lt;/a&gt; is a fan­tas­tic tem­plat­ing lan­guage that fea­tures mul­ti­ple inher­i­tance of lay­out tem­plates, and is opti­mized to be an easy to use pre­sen­ta­tion layer.&lt;/p&gt;

&lt;p&gt;This arti­cle dis­cuss­es an effec­tive Twig base tem­plat­ing set­up that I have found to work extreme­ly well for me in my &lt;a href="https://craftcms.com/"&gt;Craft CMS&lt;/a&gt; websites.&lt;/p&gt;

&lt;p&gt;How­ev­er, even if you use anoth­er CMS that uses Twig like &lt;a href="https://www.drupal.org/"&gt;Dru­pal&lt;/a&gt; or &lt;a href="https://getgrav.org/"&gt;Grav&lt;/a&gt;, or you use anoth­er tem­plat­ing lan­guage entire­ly like &lt;a href="https://laravel.com/docs/5.8/blade"&gt;Blade&lt;/a&gt; or &lt;a href="https://docs.statamic.com/antlers"&gt;Antlers&lt;/a&gt;, the prin­ci­ples dis­cussed here still apply.&lt;/p&gt;


                                Think of a tem­plat­ing lan­guage as the thin lay­er of frost­ing that cov­ers the lay­er cake that is your web­site, and makes it presentable.
                            

&lt;p&gt;The key thing to note here is that Twig is a tem­plat­ing lan­guage, and as such it should not be used for com­pli­cat­ed busi­ness or inten­sive calculations.&lt;/p&gt;

&lt;p&gt;Not that it can’t han­dle either (it can) but rather that it shouldn’t.&lt;/p&gt;

&lt;p&gt;If you’re unclear as to why, read about why Twig was cre­at­ed to begin with in the &lt;a href="http://fabien.potencier.org/templating-engines-in-php.html"&gt;Tem­plat­ing Engines in PHP&lt;/a&gt; article.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s all about that base
&lt;/h2&gt;

&lt;p&gt;Over the years, on a vast array of soft­ware projects of all shapes and sizes, I’ve seen devel­op­ers chas­ing the holy grail of code reusability.&lt;/p&gt;

&lt;p&gt;Often times it ends up being that they spend an inor­di­nate amount of time cre­at­ing ​“the one high lev­el frame­work to rule them all”, only to be con­fused when real­i­ty butts in its ugly head.&lt;/p&gt;


                                Many high lev­el frame­works are either over-engi­neered &lt;span&gt;&amp;amp;&lt;/span&gt; unwieldy or so spe­cif­ic they aren’t that reusable in the end anyway.
                            

&lt;p&gt;I try to be more prac­ti­cal about which things I will actu­al­ly re-use (and some would say less ambitious).&lt;/p&gt;

&lt;p&gt;Web­sites cre­at­ed in Craft CMS tend to be more on the bespoke side of things, oth­er­wise they might be bet­ter done in a more cook­ie-cut­ter sys­tem anyway.&lt;/p&gt;

&lt;p&gt;So what I re-use are very fun­da­men­tal things like the build sys­tem (dis­cussed in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-5646-temp-slug-4335831"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; arti­cle), and a base tem­plat­ing system.&lt;/p&gt;

&lt;p&gt;If you’re going to build any­thing of sub­stance, it’s cru­cial that the base it’s built on is robust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nBbzZhcr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/human-tower-base.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nBbzZhcr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/human-tower-base.jpg" alt="Human tower base"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here’s what I want out of a base tem­plat­ing system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The abil­i­ty to use it unmod­i­fied on a wide vari­ety of projects&lt;/li&gt;
&lt;li&gt;One tem­plate that can be used both as a web page, and as pop­up modals via AJAX / XHR&lt;/li&gt;
&lt;li&gt;Imple­ment core fea­tures for me, with­out restrict­ing me in terms of flexibility&lt;/li&gt;
&lt;li&gt;Allow for cre­at­ing Google AMP pages, if the project war­rants it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Often I see devel­op­ers mak­ing tem­plates that inher­it from just one lay­out, or if they use mul­ti­ple lay­outs, it’s still a sin­gle inher­i­tance chain.&lt;/p&gt;

&lt;p&gt;Twig allows for more than that. So let’s see how one approach to lever­age this might work.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO &amp;amp; pop­up modals
&lt;/h2&gt;

&lt;p&gt;Many of the points men­tioned in the pre­vi­ous sec­tion are large­ly self-explana­to­ry, but point 2 deserves more expla­na­tion. It’s all about tem­plates work­ing both as web pages and pop­up modals loaded in via AJAX / XHR.&lt;/p&gt;

&lt;p&gt;I fre­quent­ly work with Jonathan Melville of &lt;a href="https://codemdd.io/"&gt;Code MDD&lt;/a&gt; on projects, and he often does designs that have con­tent in pop­up modals.&lt;/p&gt;

&lt;p&gt;For exam­ple, if you go to the &lt;a href="https://seasidefl.com/events"&gt;Sea­side Events&lt;/a&gt; page you’ll see a num­ber of events list­ed, and if you click on an event, you’ll see the event details in a pop­up modal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dIy3FAxa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x893_crop_center-center_100_line/seaside-farmers-market-popup-modal.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dIy3FAxa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x893_crop_center-center_100_line/seaside-farmers-market-popup-modal.png" alt="Seaside farmers market popup modal"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is great, and gives it a nice app-ish feel, allow­ing the user to view mul­ti­ple events with­out leav­ing the orig­i­nal page.&lt;/p&gt;

&lt;p&gt;But for SEO rea­sons, as well as for canon­i­cal page link­ing rea­sons, the same con­tent can also be found on its own unique page: &lt;a href="https://seasidefl.com/events/seaside-farmers-market-november"&gt;Sea­side Farmer’s Mar­ket — Sat­ur­days in Novem­ber&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZKm_evqW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x888_crop_center-center_100_line/seaside-farmers-market-webpage.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZKm_evqW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x888_crop_center-center_100_line/seaside-farmers-market-webpage.png" alt="Seaside farmers market webpage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is ide­al­ly what we want to be able to do auto­mat­i­cal­ly: have the same core &lt;strong&gt;con­tent&lt;/strong&gt; be dis­playable both with and with­out the web page ​“chrome” around it.&lt;/p&gt;

&lt;p&gt;And this is one of the things that the base tem­plat­ing set­up does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The over­all structure
&lt;/h2&gt;

&lt;p&gt;Here’s an overview of what this base tem­plat­ing sys­tem looks like. It may seem involved, but we’ll break it down:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2nYohXla--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1234_crop_center-center_100_line/twig-base-templates-diagram-2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2nYohXla--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1234_crop_center-center_100_line/twig-base-templates-diagram-2x.png" alt="Twig base templates diagram 2x"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The orange round­ed rec­tan­gles rep­re­sent tem­plates that will be in your templates/_layouts/ direc­to­ry, and may vary from project to project.&lt;/p&gt;

&lt;p&gt;The blue rec­tan­gles rep­re­sent boil­er­plate tem­plates that will be in your templates/_boilerplate/_layouts/ direc­to­ry, and won’t change from project to project.&lt;/p&gt;


                                You do not have to use the exact Twig base tem­plat­ing set­up we use; but you may ben­e­fit from tak­ing away and using the con­cepts presented
                            

&lt;p&gt;If at this point you’re some­one who learns bet­ter by real-world exam­ples, the exact base tem­plat­ing sys­tem described here is used in the MIT-licensed &lt;a href="https://github.com/nystudio107/devmode"&gt;dev​Mode​.fm web­site Github repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to check it out; it’s also used in the &lt;a href="https://github.com/nystudio107/craft"&gt;nystudio107/​craft boil­er­plate set­up&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Mean­while, every­one else, read on! We’re going to break down each template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PROJECT:&lt;/strong&gt; will pre­fix each tem­plate that may vary from project to project&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BOIL­ER­PLATE:&lt;/strong&gt; will pre­fix each tem­plate that stays the same from project to project&lt;/p&gt;

&lt;p&gt;Here we go…&lt;/p&gt;

&lt;h2&gt;
  
  
  PROJECT: global-variables.twig
&lt;/h2&gt;

&lt;p&gt;Due to &lt;a href="https://dev.to/gaijinity/twig-processing-order-scope-2f0k-temp-slug-7955660"&gt;Twig’s Pro­cess­ing Order &amp;amp; Scope&lt;/a&gt;, if we want to have glob­al vari­ables that are always avail­able in all of our tem­plates, they need to be defined in the root tem­plate that all oth­ers extends from. &lt;/p&gt;

&lt;p&gt;Since these glob­als can vary from project to project, they are not part of the boil­er­plate, but they are required for the setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Root global variables that all templates inherit from -- #}
{# -- This allows for defining site-wide Twig variables as needed -- #}
{% spaceless %}

{# -- Prefetch &amp;amp; preconnect headers and links -- #}
{% set prefetchUrls = [
    alias("@assetsUrl"),
] %}
{# -- General global variables -- #}
{% set baseUrl = alias('@assetsUrl') ~ '/' %}
{% set gaTrackingId = getenv('GA_TRACKING_ID') %}

{# -- Twig output from the render; this must be in a block -- #}
{% block htmlPage %}
{% endblock %}

{% endspaceless %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--B-MTAZgU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x188_crop_center-center_100_line/blocks-global-variables.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B-MTAZgU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x188_crop_center-center_100_line/blocks-global-variables.png" alt="Blocks global variables"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The global-variables.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
htmlPage — a block that encom­pass­es the entire ren­dered HTML page&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  BOIL­ER­PLATE: base-web-layout.twig
&lt;/h2&gt;

&lt;p&gt;Every web­page, whether a reg­u­lar web page or a Google AMP page inher­its from this tem­plate. The set­up may look a lit­tle weird, but it’s done this way so that child tem­plates can over­ride bits like the open­ing &amp;lt;html&amp;gt; tag if they need to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Base web layout template that all web requests inherit from -- #}
{% extends "_layouts/global-variables.twig" %}

{%- block htmlPage -%}
    {% minify %}
    &amp;lt;!DOCTYPE html&amp;gt;
        {% block htmlTag %}
            &amp;lt;html lang="{{ craft.app.language |slice(0,2) }}"&amp;gt;
        {% endblock htmlTag %}
        {% block headTag %}
            &amp;lt;head&amp;gt;
        {% endblock headTag %}
            {% include "_boilerplate/_partials/head-meta.twig" %}
            {# -- Page content that should be included in the &amp;lt;head&amp;gt; -- #}
            {% block headContent %}
            {% endblock headContent %}
            &amp;lt;/head&amp;gt;

            {% block bodyTag %}
            &amp;lt;body&amp;gt;
            {% endblock bodyTag %}
                {# -- Page content that should be included in the &amp;lt;body&amp;gt; -- #}
                {% block bodyContent %}
                {% endblock bodyContent %}
            &amp;lt;/body&amp;gt;
        &amp;lt;/html&amp;gt;
    {% endminify %}
{%- endblock htmlPage -%}

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



&lt;p&gt;Since this is a base tem­plate that all oth­er web pages inher­it from, if we want­ed to do full page caching using the &lt;a href="https://dev.to/gaijinity/the-craft-cache-tag-in-depth-207b-temp-slug-8529389"&gt;Craft Cache tag&lt;/a&gt;, we could wrap that around the &lt;code&gt;{% minify %}&lt;/code&gt; tags here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BMlcjwZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x547_crop_center-center_100_line/blocks-base-web-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BMlcjwZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x547_crop_center-center_100_line/blocks-base-web-layout.png" alt="Blocks base web layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The base-web-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
htmlTag — the &amp;lt;html&amp;gt; tag, which child tem­plates might need to override&lt;/li&gt;
&lt;li&gt;
headTag — the &amp;lt;head&amp;gt; tag, which child tem­plates might need to override&lt;/li&gt;
&lt;li&gt;
headContent — what­ev­er tags need to go into the &amp;lt;head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
bodyTag — the &amp;lt;body&amp;gt; tag, which child tem­plates might need to override&lt;/li&gt;
&lt;li&gt;
bodyContent — what­ev­er tags need to go in the &amp;lt;body&amp;gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addi­tion, the &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/templates/_boilerplate/_partials/head-meta.twig"&gt;_boilerplate/_partials/head-meta.twig&lt;/a&gt; par­tial that con­tains boil­er­plate tags put into the &amp;lt;head&amp;gt; is includ­ed here as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  BOIL­ER­PLATE: base-ajax-layout.twig
&lt;/h2&gt;

&lt;p&gt;If the request is an AJAX / XHR request, we want to return just the page’s &lt;code&gt;{% content %}&lt;/code&gt; block, with­out any of the web page ​“chrome” around it.&lt;/p&gt;

&lt;p&gt;This is exact­ly what this tem­plate does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Base layout template that all AJAX requests inherit from -- #}
{% extends "_layouts/global-variables.twig" %}

{% block htmlPage %}
    {% minify %}
        {# -- Primary content block -- #}
        {% block content %}
            &amp;lt;code&amp;gt;No content block defined.&amp;lt;/code&amp;gt;
        {% endblock content %}
    {% endminify %}
{% endblock htmlPage %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5qpQS4U3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x228_crop_center-center_100_line/blocks-base-ajax-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5qpQS4U3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x228_crop_center-center_100_line/blocks-base-ajax-layout.png" alt="Blocks base ajax layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The base-ajax-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
content — the core con­tent that is rep­re­sent­ed on the page&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  BOIL­ER­PLATE: base-html-layout.twig
&lt;/h2&gt;

&lt;p&gt;This is the base HTML lay­out that all HTML requests inher­it from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Base HTML layout template that all HTML requests inherit from -- #}
{% extends craft.app.request.isAjax() and not craft.app.request.getIsPreview()
    ? "_boilerplate/_layouts/base-ajax-layout.twig"
    : "_boilerplate/_layouts/base-web-layout.twig"
%}

{% block htmlTag %}
    &amp;lt;html class="fonts-loaded" lang="{{ craft.app.language |slice(0,2) }}" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#"&amp;gt;
{% endblock htmlTag %}

{# -- Page content that should be included in the &amp;lt;head&amp;gt; -- #}
{% block headContent %}
    {# -- Any &amp;lt;meta&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
    {% block headMeta %}
    {% endblock headMeta %}

    {# -- Any &amp;lt;link&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
    {% block headLinks %}
    {% endblock headLinks %}

    {# -- Inline and polyfill JS #}
    {% include "_boilerplate/_partials/head-js.twig" %}

    {# -- Any JavaScript that should be included before &amp;lt;/head&amp;gt; -- #}
    {% block headJs %}
    {% endblock headJs %}

    {# -- Inline and critical CSS #}
    &amp;lt;style&amp;gt;
        [v-cloak] {display: none !important;}
        {# -- Any CSS that should be included before &amp;lt;/head&amp;gt; -- #}
        {% block headCss %}
        {% endblock headCss %}
    &amp;lt;/style&amp;gt;
    {% include "_boilerplate/_partials/critical-css.twig" %}

{% endblock headContent %}

{# -- Page content that should be included in the &amp;lt;body&amp;gt; -- #}
{% block bodyContent %}
    {# -- Page content that should be included in the &amp;lt;body&amp;gt; -- #}
    {% block bodyHtml %}
    {% endblock bodyHtml %}

    {#-- Site-wide JavaScript --#}
    {{ craft.twigpack.includeSafariNomoduleFix() }}
    {{ craft.twigpack.includeJsModule("app.js", true) }}
    {{ craft.twigpack.includeJsModule("styles.js", true) }}

    {# -- Any JavaScript that should be included before &amp;lt;/body&amp;gt; -- #}
    {% block bodyJs %}
    {% endblock bodyJs %}
{% endblock bodyContent %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xkWGmWlE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x587_crop_center-center_100_line/blocks-base-html-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xkWGmWlE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x587_crop_center-center_100_line/blocks-base-html-layout.png" alt="Blocks base html layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The base-html-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
headMeta — Any &amp;lt;meta&amp;gt; tags that should be includ­ed in the &amp;lt;head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
headLinks — Any &amp;lt;link&amp;gt; tags that should be includ­ed in the &amp;lt;head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
headJs — Any JavaScript that should be includ­ed before &amp;lt;/head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
headCss — Any CSS that should be includ­ed before &amp;lt;/head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
bodyHtml — Page con­tent that should be includ­ed in the &amp;lt;body&amp;gt;
&lt;/li&gt;
&lt;li&gt;
bodyJs — Any JavaScript that should be includ­ed before &amp;lt;/body&amp;gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addi­tion, the &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/templates/_boilerplate/_partials/head-js.twig"&gt;_boilerplate/_partials/head-js.twig&lt;/a&gt; &amp;amp; &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/templates/_boilerplate/_partials/critical-css.twig"&gt;_boilerplate/_partials/critical-css.twig&lt;/a&gt; boil­er­plate par­tials are includ­ed here as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  BOIL­ER­PLATE: amp-base-html-layout.twig
&lt;/h2&gt;

&lt;p&gt;This is the base AMP HTML lay­out that all AMP HTML requests inher­it from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Base AMP HTML layout template that AMP web requests inherit from -- #}
{% extends craft.app.request.isAjax() and not craft.app.request.getIsPreview()
    ? "_boilerplate/_layouts/base-ajax-layout.twig"
    : "_boilerplate/_layouts/base-web-layout.twig"
%}

{% do seomatic.script.container().include(false) %}
{% do craft.webperf.includeBeacon(false) %}

{% block htmlTag %}
    &amp;lt;html ⚡ lang="{{ craft.app.language |slice(0,2) }}" class="fonts-loaded"&amp;gt;
{% endblock htmlTag %}

{# -- Page content that should be included in the &amp;lt;head&amp;gt; -- #}
{% block headContent %}
    {# -- Any &amp;lt;meta&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
    {% block headMeta %}
    {% endblock headMeta %}

    {# -- Any &amp;lt;link&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
    {% block headLinks %}
    {% endblock headLinks %}

    {# -- Google AMP JavaScripts #}
    {% include "_boilerplate/_partials/amp-head-js.twig" %}

    {# -- Any JavaScript that should be included before &amp;lt;/head&amp;gt; -- #}
    {% block headJs %}
    {% endblock headJs %}

    {# -- Boilerplate &amp;amp; custom AMP CSS #}
    {% include "_boilerplate/_partials/amp-boilerplate-css.twig" %}
    &amp;lt;style amp-custom&amp;gt;
    {# -- Any CSS that should be included before &amp;lt;/head&amp;gt; -- #}
    {% block headCss %}
    {% endblock headCss %}
    &amp;lt;/style&amp;gt;
{% endblock headContent %}

{# -- Page content that should be included in the &amp;lt;body&amp;gt; -- #}
{% block bodyContent %}
    {# -- Page content that should be included in the &amp;lt;body&amp;gt; -- #}
    {% block bodyHtml %}
    {% endblock bodyHtml %}

    {# -- AMP Analytics --#}
    {% include "_boilerplate/_partials/amp-analytics.twig" %}

    {# -- Any JavaScript that should be included before &amp;lt;/body&amp;gt; -- #}
    {% block bodyJs %}
    {% endblock bodyJs %}
{% endblock bodyContent %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bSqY2EfR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x587_crop_center-center_100_line/blocks-amp-base-html-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bSqY2EfR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x587_crop_center-center_100_line/blocks-amp-base-html-layout.png" alt="Blocks amp base html layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The amp-base-html-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
headMeta — Any &amp;lt;meta&amp;gt; tags that should be includ­ed in the &amp;lt;head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
headLinks — Any &amp;lt;link&amp;gt; tags that should be includ­ed in the &amp;lt;head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
headJs — Any JavaScript that should be includ­ed before &amp;lt;/head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
headCss — Any CSS that should be includ­ed before &amp;lt;/head&amp;gt;
&lt;/li&gt;
&lt;li&gt;
bodyHtml — Page con­tent that should be includ­ed in the &amp;lt;body&amp;gt;
&lt;/li&gt;
&lt;li&gt;
bodyJs — Any JavaScript that should be includ­ed before &amp;lt;/body&amp;gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;N.B.:&lt;/strong&gt; these blocks are all pur­pose­ful­ly the same as the ones used in the base-html-layout.twig template.&lt;/p&gt;

&lt;p&gt;In addi­tion, the &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/templates/_boilerplate/_partials/amp-head-js.twig"&gt;_boilerplate/_partials/amp-head-js.twig&lt;/a&gt;, &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/templates/_boilerplate/_partials/amp-boilerplate-css.twig"&gt;_boilerplate/_partials/amp-boilerplate-css.twig&lt;/a&gt; &amp;amp; &lt;a href="https://github.com/nystudio107/craft/blob/craft-webpack/templates/_boilerplate/_partials/amp-analytics.twig"&gt;amp-analytics.twig&lt;/a&gt; boil­er­plate par­tials are includ­ed here as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  PROJECT: generic-page-layout.twig
&lt;/h2&gt;

&lt;p&gt;This is a gener­ic page lay­out that I’ve found suits most of the projects I build, and my oth­er tem­plates extends it.&lt;/p&gt;

&lt;p&gt;For sim­i­lar pages, I can even extend this lay­out to get every­thing it offers, plus what I need for anoth­er sub­set of pages. For exam­ple, see the generic-page-layout.twig below.&lt;/p&gt;

&lt;p&gt;How­ev­er, if I have oth­er pages that require rad­i­cal­ly dif­fer­ent lay­outs, I’ll just cre­ate anoth­er lay­out tem­plate that extends _boilerplate/_layouts/base-html-layout.twig and away we go!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Layout template for HTML pages -- #}
{% extends "_boilerplate/_layouts/base-html-layout.twig" %}

{# -- Any &amp;lt;meta&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
{% block headMeta %}
{% endblock headMeta %}

{# -- Any &amp;lt;link&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
{% block headLinks %}
{% endblock headLinks %}

{# -- Any CSS that should be included before &amp;lt;/head&amp;gt; -- #}
{% block headCss %}
    {% include "_inline-css/site-fonts.css" %}
{% endblock headCss %}

{# -- Page body -- #}
{% block bodyHtml %}
    &amp;lt;div id="page-container" class="overflow-hidden leading-tight"&amp;gt;
        &amp;lt;confetti&amp;gt;&amp;lt;/confetti&amp;gt;
        &amp;lt;div id="content-container" class="bg-repeat header-background"&amp;gt;

            {# -- Info header, including _navbar.twig -- #}
            {% include "_partials/info-header.twig" %}

            &amp;lt;main&amp;gt;
                &amp;lt;div class="container mx-auto pb-8"&amp;gt;
                    {# -- Primary content block -- #}
                    {% block content %}
                    {% endblock %}
                &amp;lt;/div&amp;gt;
            &amp;lt;/main&amp;gt;
        &amp;lt;/div&amp;gt;

        {# -- Content that appears below the primary content block -- #}
        {% block subcontent %}
        {% endblock %}

        {# -- Info footer -- #}
        {% include "_partials/info-footer.twig" %}

        {# -- HTML Footer -- #}
        {% include "_partials/global-footer.twig" %}
    &amp;lt;/div&amp;gt;
{% endblock bodyHtml %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oHual9c7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x348_crop_center-center_100_line/blocks-generic-page-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oHual9c7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x348_crop_center-center_100_line/blocks-generic-page-layout.png" alt="Blocks generic page layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generic-page-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
content — Pri­ma­ry con­tent block&lt;/li&gt;
&lt;li&gt;
subContent — Con­tent that appears below the pri­ma­ry con­tent block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markup in the subContent block will appear on web pages, but not on pages loaded in via AJAX / XHR.&lt;/p&gt;

&lt;p&gt;In addi­tion, it includes a few par­tials for the head­er, foot­er, etc., but you can have it do what­ev­er makes the most sense to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  PROJECT: error-page-layout.twig
&lt;/h2&gt;

&lt;p&gt;Here we fur­ther extends the generic-page-layout.twig with anoth­er lay­out tem­plate that’s specif­i­cal­ly intend­ed for error pages.&lt;/p&gt;

&lt;p&gt;Because we have a num­ber of dif­fer­ent error pages that dis­play dif­fer­ent con­tent, but have the same basic lay­out, this is the per­fect oppor­tu­ni­ty to con­sol­i­date them in anoth­er lay­out template.&lt;/p&gt;

&lt;p&gt;Instead of repli­cat­ing the con­tent for each error page, we can have the error pages extends error-page-layout.twig and have very light­weight error pages.&lt;/p&gt;

&lt;p&gt;The same idea of an inher­i­tance chain can be used in sim­i­lar situations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Layout template for error pages -- #}
{% extends "_layouts/generic-page-layout.twig" %}

{% block content %}
{% endblock %}

{% block subcontent %}
    &amp;lt;section&amp;gt;
        &amp;lt;div class="container mx-auto py-8"&amp;gt;
            &amp;lt;div class="text-center p-8 mb-8"&amp;gt;
                &amp;lt;h1 class="font-mono italic font-bold text-5xl pt-4"&amp;gt;
                    {{ entry.errorHeadline ?? 'Error' }}
                &amp;lt;/h1&amp;gt;
                &amp;lt;p class="font-sans text-xl pt-4"&amp;gt;
                    {{ (entry.errorText ?? 'An error has occurred.') |nl2br }}
                &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;

{% endblock %}

{# -- Any JavaScript that should be included before &amp;lt;/body&amp;gt; -- #}
{% block bodyJs %}
{% endblock bodyJs %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oHual9c7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x348_crop_center-center_100_line/blocks-generic-page-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oHual9c7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x348_crop_center-center_100_line/blocks-generic-page-layout.png" alt="Blocks generic page layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The error-page-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
content — Pri­ma­ry con­tent block&lt;/li&gt;
&lt;li&gt;
subContent — Con­tent that appears below the pri­ma­ry con­tent block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markup in the subContent block will appear on web pages, but not on pages loaded in via AJAX / XHR.&lt;/p&gt;

&lt;h2&gt;
  
  
  PROJECT: amp-generic-page-layout.twig
&lt;/h2&gt;

&lt;p&gt;This is the Google AMP gener­ic page tem­plate, which mir­rors the blocks and method­ol­o­gy from the generic-page-layout.twig tem­plate, but is sep­a­rat­ed out to allow for the unique tags that Google AMP requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{# -- Layout template for AMP HTML pages -- #}
{% extends "_boilerplate/_layouts/amp-base-html-layout.twig" %}

{# -- Any &amp;lt;meta&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
{% block headMeta %}
{% endblock headMeta %}

{# -- Any &amp;lt;link&amp;gt; tags that should be included in the &amp;lt;head&amp;gt; #}
{% block headLinks %}
{% endblock headLinks %}

{# -- Any JavaScript that should be included before &amp;lt;/head&amp;gt; -- #}
{% block headJs %}
{% endblock headJs %}

{# -- Any CSS that should be included before &amp;lt;/head&amp;gt; -- #}
{% block headCss %}
    {% include "_partials/amp-inline-css.css" %}
    {% include "_inline-css/site-fonts.css" %}
{% endblock %}

{# -- Page body -- #}
{% block bodyHtml %}
    {% include "_partials/amp-navbar.twig" %}
    &amp;lt;div id="page-container" class="overflow-hidden leading-tight"&amp;gt;
        &amp;lt;div id="content-container" class="bg-repeat header-background"&amp;gt;
            {# -- Info header, including _navbar.twig -- #}
            {% include "_partials/amp-info-header.twig" %}

            &amp;lt;main&amp;gt;
                &amp;lt;div class="container mx-auto pb-8"&amp;gt;
                    {# -- Primary content block -- #}
                    {% block content %}
                    {% endblock %}
                &amp;lt;/div&amp;gt;
            &amp;lt;/main&amp;gt;
        &amp;lt;/div&amp;gt;

        {# -- Content that appears below the primary content block -- #}
        {% block subcontent %}
        {% endblock %}

        {# -- Info footer -- #}
        {% include "_partials/amp-info-footer.twig" %}

        {# -- HTML Footer -- #}
        {% include "_partials/global-footer.twig" %}
    &amp;lt;/div&amp;gt;
{% endblock bodyHtml %}

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



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oHual9c7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x348_crop_center-center_100_line/blocks-generic-page-layout.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oHual9c7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x348_crop_center-center_100_line/blocks-generic-page-layout.png" alt="Blocks generic page layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The amp-generic-page-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
content — Pri­ma­ry con­tent block&lt;/li&gt;
&lt;li&gt;
subContent — Con­tent that appears below the pri­ma­ry con­tent block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markup in the subContent block will appear on web pages, but not on pages loaded in via AJAX / XHR.&lt;/p&gt;

&lt;h2&gt;
  
  
  dev​Mode​.fm page: a real world example
&lt;/h2&gt;

&lt;p&gt;So how does this all look with a real world exam­ple? Why, I’m glad you asked! Let’s have a look at the &lt;a href="https://github.com/nystudio107/devmode"&gt;dev​Mode​.fm&lt;/a&gt; home page, which extends generic-page-layout.twig:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% extends "_layouts/generic-page-layout.twig" %}

{% set includeAudioMeta = false %}

{% block headLinks %}
    {{ parent() }}
    &amp;lt;link rel="amphtml" href="{{ siteUrl('/amp') }}"&amp;gt;
{% endblock headLinks %}

{% block content %}
    {% include "_partials/_meta-schema-radio-series.twig" with {
        "showInfo": showInfo,
    } only %}
    &amp;lt;section&amp;gt;
        &amp;lt;div&amp;gt;
            {% for episode in craft.entries.section("episodes").limit(1).all() %}
                &amp;lt;div class="flex flex-wrap"&amp;gt;
                    {% include "episodes/_partials/_display_episode.twig" with {
                        "episode": episode,
                        "showInfo": showInfo,
                        "includeAudioMeta": includeAudioMeta,
                        "autoPlay": false,
                    } only %}
                &amp;lt;/div&amp;gt;
            {% endfor %}
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;
{% endblock %}

{% block subcontent %}
    {% include "episodes/_partials/_display_recent_episodes.twig" with {
        "showInfo": showInfo,
    } only %}
{% endblock %}

{# -- Any JavaScript that should be included before &amp;lt;/body&amp;gt; -- #}
{% block bodyJs %}
    {{ craft.twigpack.includeJsModule("player.js", true) }}
    {{ craft.twigpack.includeJsModule("episodes.js", true) }}
{% endblock bodyJs %}

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



&lt;p&gt;You can com­pare this to the &lt;a href="https://devmode.fm/"&gt;dev​Mode​.fm ren­dered home page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The index.twig tem­plate over­rides just four blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
headLinks — Here we add a link to point browsers at the Google AMP ver­sion of this page&lt;/li&gt;
&lt;li&gt;
content — The con­tent of this page, in this case the cur­rent episode sum­ma­ry &amp;amp; audio player&lt;/li&gt;
&lt;li&gt;
subContent — The episodes list­ing com­po­nent, dis­played under the content&lt;/li&gt;
&lt;li&gt;
bodyJs — Adds some JavaScript to han­dle the play­er &amp;amp; episodes list­ing to the page, cour­tesy of &lt;a href="https://nystudio107.com/plugins/twigpack"&gt;Twig­pack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see that this makes the actu­al tem­plates that we write pret­ty clean. And if this page was ever request­ed via AJAX / XHR, it’d return &lt;em&gt;just&lt;/em&gt; the content block.&lt;/p&gt;

&lt;p&gt;The Google AMP ver­sion of the home­page tem­plate is very similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{% extends "_layouts/amp-generic-page-layout.twig" %}

{% if entry is not defined %}
    {% set entry = craft.entries({
        "uri": " __home__",
    }).one() %}
{% endif %}

{% do seomatic.helper.loadMetadataForUri(entry.uri) %}
{% do seomatic.script.container().include(false) %}

{% block headCss %}
    {{ parent() }}
    {{ craft.twigpack.includeFile("@webroot/dist/criticalcss/amp_index_critical.min.css") }}
{% endblock headCss %}

{% block content %}
    &amp;lt;section&amp;gt;
        &amp;lt;div&amp;gt;
            {% for episode in craft.entries.section("episodes").limit(1).all() %}
                &amp;lt;div class="flex flex-wrap"&amp;gt;
                    {% include "episodes/_partials/_amp_display_episode.twig" with {
                        "episode": episode,
                        "showInfo": showInfo,
                    } only %}
                &amp;lt;/div&amp;gt;
            {% endfor %}
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;
{% endblock %}

{% block subcontent %}
    {% include "episodes/_partials/_amp_display_recent_episodes.twig" with {
        "showInfo": showInfo,
    } only %}
{% endblock %}

{% block bodyJs %}
{% endblock bodyJs %}

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



&lt;p&gt;It’s explic­it­ly load­ing the appro­pri­ate entry, because it won’t be auto-inject­ed for us by Craft, and then it loads the appro­pri­ate meta­da­ta for the route via seomatic.helper.loadMetadataForUri() and excludes all scripts via seomatic.script.container().include(false) because Google AMP does­n’t allow for them.&lt;/p&gt;

&lt;p&gt;It’s also using Twig­pack to include the full CSS for the page inline (as per Google AMP spec) but oth­er than that… it’s the same as the reg­u­lar web page example.&lt;/p&gt;

&lt;h2&gt;
  
  
  All about that Bass
&lt;/h2&gt;

&lt;p&gt;While you cer­tain­ly could just start using my boil­er­plate, odds are good you’ll want to cus­tomize some things to suit your tastes.&lt;/p&gt;

&lt;p&gt;That’s total­ly fine. What’s impor­tant is the struc­ture and method­ol­o­gy, not the spe­cif­ic imple­men­ta­tion details.&lt;/p&gt;

&lt;p&gt;The point of a mod­u­lar­ized sys­tem like this is that if you want­ed to add, say, a way to out­put the same con­tent in JSON for­mat, you could. Just slap in anoth­er lay­out in the right place, and away you go.&lt;/p&gt;

&lt;p&gt;Enjoy the oblig­a­tory ​“All About That Bass” and have an excel­lent day!&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/nystudio107/devmode"&gt;dev​Mode​.fm web­site Github repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nystudio107/craft"&gt;nystudio107/​craft boil­er­plate setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>craftcms</category>
      <category>twig</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Using Tailwind CSS with Gatsby, React &amp; Emotion Styled Components</title>
      <dc:creator>Andrew Welch</dc:creator>
      <pubDate>Sun, 20 Oct 2019 14:08:00 +0000</pubDate>
      <link>https://dev.to/gaijinity/using-tailwind-css-with-gatsby-react-emotion-styled-components-53h</link>
      <guid>https://dev.to/gaijinity/using-tailwind-css-with-gatsby-react-emotion-styled-components-53h</guid>
      <description>&lt;h1&gt;
  
  
  Using Tailwind CSS with Gatsby, React &amp;amp; Emotion Styled Components
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Learn how to use the util­i­ty-first Tail­wind CSS with Emo­tion ​“CSS-in-JS” Styled Com­po­nents in a Gats­by JS + React project.
&lt;/h3&gt;

&lt;p&gt;Andrew Welch / &lt;a href="https://nystudio107.com"&gt;nystudio107&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PBAGHPlX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/4124/gatsby-react-emotion-styled-components.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PBAGHPlX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/4124/gatsby-react-emotion-styled-components.jpg" alt="Gatsby react emotion styled components"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tailwindcss.com/"&gt;Tail­wind CSS&lt;/a&gt; is a util­i­ty-first CSS frame­work that allows for rapid­ly build­ing cus­tom designs, and it is some­thing I’ve adopt­ed as a stan­dard part of my workflow.&lt;/p&gt;

&lt;p&gt;If you want to hear the why’s and how’s of Tail­wind CSS, check out the &lt;a href="https://devmode.fm/episodes/tailwind-css-utility-first-css-with-adam-watham"&gt;Tail­wind CSS Util­i­ty-First CSS dev​Mode​.fm pod­cast episode&lt;/a&gt;. For the pur­pos­es of this arti­cle, we’ll just assume you’re all on-board the Tail­wind Express like I am.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xYaEuE5G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/tailwind-utility-first-css-express.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xYaEuE5G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/tailwind-utility-first-css-express.jpg" alt="Tailwind utility first css express"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since I’m a fan of Tail­wind CSS, when I start­ed work­ing with &lt;a href="https://www.gatsbyjs.org/"&gt;Gats­by&lt;/a&gt; &amp;amp; &lt;a href="https://reactjs.org/"&gt;React&lt;/a&gt;, I want­ed a way to bring Tail­wind with me. I end­ed up decid­ing to go with the &lt;a href="https://emotion.sh/docs/introduction"&gt;Emo­tion CSS-in-JS&lt;/a&gt; approach, specif­i­cal­ly using &lt;a href="https://emotion.sh/docs/styled"&gt;Styled Com­po­nents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Again, we’ll just assume you’re on-board with Emo­tion CSS-in-JS; if not, check out the &lt;a href="https://devmode.fm/episodes/css-in-js-an-emotional-topic"&gt;CSS in JS, an Emo­tion­al Top­ic dev​Mode​.fm pod­cast episode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This arti­cle dis­cuss­es how to make all of these tech­nolo­gies play nice togeth­er, and explores some of the fun things the result­ing stack enables you to do.&lt;/p&gt;

&lt;p&gt;If you want a head start on this set­up, check out Paulo Elias’s &lt;a href="https://github.com/pauloelias/gatsby-tailwind-emotion-starter"&gt;gats­by-tail­wind-emo­tion-starter&lt;/a&gt; to get you going. Oth­er­wise, buck­le up and let’s dive right in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are we doing this?
&lt;/h2&gt;

&lt;p&gt;If you haven’t worked with &lt;a href="https://spin.atomicobject.com/2018/12/28/css-in-javascript-benefits/"&gt;CSS-in-JS&lt;/a&gt; or Styled Com­po­nents before, there are some nice advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can use full-blown JavaScript&lt;/li&gt;
&lt;li&gt;CSS is scoped to just the com­po­nent, and does­n’t ​“bleed out”&lt;/li&gt;
&lt;li&gt;You auto­mat­i­cal­ly get just the CSS used on each page&lt;/li&gt;
&lt;li&gt;You auto­mat­i­cal­ly get Crit­i­cal CSS&lt;/li&gt;
&lt;li&gt;The end result is just CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may or may not be con­vinced that CSS-in-JS is a good idea, but these are tan­gi­ble ben­e­fits that I’ve found, and thus my desire to use CSS-in-JS and Styled Components.&lt;/p&gt;

&lt;p&gt;Adding Tail­wind CSS to the mix brings all of the won­der­ful ben­e­fits of a utilty-first CSS frame­work like Tail­wind CSS to our Styled Components.&lt;/p&gt;

&lt;p&gt;So with that said, let’s get to it!&lt;/p&gt;

&lt;h2&gt;
  
  
  A Hybrid Approach
&lt;/h2&gt;

&lt;p&gt;The excel­lent &lt;a href="https://www.gatsbyjs.org/docs/"&gt;Gats­by doc­u­men­ta­tion&lt;/a&gt; has two approach­es list­ed for using Tail­wind CSS with Gatsby:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gatsbyjs.org/docs/tailwind-css/#option-1-postcss"&gt;Option #1: PostCSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gatsbyjs.org/docs/tailwind-css/#option-2-css-in-js"&gt;Option #2: CSS-in-JS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re actu­al­ly going to use a hybrid approach, using both the &lt;a href="https://github.com/bradlc/babel-plugin-tailwind-components"&gt;tailwind.macro&lt;/a&gt; &lt;a href="https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros"&gt;Babel Macro&lt;/a&gt; and &lt;a href="https://postcss.org/"&gt;PostC­SS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’re going to use the tailwind.macro to trans­late Tail­wind CSS class­es to Emo­tion Styled Com­po­nents for us, gen­er­at­ing just the CSS selec­tors we actu­al­ly use.&lt;/p&gt;

&lt;p&gt;Then we’ll use PostC­SS for build­ing the &lt;a href="https://tailwindcss.com/docs/adding-base-styles/"&gt;Tail­wind CSS base styles&lt;/a&gt; (and any base glob­al styles we want to use) that will be applied glob­al­ly to every page. This gives us things like the &lt;a href="https://github.com/necolas/normalize.css/"&gt;Nor­mal­ize CSS reset&lt;/a&gt; as a base.&lt;/p&gt;

&lt;p&gt;This is the rea­son for the hybrid approach: if we &lt;em&gt;just&lt;/em&gt; used the tailwind.macro, we would­n’t get any of the Tail­wind CSS base styles.&lt;/p&gt;

&lt;p&gt;You could also use the &lt;a href="https://github.com/talensjr/gatsby-theme-tailwindcss"&gt;gats­by-theme-tail­wind­c­ss&lt;/a&gt; Gats­by Theme as a way to scaf­fold things, but I think it makes sense to under­stand how all of the pieces fit togeth­er first.&lt;/p&gt;

&lt;p&gt;So let’s get going by installing Tail­wind CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Using npm
npm install --save tailwindcss

# Using Yarn
yarn add tailwindcss

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



&lt;p&gt;Next up, let’s get tailwind.macro set up!&lt;/p&gt;

&lt;h2&gt;
  
  
  Set­ting up tailwind.macro
&lt;/h2&gt;

&lt;p&gt;The tailwind.macro is a Babel Macro that allows you to use Tail­wind CSS with any CSS-in-JS library. We’ve cho­sen to use Emo­tion, so let’s get it all installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# Using npm
npm install --save @emotion/core @emotion/styled gatsby-plugin-emotion tailwind.macro@next

# Using Yarn
yarn add @emotion/core @emotion/styled gatsby-plugin-emotion tailwind.macro@next

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



&lt;p&gt;Then we need to add the gatsby-plugin-emotion to our gatsby-config.js (in the project root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-emotion`,
      options: {
        // Accepts all options defined by `babel-plugin-emotion` plugin.
      },
    },
  ],
}

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



&lt;p&gt;Then we need to cre­ate a babel-plugin-macros-config.js to tell it that we want to use Emo­tion, and where our tailwind.config.js file lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
module.exports = {
    tailwind: {
        styled: '@emotion/styled',
        config: './tailwind.config.js',
        format: 'auto',
    }
};

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



&lt;p&gt;Final­ly, there’s a &lt;a href="https://github.com/bradlc/babel-plugin-tailwind-components/issues/39"&gt;small issue&lt;/a&gt; we need to work around, which appears to be due to Tail­wind CSS’s use of reduce-css-calc start­ing with Tailwind ^1.1.0, which appar­ent­ly depends on it being &lt;a href="https://www.gitmemory.com/issue/TVke/react-native-tailwindcss/7/523364996"&gt;run via Node&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We can work around this by adding the fol­low­ing to our gatsby-node.js file (in the project root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
exports.onCreateWebpackConfig = ({actions, getConfig}) =&amp;gt; {
    // Hack due to Tailwind ^1.1.0 using `reduce-css-calc` which assumes node
    // https://github.com/bradlc/babel-plugin-tailwind-components/issues/39#issuecomment-526892633
    const config = getConfig();
    config.node = {
        fs: 'empty'
    };
};

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



&lt;h2&gt;
  
  
  Using tailwind.macro
&lt;/h2&gt;

&lt;p&gt;Now that we’ve got tailwind.macro installed, let’s have a look at how we can use it. It works very sim­i­lar to &lt;a href="https://emotion.sh/docs/styled"&gt;Emo­tion Styled Com­po­nents&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import React from 'react';
import tw from 'tailwind.macro';

const PageContainer = tw.div`
    bg-gray-200 text-xl w-1/2
`;

const Layout = ({children}) =&amp;gt; (
    &amp;lt;PageContainer&amp;gt;
        {children}
    &amp;lt;/PageContainer&amp;gt;
);

export default Layout;

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



&lt;p&gt;This will cre­ate a Styled Com­po­nent that is com­posed of the styles from the Tail­wind CSS class­es list­ed in the tem­plate lit­er­al. There’s a &lt;a href="https://github.com/bradlc/babel-plugin-tailwind-components/issues/4"&gt;Github issue&lt;/a&gt; that shows a good exam­ple of how this works.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;devel­op­ment&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import tw from 'tailwind.macro'
let styles = tw`w-1/2`

// ↓↓↓↓↓↓↓↓

import _tailwind from './path/to/your/tailwind.js'
let styles = {
  width: _tailwind.widths['1/2']
}

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



&lt;p&gt;In &lt;strong&gt;pro­duc­tion&lt;/strong&gt; (NODE_ENV=production):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import tw from 'tailwind.macro'
let styles = tw`w-1/2`

// ↓↓↓↓↓↓↓↓

let styles = {
  width: '50%'
}

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



&lt;p&gt;You can see how it direct­ly uses the JavaScript Tail­wind CSS con­fig to extract styles. The only real down­side to this approach is that it will not work with &lt;a href="https://tailwindcss.com/docs/plugins/"&gt;Tail­wind Plu­g­ins&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Build­ing on the ini­tial exam­ple above, we can mix and match Emo­tion Styled Com­po­nents cus­tom CSS with Tail­wind class­es easily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import React from 'react';
import styled from '@emotion/styled';
import tw from 'tailwind.macro';

import background from '../../static/fabric_plaid@2x.png';

const PageContainer = styled.div`
    ${tw`
        bg-gray-200 text-xl w-1/2
    `}
    background-image: url(${background});
    padding: 10px;
`;

const Layout = ({children}) =&amp;gt; (
    &amp;lt;PageContainer&amp;gt;
        {children}
    &amp;lt;/PageContainer&amp;gt;
);

export default Layout;

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



&lt;p&gt;So it actu­al­ly &lt;em&gt;com­bines&lt;/em&gt; the CSS styles from the Tail­wind CSS class­es in the ${tw` tem­plate lit­er­al with the cus­tom styled com­po­nent CSS below it. So you can do any of the fun pat­terns you do with &lt;a href="https://emotion.sh/docs/styled"&gt;Emo­tion Styled Com­po­nents&lt;/a&gt;, too!&lt;/p&gt;

&lt;p&gt;In either case, only the CSS that is actu­al­ly used on a page will be extract­ed (no need for &lt;a href="https://tailwindcss.com/docs/controlling-file-size/"&gt;PurgeC­SS&lt;/a&gt;), and the styles used by com­po­nents on a page will be inlined, appear­ing some­thing like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.css-14xcvvf-PageContainer {
    background-color:#edf2f7;
    font-size:1.25rem;
    width:50%;
    background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQBAMAAABykSv/AAAAG1BMVEXq6urr6+vp6eno6Ojn5+fs7Ozt7e3m5ubu7u7dMibkAAAf6ElEQVR4AdyX4W3jSgyEp4WvhWmBLaiFaWFbeC2k7AfNSoICHHC4vyISO8vlfOQAdGxL2PaMx);
    padding:10px;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The hashed CSS class name ensures that the styles are scoped to just the com­po­nent they are applied to, and will not leak out and affect any­thing else.&lt;/p&gt;

&lt;p&gt;Sweet!&lt;/p&gt;

&lt;h2&gt;
  
  
  Set­ting up PostCSS
&lt;/h2&gt;

&lt;p&gt;If we just left things as-is, using only the tailwind.macro, every­thing would still work, but we would­n’t get the Tail­wind CSS base styles to do things like apply a Nor­mal­ize CSS style reset, and oth­er glob­al styles.&lt;/p&gt;

&lt;p&gt;Because these base styles are super use­ful, we’ll con­fig­ure PostC­SS to gen­er­ate them for us. So let’s install the PostC­SS pack­ages we’re going to use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;h1&gt;
  
  
  Using npm
&lt;/h1&gt;

&lt;p&gt;npm install --save gatsby-plugin-postcss postcss-import postcss-preset-env stylelint&lt;/p&gt;

&lt;h1&gt;
  
  
  Using Yarn
&lt;/h1&gt;

&lt;p&gt;yarn add gatsby-plugin-postcss postcss-import postcss-preset-env stylelint&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then we need to add the gatsby-plugin-postcss to our gatsby-config.js (in the project root):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;module.exports = {&lt;br&gt;
  plugins: [&lt;br&gt;
    {&lt;br&gt;
      resolve: &lt;code&gt;gatsby-plugin-emotion&lt;/code&gt;,&lt;br&gt;
      options: {&lt;br&gt;
        // Accepts all options defined by &lt;code&gt;babel-plugin-emotion&lt;/code&gt; plugin.&lt;br&gt;
      },&lt;br&gt;
    },&lt;br&gt;
    {&lt;br&gt;
      resolve: &lt;code&gt;gatsby-plugin-postcss&lt;/code&gt;,&lt;br&gt;
      options: {&lt;br&gt;
        // Accepts all options defined by &lt;code&gt;gatsby-plugin-postcss&lt;/code&gt; plugin.&lt;br&gt;
      },&lt;br&gt;
    },&lt;br&gt;
  ],&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We’ll also need to cre­ate a postcss.config.js file (in the project root) to tell it what PostC­SS plu­g­ins we want to use (includ­ing tailwindcss):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;module.exports = {&lt;br&gt;
    plugins: [&lt;br&gt;
        require('postcss-import')({&lt;br&gt;
            plugins: [&lt;br&gt;
                require('stylelint')&lt;br&gt;
            ]&lt;br&gt;
        }),&lt;br&gt;
        require('tailwindcss')('./tailwind.config.js'),&lt;br&gt;
        require('postcss-preset-env')({&lt;br&gt;
            autoprefixer: { grid: true },&lt;br&gt;
            features: {&lt;br&gt;
                'nesting-rules': true&lt;br&gt;
            },&lt;br&gt;
            browsers: [&lt;br&gt;
                '&amp;gt; 1%',&lt;br&gt;
                'last 2 versions',&lt;br&gt;
                'Firefox ESR',&lt;br&gt;
            ]&lt;br&gt;
        })&lt;br&gt;
    ]&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The postcss.config.js file is used by PostC­SS to con­fig­ure the plu­g­ins and set­tings that PostC­SS will use. The impor­tant bit here is that we’re doing a require('tailwindcss') to include Tail­wind CSS.&lt;/p&gt;

&lt;p&gt;The oth­er PostC­SS plu­g­ins we’re includ­ing here are just things that I find use­ful. You can read more about them in-depth in the &lt;a href="https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-5646-temp-slug-4335831"&gt;An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Then we need to cre­ate a file for the Tail­wind CSS base styles, as well as any glob­al styles we might want to add in src/utils/globals.css:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;@tailwind base;&lt;/p&gt;

&lt;p&gt;// Add any global styles here&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Final­ly, we need to load these styles in the gatsby-browser.js (in the project root):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;import "./src/utils/globals.css"&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That’s it! When we do a build, it’ll now gen­er­ate the Tail­wind CSS base styles, and any of our glob­al styles, and include them on the page. Here’s a trun­cat­ed ver­sion of what that looks like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */&lt;/p&gt;

&lt;p&gt;html {&lt;br&gt;
    line-height: 1.15;&lt;br&gt;
    -webkit-text-size-adjust: 100%&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;body {&lt;br&gt;
    margin: 0&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;main {&lt;br&gt;
    display: block&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;/* Truncated here */&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap­ping Up
&lt;/h2&gt;

&lt;p&gt;That’s all she wrote! I’ve found that being able to inter­twine the famil­iar Tail­wind CSS styles that I’m used to with Emo­tion Styled Com­po­nents had cre­at­ed a real­ly com­pelling way to work with CSS.&lt;/p&gt;

&lt;p&gt;In the process of actu­al­ly using CSS-in-JS and Styled Com­po­nents, I found that many of my uncer­tain feel­ings about it dis­ap­peared. It turned my frown upside down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8_jOI9GJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/css-in-js-emotions.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8_jOI9GJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/css-in-js-emotions.jpg" alt="Css in js emotions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Part of the rea­son is that all of this is just a real­ly sophis­ti­cat­ed way to gen­er­ate and scope CSS in a way that allows for an excel­lent devel­op­er experience.&lt;/p&gt;

&lt;p&gt;And since we’re using Gats­by to ren­der every­thing out to sta­t­ic pages, it results in an excel­lent user expe­ri­ence too.&lt;/p&gt;

&lt;p&gt;The fact that it ends up scop­ing the CSS to each com­po­nent, and auto­mat­i­cal­ly inlin­ing just the CSS that we use on each page is pret­ty fantastic.&lt;/p&gt;

&lt;p&gt;It makes process­es like PurgeC­SS and Crit­i­cal CSS as described in the &lt;a href="https://dev.to/gaijinity/implementing-critical-css-on-your-website-37d5-temp-slug-6012917"&gt;Imple­ment­ing Crit­i­cal CSS on your web­site&lt;/a&gt; arti­cle unnecessary.&lt;/p&gt;

&lt;p&gt;Give it a whirl, and you might just &lt;a href="https://www.youtube.com/watch?v=mv9cWgkpIZ4"&gt;sec­ond that emo­tion&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;If you want to be notified about new articles, follow &lt;a href="https://twitter.com/nystudio107"&gt;nystudio107&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Copyright ©2020 nystudio107. Designed by nystudio107&lt;/small&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>gatsby</category>
      <category>emotion</category>
    </item>
  </channel>
</rss>
