<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Working Concept Blog</title>
        <link>https://workingconcept.com//blog</link>
        <description>Spontaneously updated notes from the field.</description>
        <lastBuildDate>Wed, 07 May 2025 02:26:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Astro</generator>
        <image>
            <title>Working Concept Blog</title>
            <url>https://workingconcept.com//apple-touch-icon.png</url>
            <link>https://workingconcept.com//blog</link>
        </image>
        <copyright>All rights reserved 2023, Working Concept Inc.</copyright>
        <atom:link href="https://workingconcept.com//blog/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Going Static, Quickly]]></title>
            <link>https://workingconcept.com/blog/going-static-quickly</link>
            <guid>https://workingconcept.com/blog/going-static-quickly</guid>
            <pubDate>Mon, 27 Jun 2016 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ve always enjoyed tinkering with this diminutive site, and lately I’ve continued the quest to improve speed and uptime while staying ruthlessly cheap about it. The result has been a switch from Statamic to a static site generator called <a href="https://sculpin.io/">Sculpin</a>, along with a hesitant return to <a href="https://www.cloudflare.com/">CloudFlare</a>. It took about a month of experiments and minor breakthroughts to get there.</p>
<p>I quietly got around to supporting SSL for my site in late February, mostly to be a good citizen of the internet. A recent series of Webfaction outages got me thinking about ways to circumvent downtime, and I was inspired by a client’s deft use of Fastly to solve the problem. I wandered my way into some requirements for this latest endeavor:</p>
<ol>
<li>Generate a static site to eliminate the need for any application caching.</li>
<li>Use a CDN for whole-site caching and global mirroring in the name speed and relative immunity from downtime, with an edge node in Seattle because I adore low pings yet will not relocate for them. (With an idealistic global page load time of &lt;1s, and initial connects in &lt;10ms.)</li>
<li>Switch entirely to SSL (https) URLs so <a href="https://en.wikipedia.org/wiki/PRISM_(surveillance_program)">PRISM</a> has to try a little harder.</li>
<li>Keep using a bare domain without any <em>www</em> because it looks nice that way.</li>
</ol>
<p>Here’s how that went.</p>
<h2>Generating the site with Sculpin</h2>
<p>I’ve been happily using Statamic since 2012, and it dawned on me that I don’t have any compelling reason for a CMS since I’m the sole author of a tiny, infrequently-updated site. Statamic has a static generator built in, but I figured I’d go all the way and remove the CMS from the equation. Jekyll seemed like the obvious choice, but Sculpin was a bit friendlier thanks to its reliance on Twig templates and native post tag handling.</p>
<p>The only roadblocks were easy to clear: I built a simple PHP form handler (hosted on a subdomain) and went through some dark URL rewriting to convince Apache to do without trailing slashes.</p>
<p>It also turns out that a rather critical aspect of being cacheable is <em>asking to be cached</em>. I’ve not always taken the time to specify cache headers, which can be a crucial mistake if a cache service is handling requests at face value. Like CloudFlare, for example, which tends not to cache HTML responses unless specifically asked to, rendering the Always Online™ feature worthless if it’s got no cached page to serve. </p>
<p>I used my .htaccess Apache directive to set Expires and Cache-Control values for different file types, and whipped up a shell script to publish the Sculpin site, copy .htaccess into the mix (which Sculpin otherwise leaves out), and publish it to Webfaction via <code>rsync</code>.</p>
<h2>Choosing a CDN</h2>
<p>This long journey had me comparing KeyCDN, Fastly, CloudFront, MaxCDN, CacheFly, section.io, and poking at CloudFlare again.</p>
<p>I could have used <a href="https://www.keycdn.com/">KeyCDN</a> for full site caching, and it would have been really fast and really cheap with free custom SSL. Pings and TTFB to KeyCDN’s nodes were easily &lt;10ms for most of my tests, and I would have lived with having to serve the site from a secret domain to have it pulled into a zone and served at workingconcept.com. (Pulling from workingconcept.com <em>while serving from it</em> created an infinite loop. Derp.) KeyCDN even offers origin shielding, which is another impressive offering from one of the least expensive CDNs around. A seemingly impatient support response confirmed what I suspected: there’d be no good way to serve the site from a bare domain. The bare domain is also called an apex domain by those with better vocabularies, and it would turn out to be the greatest challenge among my list of requirements. While I could use DNS Made Easy’s proprietary ANAME, which is kind of a CNAME living on the root A record, doing that robbed KeyCDN of its ability to distribute traffic geographically and instead served every request from its Washington D.C. node. I thought about going <code>www</code> and couldn’t. I contemplated living without global mirrors, and couldn’t.</p>
<p>So on I went to <a href="https://www.fastly.com/">Fastly</a>, which is a shockingly fast service built on Varnish. A client uses it for its web product, which helped me appreciate Varnish (which is like a software memory stick) and Fastly’s platform. My site would be on Fastly now if I wasn’t priced out of it for this humble blog. While I could have skated by for free with my low traffic, full SSL and whole-site caching would have been prohibitively expensive. This was confirmed by an uncommonly personable and hilarious support agent.</p>
<p>I’ve been using <a href="https://aws.amazon.com/cloudfront/">Amazon CloudFront</a> for static assets since early 2014, and while it could have been a viable option I didn’t want to switch to Route 53 for DNS since it never seems to appear in the top tier of performance benchmarks. I generally find AWS to be a bit more slow and a bit more expensive than alternatives, even though it’s a vast wonderland of integrated services.</p>
<p><a href="https://www.maxcdn.com/">MaxCDN</a> had a nice control panel, impressive speed, and a handful of nice options like the ability to designate an IP address—rather than a domain name—for a pull zone. As I learned with KeyCDN, this can make it easy to pull from the origin you’re also shielding. The lovely admin interface was littered with upsells, and an agonizing cancellation process ultimately confirmed that whole-site caching wasn’t going to be possible with a bottom-tier (cheap!) client.</p>
<p><a href="https://www.cachefly.com/">CacheFly</a>’s control panel was an uncomfortable journey back in time, and I promptly fled once I figured out that whole-site caching—or anything more than hosting static files—wasn’t going to be a good idea there.</p>
<p><a href="https://www.section.io/">section.io</a> was intriguing and confusing enough that I toured it twice. It’s sort of like a scrappy Fastly, made up of various pieces that developers and ops folks would mostly likely appreciate. Where Fastly had a well-unified, seemingly polished control panel, section.io’s often left me uncomfortable and uncertain what I was getting into, even though it was clearly a flexible product aimed at fitting in with developers’ workflows. Like Fastly, it made it possible to combine Varnish with a CDN. Custom SSL would have been free, and I might have easily afforded low apex domain traffic by handing over DNS to section.io. Support was prompt, personal, and friendly, and engineers fixed an innocent DNS issue that I had as I was kicking the tires. Unlike Fastly, inexperience with Varnish may have kept me from being <em>qualified</em> to work with section.io. A confusing inability to cache static pages and lackluster CDN speeds led me reluctantly away.</p>
<p>And that’s how I returned to <a href="https://www.cloudflare.com/">CloudFlare</a>. I’ve been routinely scared away from free accounts over infuriating 522 errors, with CloudFlare blaming an unresponsive server while the server’s <em>usually</em> perfectly happy and healthy. I’ve had better luck with paid CloudFlare accounts using full DNS setups (as opposed to CNAME only), and Railgun is really nice when it’s an option. My tests with CloudFlare yielded heartening waterfall timings with &lt;100ms total for a simple static page, all over the globe—from that all-important apex domain. While custom SSL wasn’t a cheap/free option, I don’t mind the shared certificate<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>. I also don’t like CloudFlare’s messaging during an outage because I’d prefer that a visitor not even <em>know</em> the origin went offline, but at least all this thrilling content can weather a storm.</p>
<p>DNS Made Easy has been consistently fast and issue-free since I started using its DNS service in February 2014. But I bravely and/or foolishly left a good thing and pointed my DNS to CloudFlare.</p>
<h2>tl;dr</h2>
<p>I’m back on CloudFlare, finally specifying appropriate cache headers, with a completely static site served via https on a bare (apex) domain. The plan has so far succeeded, and I’m curious what my uptime will look like in the coming months.</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>Which is served, as most of these free SSL options are, via <a href="https://en.wikipedia.org/wiki/Server_Name_Indication">SNI</a>. This is a newer option which overcomes the one-certificate-per-IP limitation at the expense of leaving out older browsers and programming languages. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Git Annex vs. Git LFS]]></title>
            <link>https://workingconcept.com/blog/git-annex-vs-git-lfs</link>
            <guid>https://workingconcept.com/blog/git-annex-vs-git-lfs</guid>
            <pubDate>Tue, 08 Dec 2015 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Many of us have felt the shameful sting of committing a large file to an otherwise pristine repository. “But <em>it’s</em> important <em>too</em>,” you pleaded. “Idiot,” they snarked, begrudgingly fixing your mistake while secretly acknowledging that you had a point. Years later, we have a resolution.</p>
<p>I’ve been working with Git Annex thanks to <a href="https://about.gitlab.com/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/">GitLab’s adoption early this year</a>. GitHub <a href="https://github.com/blog/1986-announcing-git-large-file-storage-lfs">opted to support Git LFS</a>, which I’ve only started poking at now that GitLab <a href="https://about.gitlab.com/2015/11/23/announcing-git-lfs-support-in-gitlab/">decided to support it, too</a>.</p>
<p>But which one to use? Each will help you solve that large-file problem in a slightly different way. Think of Git Annex as an experienced librarian waiting at the information desk. You’ll need to walk up to ask your question or return your book, but the librarian can help you in a variety of ways by getting books moved around, checking up on things, and generally being a pro at cataloging stuff. Git LFS is more like your large file personal assistant, working alongside you to keep track of things you’ve pointed out.</p>
<p>In other words, my experience with Annex is that it’s full-featured and a bit less focused in its approach. It’s easy enough to check in files and sync them among various locations, but there are also testing tools, a web-based GUI, and lots of options you can use in different situations. The <a href="https://git-annex.branchable.com/">git-annex project site</a> reveals a lot: plenty of features, updates, discussions, and enough threads that some sort of trail off.</p>
<p>Git LFS is at the other end of things: a bit nicer-looking, a bit more straightforward, and significantly simpler. Tack it on to your repository, tell it what kind of files to watch, and then pretty much forget about it. If you check in a file (with a normal <code>git add whatever.mp4</code>), the magic happens via a pre-push hook where LFS will check your watch list and spring into action if needed. It otherwise blends in after minimal configuration.</p>
<p>Let’s take an identical set of files and commit them using each.</p>
<h2>Our Fake Project</h2>
<p>A markdown file and four PNGs that we’re going to treat as large files:</p>
<ul>
<li>test.md</li>
<li>assets/01-north-pole-daylight.png</li>
<li>assets/02-plus-blue-light.png</li>
<li>assets/03-night-glow.png</li>
<li>assets/04-day-glow.png</li>
</ul>
<h2>Git Annex</h2>
<p>Once git-annex is installed, we need to configure our repository to use it:</p>
<pre><code>$ git annex init
init ok
(recording state in git...)</code></pre>
<p>Add our one normal file as usual:</p>
<pre><code>$ git add test.md</code></pre>
<p>Add our images to the annex:</p>
<pre><code>$ git annex add assets/*
add assets/01-north-pole-daylight.png ok
add assets/02-plus-blue-light.png ok
add assets/03-night-glow.png ok
add assets/04-day-glow.png ok
(recording state in git...)</code></pre>
<p>The files are turned into symbolic links that point to data in .git/annex/objects. (You can easily undo this if you ever need to, restoring the files and getting rid of the annex.) Push it all:</p>
<pre><code>$ git push origin
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 282 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)</code></pre>
<p>Critically important here is that the PNG files are not yet at the origin, only symbolic links that point to them. Annex knows they’re on the local machine, but that’s the only place they exist so far. We can ask git-annex where a certain file exists:</p>
<pre><code>git annex whereis 04-day-glow.png (1 copy) 
 758aaecf-f502-44be-8854-03819db671d6 -- matt@mattBook-Air.local:~/Documents/git/annex-test [here]</code></pre>
<p>This can be useful while exploring and figuring out what’s going on. Now let’s actually sync the file data:</p>
<pre><code>$ git annex sync --content
commit ok
pull origin 
ok</code></pre>
<p>Each file will be copied with a progress report that looks like this:</p>
<pre><code>copy assets/01-north-pole-daylight.png copy assets/01-north-pole-daylight.png (checking origin...) (to origin...) 
SHA256E- s1093459--12cbb85304bc084cedd0537830a09d55e3cd2224917e9faaec390b27ad7f2d98.png
 1093459 100% 126.44MB/s 0:00:00 (xfer#1, to-check=0/1)

sent 1093758 bytes received 42 bytes 128682.35 bytes/sec
total size is 1093459 speedup is 1.00
ok</code></pre>
<p>Before (as seen above) and after the binary transfer, git-annex will make sure the file metadata is properly synced as well.</p>
<pre><code>pull origin 
ok
(recording state in git...)
push origin 
Counting objects: 14, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (10/10), done.
Writing objects: 100% (14/14), 1.08 KiB | 0 bytes/s, done.
Total 14 (delta 4), reused 0 (delta 0)
To git@gitlab.com:workingconcept/annex-test.git
 4ef0edb..a8f52cd git-annex -&gt; synced/git-annex
ok</code></pre>
<p>We can use the same trick from above to confirm that a given PNG now lives in more than one place:</p>
<pre><code>git annex whereis 04-day-glow.png (2 copies) 
1d3c732a-ff9a-490e-a0c8-18ee13694c7a -- "GitLab" [origin]
758aaecf-f502-44be-8854-03819db671d6 -- matt@mattBook-Air.local:~/Documents/git/annex-test [here]</code></pre>
<p>Success! It’s easy enough to clone the repository and use <code>git annex sync --content</code> to get everything synced up. A long list of files will go slowly since they’re transferred one at a time, but my experience has been pretty solid<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>.</p>
<h2>Git LFS</h2>
<p>Now let’s do the same thing with LFS. Same first step, which is to invite git-lfs into the repository:</p>
<pre><code>$ git lfs install
Updated pre-push hook.
Git LFS initialized.</code></pre>
<p>Simple, yet hugely different: tell LFS what kind of files we’ll treat separately:</p>
<pre><code>$ git lfs track "assets/*"
Tracking assets/*</code></pre>
<p>In this case, everything in the assets folder. It could just as well be extensions like <code>*.psd</code> or <code>*.mp4</code>, or a handful of paths. Now let’s add everything and commit it:</p>
<pre><code>$ git add .
$ git commit -m "Test commit."
[master (root-commit) 02a1537] Test commit.
 6 files changed, 16 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 assets/01-north-pole-daylight.png
 create mode 100644 assets/02-plus-blue-light.png
 create mode 100644 assets/03-night-glow.png
 create mode 100644 assets/04-day-glow.png
 create mode 100644 test.md</code></pre>
<p>That’s it, no extra step. Push:</p>
<pre><code>$ git push origin -u
Git LFS: (4 of 4 files) 3.95 MB / 3.95 MB 
Counting objects: 9, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 1.02 KiB | 0 bytes/s, done.
Total 9 (delta 0), reused 0 (delta 0)
To git@gitlab.com:workingconcept/lfs-test.git
 * [new branch] master - master
Branch master set up to track remote branch master from origin.</code></pre>
<p>Note that LFS reported back about what it had to transfer, and that after the initialization we just go about our business as we normally would. LFS added <code>.gitattributes</code> to store a reference to its watched file patterns, and it jumps in and does its magic only when it needs to. Complete contents of <code>.gitattributes</code>:</p>
<pre><code>assets/* filter=lfs diff=lfs merge=lfs -text</code></pre>
<p>The second thing that LFS quietly adds to your repository is a pre-push hook. Thanks to these two things, you don’t have to interact with lfs directly like you would with git-annex. Contents of the pre-push hook:</p>
<pre><code class="language-shell">#!/bin/sh
command -v git-lfs &gt;/dev/null 2&gt;&1 || { echo &gt;&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
git lfs pre-push "$@"</code></pre>
<h2>Feel Good Ending</h2>
<p>So you have options. I don’t know that either one’s better, it’s just a matter of choosing what works for you and is supported by your Git host.</p>
<p>I’ve not gotten myself into any complex situations yet, and I’m working on converting some repositories largely by running <code>uninit</code> to un-annex files and remove every trace of git-annex from the repository so I can turn around and check those files in with LFS.</p>
<p>Let me know if you’ve stumbled upon this post with more insights, corrections, or flagrant objections!</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>I’ve been stumped a few times when cloned repositories just don’t sync binary data, but it’s been a matter of setting <code>annex-ignore = false</code> in .git/config. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Let's Interactivate]]></title>
            <link>https://workingconcept.com/blog/lets-interactivate</link>
            <guid>https://workingconcept.com/blog/lets-interactivate</guid>
            <pubDate>Wed, 28 Jan 2015 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Being concerned with word usage but somewhat lacking in vocabulary, I often find myself making up words. I know what this implies about my intelligence and can live with that part, but recently I decided that a common web development practice <em>must</em> have some word I don’t know.</p>
<p>The situation: I’m taking a flat html file and integrating it into a content management system. It’s no problem describing that task in a conversation, like "we’d like to make that page dynamic." It gets tricky when I need a concise way of listing the task among others: </p>
<ul>
<li>“make About page dynamic” (wordy)</li>
<li>“dynamic-ify About page” (made up word)</li>
<li>“dynamize About page” (will anybody know what this means?)</li>
</ul>
<p>I took to the English Language & Usage Stack Exchange, where <a href="http://english.stackexchange.com/questions/224264/is-there-a-concise-word-or-phrase-for-making-something-dynamic">the question more or less flopped</a>. There were some interesting ideas, but no legitimate English word was uncovered.</p>
<p>That’s why I think we should all use the word <strong>interactivate</strong>. Sure, it’s <a href="http://www.urbandictionary.com/define.php?term=interactivate">not even fully embraced by Urban Dictionary</a>, but I think it has merit. We are, in fact, <em>activating</em> an otherwise static and lifeless file, much like a chemist (or wizard!) might <a href="http://en.wikipedia.org/wiki/Activation">activate</a> inert ingredients and turn them into something more useful. The presence of <em>interact</em> is still important, as it correctly conveys the transition from a static thing to one that’s capable of being acted upon via the content management system.</p>
<p>This is all so that my little bullet can be tidy and instantly understandable:</p>
<ul>
<li>“interactivate About page”</li>
</ul>
<p>Building your mobile app after solely working with wireframes and comps? <em>Interactivate</em>!</p>
<p>Making a prototype of a thing you sketched on a napkin? Why, you may just be <em>interactivating</em>!</p>
<p>Now who’s with me on this?</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Synology Gmail Backup, Again]]></title>
            <link>https://workingconcept.com/blog/synology-gmail-backup-again</link>
            <guid>https://workingconcept.com/blog/synology-gmail-backup-again</guid>
            <pubDate>Sun, 11 Jan 2015 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Using <a href="http://gmvault.org/">GMVault</a> to back up Google Apps email to my Synology <a href="http://amzn.com/B00FY6DV3S">DS214</a> has been great, when it works. And it seems like it fails pretty often: software update, token expiration, strong gust of wind, etc.</p><p>My <a href="{entry:78:url}">original article from April 2014</a> turned into a bit of a mess, but after a few reinstalls and helpful comments (thanks David and Simon) I’ve got a much shorter set of instructions:</p><ol><li>Install the Python (2.7) package from the Synology package manager.</li><li>SSH into the Synology box as root.</li><li><p>Download and install pip:</p> <pre><code>wget https://bootstrap.pypa.io/get-pip.pypython get-pip.py</code></pre></li><li><p>Use pip to install virtualenv:</p>&lt;pre&gt;&lt;code&gt;pip install virtualenv&lt;/code&gt;&lt;/pre&gt;</li><li>Log in as your normal user, and <code>cd</code> to wherever you want to install GMVault.</li><li><p>Set up a virtual environment for GMVault to run in:</p>&lt;pre&gt;&lt;code&gt;virtualenv gmvault_env&lt;/code&gt;&lt;/pre&gt;</li><li>Install GMVault in this little environment: &lt;pre&gt;&lt;code&gt;cd gmvault_env/bin./pip install --verbose --pre gmvault --allow-external IMAPClient&lt;/code&gt;&lt;/pre&gt;</li></ol><p>Now you can run <code>./gmvault</code> from this directory, or put together scripts that reference it by full path.</p><p>If you’re like me and your backups frequently see IMAP timeouts, don’t forget to <code>cd ~/.gmvault</code> and edit <code>gmvault_defaults.conf</code> so that <code>enable_imap_compression=False</code>.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Minor Facelift]]></title>
            <link>https://workingconcept.com/blog/a-minor-facelift</link>
            <guid>https://workingconcept.com/blog/a-minor-facelift</guid>
            <pubDate>Wed, 19 Nov 2014 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’re used to visiting this site often, and I’m not sure why you would, you probably noticed some minor changes. Over the weekend, I rebuilt my Statamic theme with <a href="http://bourbon.io/">Bourbon</a>, <a href="http://neat.bourbon.io/">Neat</a>, and <a href="http://bitters.bourbon.io/">Bitters</a>. This was brought about by a newfound love of <a href="https://thoughtbot.com/">thoughtbot</a> after <a href="http://elcontraption.com/">Darin</a> and <a href="http://sccottt.com/">Scott</a> pointed me to the lovely <a href="https://playbook.thoughtbot.com/">thoughtbot playbook</a>. That and I was upset at some lackluster home page rendering I noticed with Safari on iOS8.</p>
<p>Features:</p>
<ul>
<li>better and more fluid mobile breakpoints</li>
<li>beautifully organized Sass that you won’t see or notice</li>
<li>cleaner CSS that’s also about 10KB lighter</li>
<li>code sample syntax highlighting with <a href="http://prismjs.com/">Prism</a></li>
</ul>
<p>Not yet completed:</p>
<ul>
<li>an interesting home page</li>
<li>a focused blog with regular posts</li>
</ul></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Focus]]></title>
            <link>https://workingconcept.com/blog/focus</link>
            <guid>https://workingconcept.com/blog/focus</guid>
            <pubDate>Sun, 09 Nov 2014 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’re reading my blog, you might think I’ve been living in the woods since June. Unfortunately I haven’t, because boy would the facial hair be bizarre, but there <em>has</em> been plenty going on.</p>
<p>The five-year-and-counting experiment is still going strong, now from a new office with some new clients. For somebody like me that enjoys diverse and interesting challenges, saying "no" to things and focusing on <em>great</em> over <em>good</em> is hard. It means deliberately taking on fewer projects, fewer clients, and saying no to good opportunities. It means releasing inactive side projects back out into the ether, where they can’t quietly compete with other thoughts and ideas. It means having the discipline to identify those little things that sap attention and deal with them. It means letting dumb domain names expire and being okay with it.</p>
<p>Raina (my wife and archnemesis) has joined me during the day, and her strict Trello discipline alone has sharpened the view on the ground and at 10,000 feet. My year-old(ish) decision to jump headfirst into Craft has led to fun and interesting work, including the past week I spent with an awesome<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>, recently-acquired startup in beautiful Palo Alto. Being there was energizing and made me want to be better, which is how I know I’m on a good path despite making all of this up. Even minor improvements to focus have yielded more fun and more interesting work.</p>
<p>In hopelessly dorky terms, I’m happiest when I’ve reached high I/O in strong relationships. Hopefully I can continue weeding out idle processes and patching memory leaks until I can fly<sup id="fnref1:2"><a href="{fn:2:url||}" class="footnote-ref">2</a></sup>.</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>Not used in the common, "that taco was awesome" kind of way. I love good tacos, but I was shocked and <em>very much in awe</em> of the human beings I met and the over-the-top offices they work in. I’ve never been among a more intelligent, energetic, and welcoming bunch of people. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
<li id="fn:2">
<p>I’m not a robot or capable of unassisted flight. <a href="{fnref1:2:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Craft CMS Stack Exchange Proposal]]></title>
            <link>https://workingconcept.com/blog/craft-cms-stack-exchange-proposal</link>
            <guid>https://workingconcept.com/blog/craft-cms-stack-exchange-proposal</guid>
            <pubDate>Mon, 02 Jun 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Wouldn’t you love to live in a world where Craft questions are asked and answered on Stack Exchange instead of Google+? Of course you would!</p>
<p>So get on over there and <a href="http://area51.stackexchange.com/proposals/69388/craft-cms?referrer=dW1py70ah5jdEdKwLJfBTQ2">support the proposal</a>!</p>
<p><em>Edit: Committed! It didn’t take long, so the beta period should be pretty fun.</em></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Goodbye, Mountee]]></title>
            <link>https://workingconcept.com/blog/goodbye-mountee</link>
            <guid>https://workingconcept.com/blog/goodbye-mountee</guid>
            <pubDate>Thu, 15 May 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I just read <a href="http://hellomountee.com/support/discontinuing_mountee/">the news</a> and it’s not good: John and Padraig are no longer maintaining and supporting Mountee, the magical Mac app that let me work with EE templates like plain old files in Sublime Text. I could search and replace across templates, add/delete/rename, change PHP and cache settings from finder, and have a lovely time of it.</p>
<p>After a brief panic and some cranky grumbling I’ve saved all my templates as files for the first time. I’m not looking forward to this "synchronize templates" step that’ll be required with every change, and my productivity is bound to take a dip until I figure out how Mountee-less EE humans have been coping all along.</p>
<p>Craft looks that much better now and it <em>didn’t even have to do anything</em> this time!</p>
<hr>
<p><strong>Update 10/16/14:</strong> Nevermind! <a href="http://www.hopstudios.com/blog/hop_studios_acquires_mountee#.VEAdbYvF8_M">Hop Studios has taken ownership of Mountee</a> and will apparently keep it going. Now that I’ve got flat, versionable templates I’m not sure I can go back though.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[109 Days with Webfaction]]></title>
            <link>https://workingconcept.com/blog/109-days-with-webfaction</link>
            <guid>https://workingconcept.com/blog/109-days-with-webfaction</guid>
            <pubDate>Mon, 17 Feb 2014 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>In October 2013, <a href="http://mediatemple.net/blog/2013/10/15/momentous-news-godaddy-mt-media-temple/">GoDaddy’s scandalous acquisition of Media Temple</a> sent me on a quest to replace my common recommendation of the DV product. I’ve been giving <a href="https://www.webfaction.com/?affiliate=wrkcpt">Webfaction</a> (affiliate link) a go and am quite pleased so far.</p>
<h2>Contenders</h2>
<p>I experimented with <a href="http://www.ramnode.com/">RamNode</a> (fast!), <a href="http://www.vpscheap.net/">VPSCheap</a> (yay resources!, more limited connection speed), and considered <a href="https://www.digitalocean.com/">DigitalOcean</a>. I can’t believe that hosts like RamNode and VPSCheap even exist; <em>do they even know</em> how much power and speed they’re giving away for so little? While they deserve separate write-ups, they offer unmanaged hosting which I’ll only consider for my own development endeavors. I’m not a server guru so I won’t force my inexperience on clients when it comes to production sites.</p>
<p>DigitalOcean still captured my interest, as it seems (anecdotally) popular with many a web nerd, but a <a href="http://www.macdrifter.com/2013/10/my-webfaction-hosting-experience.html">shining account from mega nerd Gabe Weatherhead</a> pulled me into Webfaction’s corner. And I like it there.</p>
<h2>The Promise</h2>
<p>Supposedly, Webfaction provides speedy hosting with ample resources and good support. It’s geared for developers, whatever that means.</p>
<h2>First Impressions</h2>
<p>I signed up for my trial account and logged into the control panel, flush with dopamine. At first glance, “geared for developers” simply meant “convoluted setup that requires patience.” You don’t add a domain name and dump your (digital) bags in a corner via SFTP or SSH. Instead you linger in the lobby, bags in hand, trying to figure out how to check out a room.</p>
<p>But it’s not hard, and one might even start thinking of it as smart: you define a new application (sort of a tailored environment for your intended app), a new domain, and a new website which simply links the application and domain. Hey, kind of smart—and that orderly flexibility could be useful later!</p>
<p>I got my Statamic site moved over, which is painless because it’s just PHP, Yaml, and Markdown. One clone of a Git repo and I’m on to the next app.</p>
<p>Worth mentioning here is that I could SSH right in and interact with Git and other friendly command line utilities. I felt right at home. There’s limited root access as well, even though I’ve not done anything interesting enough to test it.</p>
<p>Also nice was that when choosing an environment I could select—among other things—PHP 5.3, 5.4, or 5.5. Modern presets (Ghost, Node, etc.) were available, not endless lists of junk. To be clear, Webfaction’s “application” isn’t the same thing as a cPanel-style one-click installer that gets you up and running with WordPress and a database name you wish you would have chosen yourself. Technically speaking, <a href="http://docs.webfaction.com/user-guide/websites.html">it’s more thoughtful than that</a>.</p>
<p>Setting up a new MySQL database was child’s play. Nothing to write about there, other than a note that PHPMyAdmin is available should you need a GUI for your MySQLing.</p>
<h2>Support</h2>
<p>I’ve been spoiled being able to speak on the phone with a human being at Media Temple. Someone I can understand, who can understand me and correct my mistake or fix a problem. The same goes for chat, which is typically quick and productive. Nobody ever asks if I’ve turned it off and on again.</p>
<p>In the event of a necessary Media Temple support ticket, there’s always thorough and expert attention within the course of a day or two.</p>
<p>So Webfaction had a lot to live up to.</p>
<p>Webfaction doesn’t offer phone support to mortals like me, so all the pressure is on its ticketing system.</p>
<p>My full account was established on 10/24, and the seemingly-automated “your account is ready” email allegedly came from a guy named Wayne. I cheerily thanked “Wayne” just for the fun of it, and ten minutes later he extended a personal welcome and reminded me they’re ready to answer questions when I have them<sup id="fnref1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>. Wayne was real!</p>
<p>I submitted my first real ticket with low priority on 10/30, trying to figure out why PHP’s <code>file_get_contents()</code> was mysteriously timing out in one app. Ryan’s response came 8 hours later, and he asked some questions that immediately led me to the issue.</p>
<p>My second low priority ticket got a response in six minutes, and was wrapped up within a 15-minute window.</p>
<p>But let’s get serious with my only <em>normal</em> priority ticket. I got a password wrong and locked myself out of SSH. (Don’t judge, you’ve done it.) I submitted a ticket for this on 12/31 expecting an annoying delay, and was robbed of that annoyance when Björn cleared the IP block <strong>two minutes later</strong>.</p>
<p>My check goes in the “so far, so good” column on support, but you can interpret for yourself.</p>
<h2>Speed</h2>
<p>I moved an AngularJS+CodeIgniter app from Pagoda Box<sup id="fnref2"><a href="{fn:2:url||}" class="footnote-ref">2</a></sup> to Webfaction and the performance difference was night and day. Rendering times dropped a half second or more, and the entire app felt significantly more speedy—nearly (but not entirely) instantaneous.</p>
<p>Every site I’ve deployed on Webfaction (<a href="https://codeigniter.com/">CodeIgniter</a>, <a href="https://expressionengine.com/">ExpressionEngine</a>, <a href="https://statamic.com/">Statamic</a>, <a href="https://craftcms.com/">Craft CMS</a>, <a href="https://bugify.com/">Bugify</a>) has felt faster, even if I’ve not bothered to benchmark in each case. I guess it’ll be tough for you to take my word for it, but you’ll have to. Or challenge me in the comments and I’ll benchmark upon learning that non-imaginary people read my blog.</p>
<p>In the thirty days prior to my Webfaction migration (from Pagoda Box), Pingdom reports 99.92% uptime with 21 downtimes. The thirty days after the switch have a 99.96% uptime with 2 downtimes<sup id="fnref3"><a href="{fn:3:url||}" class="footnote-ref">3</a></sup>.</p>
<p>Pingdom’s global response time averages (all European and North American nodes) in those same time periods: 954ms before the switch and 392ms after. Exact same URL and app being tested.</p>
<p>This is all … <em>ahem</em> … under very little load.</p>
<h2>Mail</h2>
<p>I’m always skeptical about email services that come with a hosting package. In my experience, particularly in shared environments, the accounts are like spam sponges and are often slow and intermittently unavailable. Not exactly top-notch. I use Google Apps for mail so I’m immune to these situations, but I still have to consider clients that may rely on included services.</p>
<p>Unfortunately all I know is that server-generated messages go out instantly, and <a href="http://www.macdrifter.com/2013/10/my-webfaction-hosting-experience.html#comment-1085190995">Gabe says</a> the email service is good.</p>
<h2>Downsides</h2>
<p>The service and support have been great, but Webfaction isn’t perfect.</p>
<p>My most serious complaint is that only one set of credentials has full access to the control panel. Separate contacts can be listed as technical, support, billing, etc., but only one username and password can be used to access the control panel. Webfaction says they’re looking to improve this situation, but it’s definitely an annoyance for smoothly handing off (or sharing) client access.</p>
<p>The control panel generally looks nice and is straightforward to use, but it drives me bonkers that clicking on a parent navigation item simply reveals child options and another selection is required to do something. I’m sure they’re all very nice people, but come on: give me a useful landing page or a flyout menu. I’m used to this now, but I still don’t like it.</p>
<h2>Working Conclusion</h2>
<p>I like Webfaction. My apps run consistently faster and have good uptime, the support is great, and the quality of services and resources and support all feel like a good value for the price.</p>
<p>I’ll update my review if there are any new developments, but I’m happy to stay and am confidently able to recommend Webfaction to clients.</p>
<p>I’ll share <a href="https://www.webfaction.com/?affiliate=wrkcpt">one last affiliate link</a> just because I can, and you should know that I’m just another nerd with no special relationship to Webfaction or any other hosting company. But please, give me free stuff to play with and I’ll write about it!</p>
<p>If you’re one of the three people that reads this, let me know! I’m happy to answer questions and I like learning how others are faring with their hosting.</p>
<h2>Other Disclosure</h2>
<p>I got an email from Webfaction inviting me to dust off the Twitter account and tweet about my experience, good or bad, and get a free month of service. I was already planning on writing a bit more about it, so I sort of followed through but in more than 140 characters.</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>I’m often guilty of not R’ing T F’ing M, but <a href="http://docs.webfaction.com/user-guide/">Webfaction’s docs</a> have typically saved me from asking a fair number of questions. <a href="#fnref1" rev="footnote" class="footnote-backref">↩</a></p>
</li>
<li id="fn:2">
<p>Possibly an unfair comparison since this was a barebones, bottom-of-the-barrel Pagoda Box configuration. <a href="#fnref2" rev="footnote" class="footnote-backref">↩</a></p>
</li>
<li id="fn:3">
<p>Pagoda Box was a really cool platform with good support, just awful uptime and questionable bottom-tier performance. <a href="#fnref3" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Writing]]></title>
            <link>https://workingconcept.com/blog/writing</link>
            <guid>https://workingconcept.com/blog/writing</guid>
            <pubDate>Mon, 16 Dec 2013 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Lately I’ve been making an effort to evaluate and improve my writing wherever it happens. I found the book <a href="http://amzn.com/0060891548">On Writing Well</a> to be an informative, inspiring page-turner, which can be unfairly distilled to one idea: “clear writing is the result of clear thinking.”</p>
<p>You should read it, especially if you don’t think you should read it.</p>
<p>I’m mumbly and occasionally rambling in person, and it normally takes me a few approach runs before I taxi into the airport of understanding. But I love bringing order and clarity to things even if just for myself. Since enjoyment does not equal proficiency, I keep a journal and try to write in it every day. William Zinsser, author of the aforementioned book, is conveniently on board with this:</p>
<blockquote>
<p>You learn to write by writing. It’s a truism, but what makes it a truism is that it’s true. The only way to learn to write is to force yourself to produce a certain number of words on a regular basis.</p>
</blockquote>
<p>I enjoyed Zinsser’s definition of clutter as “the official language used by corporations to hide their mistakes,” which extends to people like me.</p>
<p>One of my favorite highlights is one that I should be reading every time I bother to write more than a paragraph:</p>
<blockquote>
<p>Be grateful for everything you can throw away. Reexamine each sentence you put on paper. Is every word doing new work? Can any thought be expressed with more economy? Is anything pompous or pretentious or faddish? Are you hanging on to something useless just because you think it’s beautiful? Simplify, simplify.</p>
</blockquote>
<p>Like anyone, my written persona gets to sound much more focused and eloquent when I make the effort to scrutinize my words. Hopefully when you’ve read my email, Basecamp message, or sporadic blog post you’ll see evidence of careful consideration. If not, please call me on it, because my unclear writing certainly deserves clearer thinking.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[[link] The problems ‘crafting’ code]]></title>
            <link>https://workingconcept.com/blog/problems-with-crafting-code</link>
            <guid>https://workingconcept.com/blog/problems-with-crafting-code</guid>
            <pubDate>Sun, 24 Nov 2013 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><a href="http://csswizardry.com/2013/11/the-problems-with-crafting-code/">Harry Roberts said it much better</a> in his article about ‘crafting’ software. It’s focused and even productive, in stark contrast to my recent attempt to quantify my frustration with the word.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AMPPS + OSX Mavericks]]></title>
            <link>https://workingconcept.com/blog/ampps-osx-mavericks</link>
            <guid>https://workingconcept.com/blog/ampps-osx-mavericks</guid>
            <pubDate>Tue, 22 Oct 2013 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>Update:</strong> Softaculous updated AMPPS so that the GUI tools will ultimately work with Mavericks, and <a href="http://www.softaculous.com/board/index.php?tid=4639">posted some patching instructions</a> for those already using 2.1. <a href="http://www.ampps.com/download">Time to download</a>!</p>
<p>I jumped in with both feet today and upgraded both of my work machines. Surprisingly enough, the only thing that’s broken so far (aside from <a href="http://osxdaily.com/2011/12/12/hide-spotlight-menu-icon-mac-os-x/">hiding spotlight</a> again and <a href="http://www.groths.org/software/trimenabler/">re-enabling TRIM</a>) is AMPPS – specifically Apache.</p>
<p>I’ll post an update when I get somewhere more intelligent, but for now I’ve found that I can force start the bundled Apache under sudo via command line:</p>
<pre><code> sudo /Applications/AMPPS/apache/bin/httpd -k start</code></pre>
<p>You can of course stop Apache this way as well…</p>
<pre><code> sudo /Applications/AMPPS/apache/bin/httpd -k stop</code></pre>
<p>This was mentioned on the <a href="http://www.ampps.com/wiki/Category:FAQs#Mac_OS_X">AMPPS Wiki</a> and works just fine.</p>
<p><strong>But why does this happen?</strong></p>
<p>The system log leads me to believe that the problem lies squarely with a missing <code>/etc/authorization</code>, a <a href="http://www.afp548.com/2013/10/22/modifying-the-os-x-mavericks-authorization-database">normal and expected change</a> that’s part of Mavericks’ revamped security.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Please, Support Your Commercial Software]]></title>
            <link>https://workingconcept.com/blog/please-support-your-commercial-software</link>
            <guid>https://workingconcept.com/blog/please-support-your-commercial-software</guid>
            <pubDate>Sat, 14 Sep 2013 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ve been working on an ExpressionEngine project, for which we decided years ago to use your add-on. It’s a commercial add-on, meaning that people pay for your work to be able to take advantage of what the product advertises. It’s still being sold as I write this.</p>
<p>After years of evolution, the product sucks. Simple things like broken pagination, a data exporter that only provides 100 records, and a high-profile tool that denies you access even as a Super Admin – bugs that aren’t hard to fix. Bigger things, like sloppy code and questionable implementation – those are harder to fix unless you refactor and get serious about planning well and working cleanly.</p>
<p>But these things could be excused, at least, if you were taking feedback from your users and constantly patching up the junk. I write software, and I’m not at all perfect or always designing things I’m proud of – I understand how that can happen. What I don’t understand, however, is how you can release this software to the public with a certain expectation, take money, and then turn your back to problems by not supporting your software.</p>
<p>This is irresponsible and bad business. I’m not sure if the Pixel & Tonics and Lows and Causing Effects of the world necessarily care deeply about their customers’ wellbeing, but I would be shocked if they didn’t all agree that happy customers are an essential part of the business. In each case there are products that fill specific needs (like yours), and thoughtful, friendly, responsive support – very much unlike yours.</p>
<p>I would love to have a product I could charge for, evolve, and support – but the type of workload I maintain right now would leave support efforts (or current clients) shortchanged and I’m not willing to half-ass something out into the marketplace. I’m not willing to put something out there and charge for it ultimately to hurt the reputation of ExpressionEngine’s talented, thoughtful development community. Or worse, to create new headaches for the customers whose problems my widget attempted to solve.</p>
<p>If your product sucks so bad that you’re completely overwhelmed trying to support it, apologize to the people whose money you’ve stolen and take it off the market. Or open source it so we can all fix it up together. Or just work out the specifics of your business so that you can at least catch up to your promises and get your product back on track.</p>
<p>I’m not asking that you write perfect software, that you test like you should, or that you rethink everything you’ve done. But please: in the interests of professionalism, decency, honesty, and consideration of the community, support your software or make it go away.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TextExpander and OSX Mavericks]]></title>
            <link>https://workingconcept.com/blog/textexpander-osx-mavericks</link>
            <guid>https://workingconcept.com/blog/textexpander-osx-mavericks</guid>
            <pubDate>Wed, 12 Jun 2013 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>For whatever reason, I’ve once again been invited to the AppleSeed program and I’m running Mavericks (10.9) on my MacBook Air. A bazillion other blogs will have thoughts on the subject, I’d just like to point out that TextExpander works as long as you figure out how to enable assistive devices in the rearranged System Preferences…</p>
<p>Visit <strong>Security & Privacy</strong>, then select the <strong>Privacy</strong> tab. There you’ll find <strong>Accessibility</strong>, and to the right will be a list of applications that you can allow to control your computer. Enable TextExpander and all is well again.</p>
<p>I lived without TextExpander <em>for an entire day</em> before figuring this out. Thanks to <a href="http://forums.macrumors.com/showthread.php?t=1593859">treichert in the MacRumors forums</a>.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[SmartStart WordPress Theme Fix]]></title>
            <link>https://workingconcept.com/blog/smartstart-wordpress-theme-fix</link>
            <guid>https://workingconcept.com/blog/smartstart-wordpress-theme-fix</guid>
            <pubDate>Thu, 31 Jan 2013 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><em>This is an oddly-specific post that I’m leaving here just in case my blog can better focus on the issue than ThemeForest’s flat comments<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>.</em></p>
<p>A rather common problem with the <a href="https://web.archive.org/web/20130131011838/http://themeforest.net/item/smartstart-wp-responsive-html5-theme/discussion/2067920?page=87">SmartStart Responsive HTML5 WordPress theme</a> is that upon upgrading to WordPress 3.5, the site will suddenly start throwing an error on portfolio pages:</p>
<pre>Warning: array_count_values() [function.array-count-values]: Can only count STRING and INTEGER values! in (...)/themes/smartstart/functions/custom-functions.php on line 640
</pre>
<p>A quick fix that drops the error and seems to leave everything in good order is to wipe out a line that tries to count something that’s not there. You can do this by opening <strong>smartstart/functions/custom-functions.php</strong> in a text editor (it’s okay if PHP seems scary) and finding <strong>line 640</strong>. You’re looking for this part:</p>
<pre><code class="language-php">$slide_types = array_count_values( $project_slider[0][0] );</code></pre>
<p>And you’ll want to change it to look like this…</p>
<pre><code class="language-php">//$slide_types = array_count_values( $project_slider[0][0] );
$slide_types = 0;</code></pre>
<p>Don’t forget to save your file! By adding two forward slashes in front of the original line you’re just commenting it out and telling WordPress to ignore it, so if you have any problems you can easily undo your change.</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>Flat comments seem pretty lame since the discussion areas often act like support forums, and many helpful bug reports and solutions get buried. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Glasses]]></title>
            <link>https://workingconcept.com/blog/glasses</link>
            <guid>https://workingconcept.com/blog/glasses</guid>
            <pubDate>Sun, 13 Jan 2013 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><em>Warning: I usually save personal posts for my private journal, but I feel like I should share this reflection at the risk of boring both my readers more than usual.</em></p>
<p>I got my first pair of glasses this week and I’m amazed at the literal change in perspective. My unaided vision isn’t horrible, but years in front of glowing rectangles have started to bear noticeable effect. On top of this, my eyes apparently have varying astigmatisms – meaning the wacky curvature of my eyeballs results in distortion. I had no idea, of course, because I’ve had my life thus far to adjust. Adding corrective lenses, however, is a headache: my "corrected" field of vision falls off more sharply and artificially to the left, so everything on my monitor is warped by a bizarre, horizontal gravity. Every close, flat surface tapers to follow a new and uncomfortable perspective. My world is suddenly crisp, richly detailed, and disturbingly shifted.</p>
<p>Here’s the cool part though: this is temporary because my brain will adjust. I’m already less distracted by the frames on my face, and after a few days my brain should adapt to signals that are different from those it’s been interpreting my whole life. How cool is that?</p>
<p>By positioning some shaped polycarbonate in front of my eyes, my quality of life improves. The hardware update does not require any software change. Sometimes I’m frustrated with the limitations of my own brain, and I forget that it’s a marvelous and powerful thing whether I appreciate it or not.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[[link] Design is Not Veneer]]></title>
            <link>https://workingconcept.com/blog/link-design-is-not-veneer</link>
            <guid>https://workingconcept.com/blog/link-design-is-not-veneer</guid>
            <pubDate>Sat, 29 Dec 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ve been moved by this article I came across today, which passionately clarifies the vast difference between design and decorating with enviable brevity and resolve. The implications are beyond design or any particular commercial endeavor, though the focus is on web design.</p>
<p>In other words, please read this. <a href="http://aralbalkan.com/notes/design-is-not-veneer/">Aral Balkan: Design is not veneer.</a></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Script: Copy Harvest Timesheet]]></title>
            <link>https://workingconcept.com/blog/script-copy-harvest-timesheet</link>
            <guid>https://workingconcept.com/blog/script-copy-harvest-timesheet</guid>
            <pubDate>Tue, 11 Dec 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>In March of this year, I left <a href="http://www.marketcircle.com/billings/">Billings</a> for the more cloud-friendly <a href="http://www.getharvest.com/">Harvest</a> time tracking and invoicing service. It was an excellent choice, and the only complaint I’ve had is that recently I’ll have to log in every day to duplicate the previous day’s Timesheet, making my usual billable buckets available and ready to go.</p>
<p>Since this is an annoying and painfully redundant task, I decided to cut it down to a script I can run with Alfred. For this, I’m using some simple PHP to execute commands via the <a href="http://www.getharvest.com/api">Harvest API</a>. I simply choose my new "Harvest - Duplicate Timesheet" command, a few seconds pass, and then the browser opens to my current Timesheet to confirm that all went well.</p>
<h2>What This Does</h2>
<ol>
<li>Searches a day at a time into the past (from today) to find the last-used Timesheet. I’ve limited the search to 10 days so we don’t get an infinite loop – you may need to adjust if you rarely work or take long vacations.</li>
<li>Duplicates each item from the last sheet onto today’s, with 0 hours.</li>
<li>Toggles the last timer. By default, the last timer will start out running. By toggling it, our new Timesheet will be ready to go with no items running.</li>
</ol>
<h2>What This Doesn’t Do</h2>
<p>Anything else. I made it for myself, and you’re free to improve it – it won’t be friendly warning you about invalid credentials or telling you that it can’t find a last-used Timesheet within the search period.</p>
<p>On the plus side, it would also have a hard time screwing anything up. It doesn’t modify existing data, it doesn’t delete anything, and it doesn’t change any timers. It’s pretty simple.</p>
<p>I guess I have to remind you that I offer no warranty, and should you ruin everything and blow up the planet while playing with my humble PHP, it’s not my fault.</p>
<h2>Making It Go (Three Steps)</h2>
<h3>1. Grab the Script</h3>
<p>Clone or save <a href="https://github.com/mattstein/duplicate-harvest-timesheet/blob/master/harvest-duplicate-timesheet.php">this PHP</a>, add your account credentials, and put it somewhere that it can be called upon. (I have a Google Drive folder of scripts, for example, which I can conveniently run with the same path on any machine.)</p>
<h3>2. Create an Alfred Trigger</h3>
<p>You could do this with cron so it’d be totally automated, but for some reason I still prefer Alfred.</p>
<ol>
<li>Open Alfred’s <strong>Extensions</strong> preference pane.</li>
<li>Click the little "+" (bottom left) to create a new <strong>Shell Script</strong>.</li>
<li>Add some basic information, a title, description, and choose a keyword that you like. I run this silently, but this is your choice of course.</li>
<li>In the Command box, add… <pre class="language-bash"><code>php ~/path/to/your/harvest-duplicate-timesheet.php
open https://shortname.harvestapp.com/time</code></pre> Change the path and account shortname to reflect your own details.</li>
<li>Save.</li>
</ol>
<h3>3. Run it!</h3>
<p>Use your new trigger, and if all goes well you’ll just wait a few seconds and your freshly-copied Timesheet will be staring right back at you from the browser.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Devot:ee Pride]]></title>
            <link>https://workingconcept.com/blog/devot-ee-pride</link>
            <guid>https://workingconcept.com/blog/devot-ee-pride</guid>
            <pubDate>Thu, 29 Nov 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>You’ve dealt with that person before, the one who gets way too excited over his infinitesimal achievement. Yesterday, dear reader, that person was me! Two goals accomplished:</p>
<ol>
<li>Post something useful, anything, even <a href="http://devot-ee.com/add-ons/dollars-fieldtype">a simple fieldtype</a>, to Devot:ee.</li>
<li>Experience the thrill of having a friendly stranger improve something I made. This was at the hands of <a href="http://elivz.com">Mr. Eli Van Zoeren</a>, who kindly made said fieldtype available for <a href="http://pixelandtonic.com/matrix">Matrix</a> and shared the result.</li>
</ol>
<p>Thanks again, Eli! And of course <a href="http://devot-ee.com/members/profile/ryan-masuga">Ryan Masuga</a>, whose hard work makes Devot:ee an essential resource.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[EllisLab and the Future of EE]]></title>
            <link>https://workingconcept.com/blog/ellislab-and-the-future-of-expressionengine</link>
            <guid>https://workingconcept.com/blog/ellislab-and-the-future-of-expressionengine</guid>
            <pubDate>Wed, 28 Nov 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>EllisLab recently made announcements that mark the beginning of … something. A redesigned site (with <a href="https://web.archive.org/web/20121128180202/https://ellislab.com/expressionengine">significant reorganization</a>, <a href="https://web.archive.org/web/20161003140726/https://ellislab.com/blog/entry/new-ceo-chief-maker-and-ellislab-focus">new CEO</a>, and the <a href="https://web.archive.org/web/20160318230220/http://ellislab.com/blog/entry/introducing-a-new-ellislab-support-experience">move to paid official support</a> were among changes publicized on the company blog. The pro network ended abruptly, and <a href="http://eeinsider.com/blog/the-new-ellislab.com-you-want-my-opinion/">thoughtful</a> <a href="http://www.hopstudios.com/blog/ten_reactions_to_ellislabs_major_policy_shifts_eecms">reactions</a> are making their way to the interweb.</p><p>People (myself included) can be resistent to change, and having to pay for something that’s always been free<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup> is a bummer if not worse. I also understand that change can be good, even essential, and I’m happy to pay for something that’s valuable to me.</p><p>While I truly hope that EllisLab has refocused itself to vigorously evolve and support excellent software, I’m pessimistic about what the changes could mean for its community and how that will shape ExpressionEngine’s future.</p><p>When I started using ExpressionEngine in 2007, it was an exciting endeavor. The control panel was one of very few that looked modern and visually considered, the documentation made it easy to start using powerful templating features, and the forums were already bustling with other users and EllisLab staff. I felt like I was just getting into something that did more than I needed, and where everybody was strangely friendly and had my back if I could ask good questions. (And in some cases, patient souls helped me understand how to ask better questions.)</p><p>ExpressionEngine became easier to recommend as I got more familiar with it. It worked for small clients and larger clients. I heard someone from the Huffington Post talk about caching and optimization for high-traffic sites, then helped build larger sites in the studio I worked at. It seems like the momentum just kept building, and ExpressionEngine and I kept changing at a steady pace. And it was good.</p><p>Now 2012. I rarely build an EE site anymore that <em>doesn’t</em> use <a href="http://buildwithstructure.com/">Structure</a>. Very often <a href="http://devot-ee.com/add-ons/matrix">Matrix</a>, <a href="http://devot-ee.com/add-ons/playa">Playa</a>, <a href="http://devot-ee.com/add-ons/wygwam">Wygwam</a>, and maybe a dozen addons are part of a project. The ExpressionEngine forums have seemed less active and less frequented by EllisLab staff. <a href="http://devot-ee.com/">Devot-ee</a> is where I go to hunt for an add-on that will save me hours of work. I get occasional contacts through <a href="http://director-ee.com/">Director-ee</a>. The Stack Exchange initiative seems to be gaining momentum and is now in <a href="http://expressionengine.stackexchange.com">public beta</a>. More and more of my meaningful time in the ExpressionEngine universe is outside of any official sites or support channels.</p><p>It seems like EllisLab knows this, and would like to get out of the business of facilitating community altogether, to get back to improving the quality and support of its software. It knows that it’s great for large sites, and that it can thrive where budgets are big enough to include ongoing support costs. It knows that it doesn’t need to appeal to the little guy anymore, that he’ll look elsewhere and find <del>Blocks</del> <a href="http://buildwithcraft.com/">Craft</a>, <a href="https://www.pyrocms.com/">PyroCMS</a>, <a href="http://laravel.com/">Laravel</a>, <a href="http://statamic.com/">Statamic</a>, and others. It seems smart.</p><p>The EE community wouldn’t exist without ExpressionEngine. I’ve benefitted from it, as have clients of mine large and small. What I’ll be interested to see is how ExpressionEngine changes with a less diverse and centralized user base.</p><p>I’m still just as confident that ExpressionEngine is an excellent choice for large projects, though I think it’s also a good time to be get to know other products that will fill the growing gap.</p><div class="footnotes"><hr><ol><li id="fn:1"><p>ExpressionEngine support has never been free, it’s just been included in that one-time license fee. This apparently hasn’t worked out well for EllisLab. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p></li></ol></div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Design & Human Experience]]></title>
            <link>https://workingconcept.com/blog/design-human-experience</link>
            <guid>https://workingconcept.com/blog/design-human-experience</guid>
            <pubDate>Sun, 18 Nov 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><blockquote>
<p><em>“</em>To design something really well you have to get it. You have to really grok what it’s all about. It takes a passionate commitment to thoroughly understand something – chew it up, not just quickly swallow it. Most people don’t take the time to do that. Creativity is just connecting things.</p>
<p>When you ask a creative person how they did something, they may feel a little guilty because they didn’t really do it, they just saw something. It seemed obvious to them after awhile. That’s because they were able to connect experiences they’ve had and synthesize new things. And the reason they were able to do that was that they’ve had more experiences or have thought more about their experiences than other people have. Unfortunately, that’s too rare a commodity. A lot of people in our industry haven’t had very diverse experiences. They don’t have enough dots to connect, and they end up with very linear solutions, without a broad perspective on the problem. The broader one’s understanding of the human experience, the better designs we will have.”</p>
</blockquote>
<p>– Steve Jobs</p>
<p><em>via <a href="https://web.archive.org/web/20150321200548/https://vanschneider.squarespace.com/journal/r6g1aie5v9e7xwzwrilkd7d1d6g17k">vanSchneider Blog</a></em></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Challenge of Structure Previews]]></title>
            <link>https://workingconcept.com/blog/the-challenge-of-structure-previews</link>
            <guid>https://workingconcept.com/blog/the-challenge-of-structure-previews</guid>
            <pubDate>Wed, 31 Oct 2012 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>It happened again today. Every time I introduce a client to the ExpressionEngine admin panel, the same question comes up: "how do I preview my changes before I make them live?" It’s a natural question for someone who wants to be sure their edits look right before putting them in front of a vast, faceless audience. And I’m still not sure how to answer.</p>
<p>Sure, there are plenty of ways to preview new posts. You can find articles, add-ons, and various methods for showing draft entries to the requisite few. That’s great.</p>
<p>But the elephant in the room is the Structure page, an existing and very live entry to which the client wants to make <em>and preview</em> edits. Our options are as follows:</p>
<h2>1. Upgrade to <a href="http://betterworkflow.electricputty.co.uk/">Better Workflow</a>.</h2>
<p>It’s a brilliant $65 add-on that, among other things, clones entries and takes over the task of managing their statuses. In other words, those edits take the form of a separate draft entry that can be fine-tuned and previewed without even a whiff of change on the public website. Drafted content can then be published, replacing the current live entry’s content. No copying and pasting, no changing entry IDs, all very smooth.</p>
<p>Better Workflow does not, however, fully support more exotic add-ons. This prevented us from using it, for example, on a large site that benefits from Pixel & Tonic’s <a href="http://pixelandtonic.com/assets">Assets</a>. And we were really, really bummed to part with Better Workflow – though hopefully Assets will be supported someday.</p>
<h2>2. Tinker with live updates at 4am, making iterations while the audience is hopefully sleeping.</h2>
<p>I’m sure lots of us do this, but that doesn’t make it any more attractive to a client who bothers to get decent sleep.</p>
<h2>3. Clone the page to a different URL, edit, then copy successful changes back to the original.</h2>
<p><a href="http://devot-ee.com/add-ons/mx-cloner">MX Cloner</a> can take the pain out of cloning entries. Using that, we can get a reasonable little workflow:</p>
<ul>
<li>Clone the page you’d like to edit, changing the URL and "Hide from nav?" to "Yes" if that’s relevant.</li>
<li>Submit changes to your heart’s content, previewing at your secret, public URL. (You could also change the status and restrict preview privileges to logged-in users if need be.)</li>
<li>Either scramble to simultaneously retire the old entry and replace it with the new one (URL + status changes), or copy and paste your changes one field at a time to the existing entry. For the latter, you’ll be angry if you used Matrix, Playa, or really anything other than a textarea of some sort.</li>
</ul>
<p>It’s that last part that makes this method feel a bit silly.</p>
<h2>4. Use a staging environment for managing changes, then push to the production environment when it’s time.</h2>
<p>This seems like a pretty clean way to do it in theory, but what about those things that pull the two environments out of sync? There could be Freeform submissions, template tracking updates, etc. A straightforward database + filesystem transfer could wipe out important information on the production side.</p>
<h2>Conclusion: there’s still no perfect way to preview Structure page edits.</h2>
<p>After all this time, Better Workflow seems like the only option that doesn’t require the client to jump through hoops. (Assuming the additional $65 and more limited add-on support isn’t a hoop.)</p>
<p>It’s tough to ask smaller clients to invest in a flexible, modern CMS where a pretty basic (and fair, I think) expectation of previewing content can’t be met.</p>
<p>Am I alone thinking this? Am I missing something? Is this just a sign that I’ve identified the add-on I need to make? Let me know what you think in the comments!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Retina Images]]></title>
            <link>https://workingconcept.com/blog/retina-images</link>
            <guid>https://workingconcept.com/blog/retina-images</guid>
            <pubDate>Thu, 21 Jun 2012 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I noticed how poor the internet seemed to look on Apple’s retina iPad, and for whatever reason this new retina MacBook Pro seems to have more people focused on the issue of retina graphics and the internet. I happened upon two nice-looking solutions (<a href="http://retinaimag.es/" target="_blank">Retina Images</a> and <a href="http://retinajs.com/" target="_blank">retina.js</a>) and implemented Retina Images on this site. I chose it over the latter because it serves the desired image once and works with CSS background images.</p>
<p>Retina Images gets the device pixel ratio and stores it in a cookie. Every image request is routed through a PHP file, which evaluates the cookie and whether a 2x image equivalent exists in the filesystem. The correct image is then returned. If any part of the process breaks (no cookies, for example), the original image is served. Pretty clever! The Retina Images site has <a href="http://retinaimag.es/#setupserver" target="_blank">instructions for setup</a>, so all I have left are a few tips:</p>
<ol>
<li>Make sure your “@2x” images are exactly double the pixel dimensions (not the dpi).</li>
<li>Hope that your source Photoshop files use smart objects liberally. Bonus points for having them already snapped to the nearest pixel.</li>
<li>The Retina Images PHP file has a handy (and optional) debug log that you can switch on if things aren’t working out quite right.</li>
</ol>
<p>It seems likely that future specs will make room for device resolution, but Retina Images can at least take some of the ugly out of the internet today.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mercurial on a MediaTemple DV 4 Server]]></title>
            <link>https://workingconcept.com/blog/mercurial-on-a-mediatemple-dv-4-server</link>
            <guid>https://workingconcept.com/blog/mercurial-on-a-mediatemple-dv-4-server</guid>
            <pubDate>Wed, 30 May 2012 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I stick with package managers when I can. In this case, <code>yum</code> would do the trick but it isn’t aware of Mercurial by default. (Try <code>yum install mercurial</code> with a fresh DV4 and it won’t know what to do.) To help it out, we’ll add another catalog of sorts, called RPMforge, that it can search through. We just need to get the right catalog, load it up, and tell yum to install Mercurial again.</p>
<ol>
<li>Make sure you’ve got <a href="http://kb.mediatemple.net/questions/1564/Installing+YUM+on+a+%28dv%29+Dedicated-Virtual+Server#dv_40" target="_blank">yum installed</a> and your <a href="http://kb.mediatemple.net/questions/625/How+do+I+enable+root+access+to+my+%28dv%29%3F#dv" target="_blank">root access is enabled</a>.</li>
<li>SSH into your server as the root user.</li>
<li>Create a temporary folder <code>mkdir rpm-download</code>.</li>
<li>Get all up in that directory <code>cd rpm-download</code>.</li>
<li>Download the <a href="http://wiki.centos.org/AdditionalResources/Repositories/RPMForge" target="_blank">latest x86_64 RPMforge package for CentOS 5</a> <code>wget https://www.rpmfind.net/linux/dag/redhat/el5/en/x86_64/dag/RPMS/rpmforge-release-0.5.2-2.el5.rf.x86_64.rpm</code>.</li>
<li>Add this repository to our search set <code>rpm -ivh rpmforge-release-0.5.2-2.el5.rf.x86_64.rpm</code></li>
<li>Install Mercurial! <code>yum install mercurial</code></li>
<li>Make sure it worked <code>hg</code></li>
</ol></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Brief Moment of Clarity]]></title>
            <link>https://workingconcept.com/blog/a-brief-moment-of-clarity</link>
            <guid>https://workingconcept.com/blog/a-brief-moment-of-clarity</guid>
            <pubDate>Mon, 27 Feb 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>What do I do again?</h2>
<p>I love solving problems, I’m fascinated with the web, technology, and putting things together that can make people’s lives better in some way. I’m used to wearing a lot of hats, and I like it that way, but I’m constantly forced to choose between focusing on one or two things to be really good at, or chasing a whole bunch of interests at the expense of never being The Expert at any of them.</p>
<p>A smart person I used to work for told me that the work I take will be the kind of work I keep getting. To my unamazement, this has proved true. I’ve learned that low-budget, ASAP projects where I’d occupy a narrow role are the worst to take on. They offer the most stress for the least pay and tend to be a drain on everyone involved. On the other hand, I work extremely well in situations where I’m the glue; there’s a big, undefined problem that needs to be identified, solved, and executed. It may involve translating between people with different interests and skill sets, or helping a small client solve big problems on a small budget. It may mean researching and building some weirdly-specific thing that nobody else could figure out how to do. I love this stuff, partly because I’m hardwired to take pleasure in organizing things, and partly because the conversations and relationships involved are engaging and interesting.</p>
<h2>And Then There Were Answers</h2>
<p>Through trial and error, I’ve finally started to figure out where my interests and abilities overlap. This makes it infinitely easier to know how to choose projects and who I’m looking for to collaborate with.</p>
<p>Things I know now…</p>
<p><strong>I’m a good designer and a lousy artist.</strong></p>
<p>You can argue that there’s overlap between the two, and I’ll agree to some extent. But I’m convinced that a designer is a person who solves problems, and an artist is a person who creates (or at least observes and interprets if you’re in the "nothing is new" camp).</p>
<p><strong>Reliability is something I sell.</strong></p>
<p>I want to be the person with the killer Dribbble feed, or the eagle-eyed pro that spots new trends before they’re big. But I’m neither. One of my most useful traits isn’t sexy or even all that interesting: I do what I say I will, when I’ve said I’ll do it. I try and communicate clearly, spell and punctuate like I give a damn, and actively take ownership of any mistakes while doing whatever I can to make them right. I’ve learned that, despite being hopelessly dull, this is a desirable trait.</p>
<p><strong>Work does not happen in a vacuum.</strong></p>
<p>I spend an awful lot of time at a desk by myself, and sometimes fall into the trap of believing that I work on my own. The reality, however, is that every project I work on is the result of a process, often with lots of focused communication. A huge part of what I do is to cultivate relationships that allow for efficient problem solving, be it with clients, collaborators, or both.</p>
<p><strong>My spirit animal is a stodgy old man.</strong></p>
<p>I can’t help it: I think that capitalization, punctuation, spelling, and grammar count for something. I don’t think Facebook, Twitter, or many newfangled ways to communicate make thoughtful communication obsolete. Intentionally crappy drawings don’t impress me, regardless of what the hipster holding the poster looks like. Maybe I’m drawn to things that seem inherently well-considered. Maybe I’m just a jerk.</p>
<h2>The Inevitable Redesign</h2>
<p>With these realizations, and even before articulating them, I decided it was time to redesign my tiny website. The old site, despite being tiny as well, was wasting time spreading a minimal amount of content over a few pages, and started using silly visual motifs that were more whim than anything else. So I did what felt right: focused, simplified, and made sure that design, responsiveness, and page load times would support the myriad of browsers and devices that could end up at my humble little domain. Caslon feels way more appropriate for me than Museo ever did. And as fun as it is to add excessive textures and faux embossing to type, it’s just not me. I’m also not bold enough to build a site without a stylesheet altogether. I only considered it in the moment while writing, but I suppose I’m not purist enough to take it that far.</p>
<h2>End of Post</h2>
<p>I’m not sure that this post will be meaningful for anyone but me, but it feels important to leave a note for my future self that says, "remember you’ve been learning things." After all, no matter what I end up doing next, no matter how wonderfully or terribly it goes, I can take pride in having learned things and helped people solve problems.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[HostGator Does Not Support IPv6]]></title>
            <link>https://workingconcept.com/blog/hostgator-does-not-support-ipv6</link>
            <guid>https://workingconcept.com/blog/hostgator-does-not-support-ipv6</guid>
            <pubDate>Thu, 13 Oct 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>After <a href="https://workingconcept.com//blog/cloudflare-review-bad-logo-amazing-service">getting really excited about CloudFlare</a>, I was setting it up for a client’s WordPress site when it disappeared during a DNS update. The CloudFlare-served error told me that the web host wasn’t serving data to web traffic, which seemed odd.</p>
<p>The web host was <a href="http://www.hostgator.com/" target="_blank">HostGator</a> (shared reseller account), and a chat technician confirmed my suspicion: they do not (yet) support IPv6 mapping that CloudFlare provides. So if you’ve enabled the IPv6 feature from CloudFlare’s settings and your site suddenly disappears, make sure your host supports the IPv6 protocol. Disabling the feature (it’s just a feature you can disable!!) restored the blog to its happy former state. That is all.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CloudFlare Review: Bad Logo, Amazing Service]]></title>
            <link>https://workingconcept.com/blog/cloudflare-review-bad-logo-amazing-service</link>
            <guid>https://workingconcept.com/blog/cloudflare-review-bad-logo-amazing-service</guid>
            <pubDate>Thu, 22 Sep 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I host several projects on MediaTemple DV plans for the stability, flexibility, and top-notch support. (I’d rather not be troubleshooting server issues for clients at 3AM.) You can certainly use <a href="https://www.cloudflare.com" target="_blank">CloudFlare</a> without MediaTemple hosting, they’ve just made it a one-step add-on service that you’d have to be a drooling idiot to mess up. I warily enabled the service for a static site, which involved signing up for a free account (email address, password) and enabling the service in MediaTemple’s control panel. A CNAME record was automatically created in the DNS settings, and that’s it.</p>
<p>I visited said site and it had an immediately-noticable zing. Excitedly, I added the service to workingconcept.com to see how <a href="https://www.cloudflare.com" target="_blank">CloudFlare</a> would cripple the ExpressionEngine setup. I was sure it’d fail and I’d have to revert, but I tried it anyway. <strong>I was blown away: CloudFlare seemed to have absolutely no effect but to dramatically speed up page delivery.</strong> Below is my non-scientific evaluation…</p>
<h3><a href="http://just-ping.com" target="_blank">just-ping.com</a> results, before CloudFlare (MediaTemple DV4.0, Virginia datacenter)</h3>
<table><tbody><tr class="head"><td><strong>Location</strong></td>
 <td><strong>min. rrt</strong></td>
 <td><strong>avg. rrt</strong></td>
 <td><strong>max. rrt</strong></td>
 <td><strong>IP</strong></td>
 </tr><tr><td>Singapore, Singapore</td>
 <td>262.2</td>
 <td>270.5</td>
 <td>286.5</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Amsterdam2, Netherlands</td>
 <td>84.7</td>
 <td>85.1</td>
 <td>87.5</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Florida, U.S.A.</td>
 <td>25.9</td>
 <td>26.0</td>
 <td>26.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Amsterdam3, Netherlands</td>
 <td>84.4</td>
 <td>85.2</td>
 <td>90.5</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Hong Kong, China</td>
 <td>233.3</td>
 <td>235.8</td>
 <td>237.3</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Sydney, Australia</td>
 <td>233.8</td>
 <td>234.0</td>
 <td>234.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Munchen, Germany</td>
 <td>96.4</td>
 <td>96.5</td>
 <td>96.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Cologne, Germany</td>
 <td>86.6</td>
 <td>86.7</td>
 <td>86.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>New York, U.S.A.</td>
 <td>10.0</td>
 <td>10.1</td>
 <td>10.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Amsterdam1, Netherlands</td>
 <td>88.6</td>
 <td>88.6</td>
 <td>88.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Stockholm, Sweden</td>
 <td>118.6</td>
 <td>118.7</td>
 <td>118.8</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Santa Clara, U.S.A.</td>
 <td>88.2</td>
 <td>88.7</td>
 <td>89.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Vancouver, Canada</td>
 <td>87.7</td>
 <td>87.9</td>
 <td>88.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>London, United Kingdom</td>
 <td>78.5</td>
 <td>79.0</td>
 <td>79.3</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Madrid, Spain</td>
 <td>132.1</td>
 <td>150.2</td>
 <td>167.5</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Padova, Italy</td>
 <td>107.3</td>
 <td>107.7</td>
 <td>109.0</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Austin, U.S.A.</td>
 <td>58.1</td>
 <td>58.3</td>
 <td>58.6</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Amsterdam, Netherlands</td>
 <td>85.6</td>
 <td>85.7</td>
 <td>85.9</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Paris, France</td>
 <td>98.8</td>
 <td>99.0</td>
 <td>99.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Melbourne, Australia</td>
 <td>254.3</td>
 <td>255.1</td>
 <td>256.4</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Shanghai, China</td>
 <td>208.2</td>
 <td>208.5</td>
 <td>208.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Copenhagen, Denmark</td>
 <td>108.1</td>
 <td>108.2</td>
 <td>108.4</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Lille, France</td>
 <td>82.4</td>
 <td>86.8</td>
 <td>93.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Zurich, Switzerland</td>
 <td>108.5</td>
 <td>108.7</td>
 <td>109.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Mumbai, India</td>
 <td>192.6</td>
 <td>203.0</td>
 <td>227.3</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Chicago, U.S.A.</td>
 <td>32.3</td>
 <td>32.4</td>
 <td>32.9</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Nagano, Japan</td>
 <td>177.3</td>
 <td>177.3</td>
 <td>177.5</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Haifa, Israel</td>
 <td>149.6</td>
 <td>150.9</td>
 <td>153.3</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Auckland, New Zealand</td>
 <td>200.2</td>
 <td>205.7</td>
 <td>218.4</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Antwerp, Belgium</td>
 <td>92.4</td>
 <td>92.5</td>
 <td>92.8</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Groningen, Netherlands</td>
 <td>87.7</td>
 <td>88.0</td>
 <td>88.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Moscow, Russia</td>
 <td>136.4</td>
 <td>136.6</td>
 <td>136.9</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Dublin, Ireland</td>
 <td>97.6</td>
 <td>97.6</td>
 <td>97.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Oslo, Norway</td>
 <td>112.6</td>
 <td>115.2</td>
 <td>136.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Kharkov, Ukraine</td>
 <td>156.0</td>
 <td>156.4</td>
 <td>160.0</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Manchester, United Kingdom</td>
 <td>80.1</td>
 <td>80.4</td>
 <td>82.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Vilnius, Lithuania</td>
 <td>119.1</td>
 <td>119.3</td>
 <td>119.6</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Bucharest, Romania</td>
 <td>132.8</td>
 <td>133.0</td>
 <td>133.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Kuala Lumpur, Malaysia</td>
 <td>253.4</td>
 <td>253.7</td>
 <td>254.0</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Jakarta, Indonesia</td>
 <td>283.9</td>
 <td>284.7</td>
 <td>290.6</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Cape Town, South Africa</td>
 <td>226.1</td>
 <td>226.2</td>
 <td>226.3</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Glasgow, United Kingdom</td>
 <td>85.4</td>
 <td>86.8</td>
 <td>97.6</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Lisbon, Portugal</td>
 <td>117.6</td>
 <td>117.7</td>
 <td>117.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Chicago, U.S.A.</td>
 <td>31.3</td>
 <td>31.4</td>
 <td>31.5</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Dallas, U.S.A.</td>
 <td>35.5</td>
 <td>35.7</td>
 <td>36.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Buenos Aires, Argentina</td>
 <td>156.6</td>
 <td>156.9</td>
 <td>157.2</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Istanbul, Turkey</td>
 <td>137.5</td>
 <td>137.7</td>
 <td>138.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Gdansk, Poland</td>
 <td>114.5</td>
 <td>116.2</td>
 <td>128.9</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Beijing, China</td>
 <td>295.4</td>
 <td>307.4</td>
 <td>314.7</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Belgrade, Serbia</td>
 <td>113.8</td>
 <td>118.4</td>
 <td>137.8</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Toronto, Canada</td>
 <td>22.5</td>
 <td>22.8</td>
 <td>23.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Novosibirsk, Russia</td>
 <td>190.3</td>
 <td>191.3</td>
 <td>192.1</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Athens, Greece</td>
 <td>135.3</td>
 <td>135.7</td>
 <td>136.9</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Frankfurt, Germany</td>
 <td>92.5</td>
 <td>93.4</td>
 <td>94.9</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Sofia, Bulgaria</td>
 <td>118.4</td>
 <td>118.8</td>
 <td>119.4</td>
 <td>72.10.32.198</td>
 </tr><tr><td>Budapest, Hungary</td>
 <td>109.8</td>
 <td>114.0</td>
 <td>127.4</td>
 <td>72.10.32.198</td>
 </tr></tbody></table>
<h3><br></h3>
<h3><a href="http://just-ping.com" target="_blank">just-ping.com</a> results, with CloudFlare</h3>
<table><tbody><tr><td><strong>Location</strong></td>
 <td><strong>min. rrt</strong></td>
 <td><strong>avg. rrt</strong></td>
 <td><strong>max. rrt</strong></td>
 <td><strong>IP</strong></td>
 </tr><tr><td>Singapore, Singapore</td>
 <td>176.3</td>
 <td>186.3</td>
 <td>193.1</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Amsterdam2, Netherlands</td>
 <td>1.2</td>
 <td>1.5</td>
 <td>1.7</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Florida, U.S.A.</td>
 <td>26.7</td>
 <td>27.0</td>
 <td>27.5</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Amsterdam3, Netherlands</td>
 <td>0.7</td>
 <td>1.0</td>
 <td>1.2</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Hong Kong, China</td>
 <td>2.1</td>
 <td>2.5</td>
 <td>3.2</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Sydney, Australia</td>
 <td>157.2</td>
 <td>157.6</td>
 <td>158.4</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Munchen, Germany</td>
 <td>19.1</td>
 <td>19.3</td>
 <td>20.0</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Cologne, Germany</td>
 <td>4.8</td>
 <td>5.1</td>
 <td>5.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>New York, U.S.A.</td>
 <td>4.9</td>
 <td>5.1</td>
 <td>5.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Amsterdam1, Netherlands</td>
 <td>0.7</td>
 <td>1.0</td>
 <td>1.4</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Stockholm, Sweden</td>
 <td>24.6</td>
 <td>25.0</td>
 <td>25.5</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Santa Clara, U.S.A.</td>
 <td>3.5</td>
 <td>3.9</td>
 <td>4.2</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Vancouver, Canada</td>
 <td>75.4</td>
 <td>76.3</td>
 <td>77.8</td>
 <td>173.245.60.113</td>
 </tr><tr><td>London, United Kingdom</td>
 <td>10.3</td>
 <td>10.7</td>
 <td>11.0</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Madrid, Spain</td>
 <td>34.6</td>
 <td>38.4</td>
 <td>49.1</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Padova, Italy</td>
 <td>26.1</td>
 <td>32.1</td>
 <td>57.8</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Austin, U.S.A.</td>
 <td>29.7</td>
 <td>29.9</td>
 <td>30.0</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Amsterdam, Netherlands</td>
 <td>0.9</td>
 <td>1.1</td>
 <td>1.6</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Paris, France</td>
 <td>90.5</td>
 <td>91.0</td>
 <td>91.9</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Melbourne, Australia</td>
 <td>123.9</td>
 <td>124.8</td>
 <td>126.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Shanghai, China</td>
 <td>267.0</td>
 <td>273.3</td>
 <td>278.1</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Copenhagen, Denmark</td>
 <td>15.6</td>
 <td>16.5</td>
 <td>17.3</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Lille, France</td>
 <td>17.2</td>
 <td>26.8</td>
 <td>33.1</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Zurich, Switzerland</td>
 <td>14.5</td>
 <td>14.8</td>
 <td>15.2</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Mumbai, India</td>
 <td>76.4</td>
 <td>77.0</td>
 <td>77.7</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Chicago, U.S.A.</td>
 <td>0.3</td>
 <td>0.4</td>
 <td>0.6</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Nagano, Japan</td>
 <td>6.0</td>
 <td>6.1</td>
 <td>6.4</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Haifa, Israel</td>
 <td>91.1</td>
 <td>96.7</td>
 <td>100.4</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Auckland, New Zealand</td>
 <td>149.9</td>
 <td>150.1</td>
 <td>150.4</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Antwerp, Belgium</td>
 <td>11.0</td>
 <td>11.3</td>
 <td>11.8</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Groningen, Netherlands</td>
 <td>4.4</td>
 <td>5.0</td>
 <td>5.6</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Moscow, Russia</td>
 <td>46.4</td>
 <td>46.7</td>
 <td>47.1</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Dublin, Ireland</td>
 <td>18.7</td>
 <td>18.9</td>
 <td>19.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Oslo, Norway</td>
 <td>32.0</td>
 <td>32.2</td>
 <td>32.6</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Kharkov, Ukraine</td>
 <td>104.0</td>
 <td>104.4</td>
 <td>108.0</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Manchester, United Kingdom</td>
 <td>13.2</td>
 <td>13.8</td>
 <td>14.4</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Vilnius, Lithuania</td>
 <td>36.5</td>
 <td>37.4</td>
 <td>42.1</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Bucharest, Romania</td>
 <td>35.2</td>
 <td>36.1</td>
 <td>41.9</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Kuala Lumpur, Malaysia</td>
 <td>195.6</td>
 <td>196.1</td>
 <td>197.1</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Jakarta, Indonesia</td>
 <td>48.6</td>
 <td>48.9</td>
 <td>49.3</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Cape Town, South Africa</td>
 <td>231.4</td>
 <td>231.7</td>
 <td>232.1</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Glasgow, United Kingdom</td>
 <td>20.7</td>
 <td>20.9</td>
 <td>21.3</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Lisbon, Portugal</td>
 <td>58.8</td>
 <td>59.1</td>
 <td>59.5</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Chicago, U.S.A.</td>
 <td>1.7</td>
 <td>2.0</td>
 <td>2.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Dallas, U.S.A.</td>
 <td>1.3</td>
 <td>1.5</td>
 <td>2.0</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Buenos Aires, Argentina</td>
 <td>152.6</td>
 <td>153.2</td>
 <td>156.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Istanbul, Turkey</td>
 <td>38.8</td>
 <td>39.2</td>
 <td>39.7</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Gdansk, Poland</td>
 <td>42.0</td>
 <td>42.2</td>
 <td>42.7</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Beijing, China</td>
 <td>377.7</td>
 <td>393.9</td>
 <td>406.0</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Belgrade, Serbia</td>
 <td>32.8</td>
 <td>39.9</td>
 <td>63.2</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Toronto, Canada</td>
 <td>13.8</td>
 <td>13.9</td>
 <td>14.2</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Novosibirsk, Russia</td>
 <td>99.4</td>
 <td>99.9</td>
 <td>100.4</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Athens, Greece</td>
 <td>66.5</td>
 <td>67.1</td>
 <td>70.8</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Frankfurt, Germany</td>
 <td>13.6</td>
 <td>14.3</td>
 <td>15.3</td>
 <td>173.245.60.113</td>
 </tr><tr><td>Sofia, Bulgaria</td>
 <td>39.1</td>
 <td>39.2</td>
 <td>39.4</td>
 <td>173.245.60.40</td>
 </tr><tr><td>Budapest, Hungary</td>
 <td>29.8</td>
 <td>32.7</td>
 <td>50.5</td>
 <td>173.245.60.40</td>
 </tr></tbody></table>
<p>As you can see, ping times were easily improved across the board. Remember that this is less than an hour after enabling the service.</p>
<h2>Pingdom Full Page Test</h2>
<p>I use <a href="https://www.pingdom.com/" target="_blank">Pingdom</a> to monitor my own website and client web apps, and they’ve got all kinds of great tools. Here I used the <a href="http://tools.pingdom.com/fpt/" target="_blank">Full Page Test</a> to compare page load time. This test loads an entire page and basically gives you an overview of the process: how long it takes, what assets are loaded, the order in which things stack up.</p>
<p><strong>The result: page load from 4.7 seconds to 1.8 seconds.</strong></p>
<p>I did nothing but switch on the service. I’d say that’s a significant plus for a product that’s totally free.</p>
<h2>Conclusion</h2>
<p>Yesterday I could have cared less about CloudFlare, but today I suddenly can’t live without it. I’m thoroughly impressed and hope this post is helpful to somebody. I don’t work for CloudFlare or get any sort of incentive for writing this post, but I would certainly take incentives if they were offered. I’ve already been experimenting with <a href="http://aws.amazon.com/s3/" target="_blank">Amazon S3</a> as a CDN, and <a href="http://www.rackspace.com/cloud/cloud_hosting_products/files/" target="_blank">Rackspace CloudFiles</a> – both offered inexpensive performance gains with minimal setup, but CloudFiles is another thing entirely! The pro and enterprise upgrades offer increased protection and fine-tuning along with a variety of genuinely-enticing features. As of this moment I’m still using the free account, which is serving pages much faster and allegedly adding a layer of protection from various types of attacks. Once they fix that placeholder logo of theirs I’ll be proudly recommending it to everybody I can find!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Now with More Florida]]></title>
            <link>https://workingconcept.com/blog/now-with-more-florida</link>
            <guid>https://workingconcept.com/blog/now-with-more-florida</guid>
            <pubDate>Thu, 14 Jul 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Working Concept is now based in Lake Mary, Florida. I plan on continuing work with my excellent Pacific Northwest and California clients and look forward to meeting folks in the Orlando area!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Auto-Sizing Textareas with jQuery]]></title>
            <link>https://workingconcept.com/blog/auto-sizing-textareas-with-jquery</link>
            <guid>https://workingconcept.com/blog/auto-sizing-textareas-with-jquery</guid>
            <pubDate>Sun, 22 May 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Every website that I build ends up being better than the last. Something becomes more efficient, or some new little detail becomes standard. One such detail has been automatically-sizing textareas, similar to Facebook’s. It’s a simple idea: the user types more text, the text area gets bigger. It requires no extra UI, it makes perfect sense as it happens, and it degrades without issue. The jQuery plugin I keep coming back to is <a href="http://james.padolsey.com/demos/plugins/jQuery/autoresize.jquery.js/view" target="_blank" rel="nofollow">James Padolsey’s autoResize</a>.</p>
<p>I’ve also made a habit of having textareas size automatically when the page loads, in the even that there’s already text in the field to be edited:</p>
<pre><code class="language-js">$('textarea.autosize').autoResize().trigger('change');</code></pre>
<p><em>Update: since my original link died, you may instead want to check out <a href="https://github.com/jackmoore/autosize">Jack Moore’s similar jQuery plugin</a>.</em></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WordPress Database Flakiness]]></title>
            <link>https://workingconcept.com/blog/wordpress-database-flakiness</link>
            <guid>https://workingconcept.com/blog/wordpress-database-flakiness</guid>
            <pubDate>Thu, 17 Mar 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>We’ve all been there with a WordPress install: “Can’t Establish a Database Connection”.</p>
<p>I made sure that my connection info was correct, and unlike every other PHP app install, I continued to get the error.</p>
<p>Assuming I still managed to get something wrong, I was good sport and used <a href="http://www.tizag.com/mysqlTutorial/mysqlconnection.php" target="_blank">a barebones PHP script</a> to test the connection info. To my surprise, it worked. The problem had to be with WordPress.</p>
<p>After a few hours of experimenting, I found the problem. I had created a test working copy on my public development server; the WordPress install I was using locally at “wp.localmachine.com” moved to “wp.devmachine.com”, and I had simply copied over the same database. <strong>The WordPress database references the site URL in several places, and you need to update these URLs in MySQL for your alternate site to work.</strong> The easiest way to do this is to dump your database to a .sql file, then find and replace the original domain (“localmachine.com”) with the new one (“devmachine.com”). Then import the resulting .sql file on the new server.</p>
<p>If you’d rather update the new database by hand, I believe you’ll need to change domain references in wp_site, wp_options, and wp_blogs. But be careful, wp_posts and several other areas will have references to your old domain.<br></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Good Introduction to HTML5]]></title>
            <link>https://workingconcept.com/blog/good-introduction-to-html5</link>
            <guid>https://workingconcept.com/blog/good-introduction-to-html5</guid>
            <pubDate>Sun, 29 Aug 2010 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>A List Apart’s first publication, “<a href="http://books.alistapart.com/product/html5-for-web-designers" target="_blank">HTML5 for Web Designers</a>,” is both an easy one-sitting read and a warm introduction to HTML5. I dare you to try reading it and not geek out, even just a little bit, over what’s just around the corner.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Firefox 3.6.3 Cache Issue]]></title>
            <link>https://workingconcept.com/blog/firefox-3-6-3-cache-issue</link>
            <guid>https://workingconcept.com/blog/firefox-3-6-3-cache-issue</guid>
            <pubDate>Tue, 01 Jun 2010 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ve been customizing <a href="http://expressionengine.com/forums/viewthread/38361/" target="_blank">Mark Huot’s Livesearch extension for ExpressionEngine</a>, which combines the livesearch jQuery plugin with an ExpressionEngine plugin for returning formatted query results.</p>
<p>The livesearch JavaScript has a built-in caching mechanism that didn’t seem to be clearing in Firefox. In Safari, IE, and others, valid search results (weblog entries) were being returned as one would expect. In Firefox, old/deleted entries were being returned as search results. In Firefox 3.6.3 (both for myself and my client), the browser cache couldn’t be completely cleared.</p>
<p>The only way to fix the problem was to view the JavaScript file (jquery.livesearch.js) directly in the browser, then hold shift and click refresh.</p>
<p>I also updated the headers to prevent excessive caching in the first place.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Need for Speed]]></title>
            <link>https://workingconcept.com/blog/the-need-for-speed</link>
            <guid>https://workingconcept.com/blog/the-need-for-speed</guid>
            <pubDate>Wed, 21 Apr 2010 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I like SEO (search engine optimization) because there’s always something to tweak or improve. My latest quest has been to improve the overall speed of my sites and my clients’ sites. To do this, I’ve started using two tools:</p>
<ol><li>
 <a href="https://www.pingdom.com/" target="_blank">Pingdom</a>
 This is a web app that monitors your site(s) and gives you reports on uptime and latency, among other things. I use it to see when my site is fastest and slowest, and to get some idea of what the site’s response times are around the world.
 </li>
 <li>
 <a href="https://addons.mozilla.org/en-US/firefox/addon/5369" target="_blank">YSlow</a>
 An outrageously fun (geeky fun) Firefox plugin that will grade your site in various categories, and its companion website will help explain what you need to do in order to start speeding things up.
 </li>
</ol></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing the Working Concept Blog]]></title>
            <link>https://workingconcept.com/blog/introducing-the-working-concept-blog</link>
            <guid>https://workingconcept.com/blog/introducing-the-working-concept-blog</guid>
            <pubDate>Sun, 18 Apr 2010 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Never satisfied with anything, I decided to move my blog—formerly called Spontaneous Noise with good reason—over to the company site and run it with ExpressionEngine.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rolling Your Own SVN Server]]></title>
            <link>https://workingconcept.com/blog/rolling-your-own-svn-server</link>
            <guid>https://workingconcept.com/blog/rolling-your-own-svn-server</guid>
            <pubDate>Mon, 22 Feb 2010 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>It finally dawned on me that using one subversion repository for all my projects was silly. If each project has its own repository, there are some benefits:</p>
<ol>
<li>My revision comments make lots of sense and follow a logical path.</li>
<li>My revision numbers are more meaningful, and pertain to each project directly.</li>
<li>I can easily share access to one project with someone else if I need to, without exposing all of my work or potentially sensitive information.</li>
<li>I can join the rest of the world that’s using subversion properly.</li>
</ol>
<p>So why wasn’t I doing this sooner? I learned to use subversion this way, and it actually helped when I decided to use <a href="http://beanstalkapp.com/" target="_blank">Beanstalk</a>. Beanstalk is awesome, provides lots of hooks and cool features, and is just lovely. The obstacle in my repository-for-every-project quest was Beanstalk’s limit on repositories. With the $15/month account, I could only have 10 repositories. I could pay more for additional repositories, but I’m cheap and ambitious and there seemed like there had to be a better way. And there was. Here’s how I set up stylish subversion hosting with unlimited repositories for $19.95 a month.</p>
<h2>Step 1: Sign up for Linode account.</h2>
<p>I loved <a href="http://slicehost.com/" target="_blank">Slicehost</a> but was always curious about <a href="http://linode.com/" target="_blank">Linode</a>; you get just a little bit more for a teeny bit less, and like Slicehost they seem to have a great reputation. I signed up for a Linode 360 account, which gets me blazing fast access to 16GB of storage, 360GB of memory, and 200GB of transfer per month.</p>
<h2>Step 2: Prep Linode account.</h2>
<p>Linode’s control panel makes it easy to get set up quickly. I installed Ubuntu 8.04 LTS (free). Done.</p>
<h2>Step 3: Set up the server.</h2>
<p>Equally brainless was installing VirtualMin GPL (free) using the <a href="http://www.webmin.com/vinstall.html" target="_blank">install script</a>, which is designed to work with Ubuntu 8.04. VirtualMin makes it easy to set up virtual hosts and repositories with a web-based GUI. I love the shell, but I find it easier to administer the web server with a GUI like VirtualMin. My brain only has room for about 10 terminal commands before I have to start looking things up. I then followed <a href="http://articles.slicehost.com/2008/4/25/ubuntu-hardy-setup-page-1" target="_blank">PickledOnion’s iptables setup instructions</a> (scroll down to find it) to secure things a bit.</p>
<h2>Step 4: Make some test repositories.</h2>
<p>Log into VirtualMin, set up a virtual server, and add some repositories. Ridiculously easy. I access my subversion repositories via a URL like <code>https://example.com/svn/repository-name</code>. Tested connecting with <a href="http://versionsapp.com/" target="_blank">Versions</a>, and everything worked just fine and very quickly. I dumped my Beanstalk repository, then imported it to the new server using VirtualMin.</p>
<h2>Step 5: Split projects from old setup into their own individual repositories.</h2>
<p>I was sailing along feeling clever and upbeat, and then this step was like a punch below the belt. For your benefit, dear reader, I’ll share what worked rather than the grim tale of how I got there.</p>
<ol><li>SSH’d into server via Terminal.</li>
 <li>
 <p>Dumped my current repository.</p><pre class="language-bash"><code>svnadmin dump /home/domain/svn/oldrepo &gt; oldrepo.svndump</code></pre></li><li><p>Split off a single project into its own dump, dropping unrelated revisions and renumbering them. (Note: folder structure is important here, and you should know that a project in my old repository had a path like <code>/projects/projectname/trunk/</code>.</p><pre><code class="language-bash">cat oldrepo.svndump | svndumpfilter --drop-empty-revs --renumber-revs include /projects/projectname &gt; projectname.svndump</code></pre></li><li><p>Keep filtering dumps until your paths work for a new repository. You’ll know if this isn’t working because you’ll get import errors. Long story short: view the dump source with a text editor and make sure that no paths are committed to the new repository that reference non-existing folders. My three steps basically create a tags folder and make sure that trunk gets moved back one level.</p><pre><code class="language-bash">cat projectname.svndump | sed -e 's,^Node-path: projects/projectname,Node-path: tags,' &gt; projectname2.svndump</code></pre><pre><code class="language-bash">cat projectname2.svndump | sed -e 's,^Node-path: tags/trunk,Node-path: trunk,' &gt; projectname3.svndump</code></pre><pre><code class="language-bash">cat projectname3.svndump | sed -e 's,^Node-copyfrom-path: projects/projectname/trunk,Node-copyfrom-path: trunk,' &gt; projectname4.svndump</code></pre></li>
</ol>
<p>If you’ve lived through step 4, now all you have to do is create a repository for your project and import your last filtered dump (projectname4.svndump).</p>
<h2>Step 6: Add a touch of awesomeness with Warehouse.</h2>
<p>Beanstalk is sexy and made it easy for me to share code with others. At this point I was thrilled to have unlimited repositories, but resigned to say goodbye to Beanstalk’s good looks and friendliness. Then <a href="http://warehouseapp.com/" target="_blank">Warehouse</a> brought sexy back. I just happened upon Warehouse, a web-based subversion browser that’s built on rails. And it just happened to go open source!</p>
<p>I deployed a few Ruby on Rails apps in the past, and all I remembered was that it wasn’t as easy as plopping PHP and MySQL somewhere. Ultimately the setup was simple, thanks to the last step:</p>
<ol>
<li>Add a subdomain wildcard for the server in the DNS manager.</li>
<li>Create a MySQL database for Warehouse, upload everything to the server’s web root, and run the installer.</li>
<li>Update Ruby, Rails, and Gems.</li>
<li>Install <a href="http://www.modrails.com/" target="_blank">Phusion Passenger </a>alongside Apache so there aren’t any more steps to install this app. (This is awesome, by the way.)</li>
</ol>
<p>You can figure out the rest on your own. Enjoy your new repositories, your web-based browser, and your awesome new subversion hosting!</p>
<p><strong>References:</strong></p>
<ul>
<li><a href="http://www.wreiner.at/en/2009/02/25/svn-unterverzeichnis-von-einem-repository-in-ein-neues-verschiebensvn-howto-move-subdirectory-from-one-repository-to-another/" target="_blank">How to Move a Subdirectory From One Repository to Another</a></li>
<li><a href="http://svnbook.red-bean.com/en/1.0/ch05s03.html" target="_blank">Subversion Manual, Chapter 5 Section 3</a></li>
<li><a href="http://www.yolinux.com/TUTORIALS/SubversionRepositoryDataTransfer.html">Transferring Repository Data</a></li>
</ul>
<p><strong>Update:</strong> For the hordes of people who followed these instructions and ended up with frustrating blank pages in Safari after multiple page refreshes, there’s a fix: <a href="http://code.google.com/p/phusion-passenger/issues/detail?id=354" target="_blank">code.google.com/p/phusion-passenger/issues/detail?id=354</a></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Edit in TextMate Lives Again with QuickCursor]]></title>
            <link>https://workingconcept.com/blog/edit-in-textmate-lives-again-with-quickcursor</link>
            <guid>https://workingconcept.com/blog/edit-in-textmate-lives-again-with-quickcursor</guid>
            <pubDate>Mon, 16 Nov 2009 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I complained before that my beloved “Edit in TextMate” input manager wandered off into the woods when I upgraded to Snow Leopard and started using 64-bit apps. I work with lots of sites using ExpressionEngine, and “Edit in TextMate” made life quicker/easier when editing templates. (We can have a lively debate about saving templates as files later.)</p>
<p>Ricky at <a href="http://ifthen.com/" target="_blank">IF/THEN</a> pointed me to <a href="https://github.com/jessegrosjean/quickcursor" target="_blank">QuickCursor</a>, which is like a more grown-up “Edit in TextMate.” QuickCursor stays calm as I change Safari tabs, and never seems to lose the connection between the browser window’s textarea and the TextMate window. It lets me choose the keyboard shortcut, and you’ll never guess what I made it.</p>
<p>Bonus: you can add various “Edit In…” applications with shortcuts. What’s not to like? <a href="https://web.archive.org/web/20120630141444/http://www.hogbaysoftware.com/products/quickcursor" target="_blank">Get it for yourself!</a></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Goodbye Slicehost, Hello BlipBleepBloop]]></title>
            <link>https://workingconcept.com/blog/goodbye-slicehost-hello-blipbleepbloop</link>
            <guid>https://workingconcept.com/blog/goodbye-slicehost-hello-blipbleepbloop</guid>
            <pubDate>Fri, 13 Nov 2009 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>A week ago, I logged into the <a href="http://slicehost.com/" target="_blank">Slicehost</a> admin panel, took a deep breath, and looked down at my index finger. I held my breath and clicked, and deleted my beloved Slice. Despite the fact that Slicehost will always have a special place in my nerdy little heart, I’m moving on.</p>
<p>The talented and wonderfully-bearded <a href="http://sccottt.com/" target="_blank">Scott Thiessen</a> and I have started a new project called <a href="http://blipbleepbloop.com/" target="_blank">BlipBleepBloop</a>. It’s our own tiny hosting company that we’re using for clients and to which we’re inviting a handful of folks.</p>
<p>I’m proud to be running all of my non-development sites off of a Blip account, which will inherently improve our service since both Scott and I are running our own sites there. Everything has been comfortably stable and speedy, and we’re working hard to pretty up cPanel. (Which we’re both thrilled about since we’ve each encountered so many shamelessly ugly themes in our past.)</p>
<p>Since we don’t have the ability to offer subversion hosting, I’ve moved my repositories to <a href="http://beanstalkapp.com/" target="_blank">Beanstalk</a>, which I couldn’t be happier with.</p>
<p>Want to know more about BlipBleepBloop packages? You won’t find any specs or ordering info on our website, so <a href="mailto:info@blipbleepbloop.com">email us</a> if you’re interested. That’s the idea: small and personal.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[HostGator and Postini]]></title>
            <link>https://workingconcept.com/blog/hostgator-postini</link>
            <guid>https://workingconcept.com/blog/hostgator-postini</guid>
            <pubDate>Fri, 13 Nov 2009 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’ve got a <a href="http://hostgator.com/" target="_blank">Hostgator</a> account and are interested in using Google’s excellent <a href="http://www.google.com/postini/index.html" target="_blank">Postini</a> mail filtering service, the following should get you up and running. Please note that I’m not an IT guy, I’m not speaking on Hostgator’s behalf, and I’m not responsible if mobs come after you with pitchforks or you encounter email issues.</p>
<ol>
<li>Sign up for Postini, and look for a welcome email with MX server settings that you should use.</li>
<li>Use cPanel to make sure your desired mailboxes are set up and working. (Obvious but necessary.)</li>
<li>Find either your website’s IP address or the hostname of the server you’re on. You can use any basic Whois tool, like <a href="http://cqcounter.com/whois/" target="_blank">cqcounter.com</a>’s.</li>
<li>In Postini’s admin tool, tell the delivery manager to send Postini’s messages to your mail server. Click the “Inbound Servers” tab, select an option from the “Choose Org:” dropdown, and click the “Delivery Mgr” nav item.</li>
<li>Find the “Edit” button and click it. You’ll now see “Email Servers and Load Balancing.” Make sure that one of your email servers is the hostname or IP address from step #3, and set “% Conn.” to 100% unless you have another mail server to route to.</li>
<li>Update your Hostgator MX records to automatically send all mail to Postini for processing. If “Edit MX Entry” is an option in cPanel, you can do it yourself. Otherwise you’ll need to chat with a Hostgator support specialist or file a support ticket. You want to change your MX entries (which tells the server where to send email) with those referred to in step #1. <strong>Make sure that you select the checkbox that says “Always accept mail locally even if the primary mx does not point to this server.” Otherwise you won’t get your filtered mail from Postini.</strong></li>
<li>Wait a while and see what happens.</li>
<li>If anything seems fishy, or even if everything seems to be working well, use Postini’s “System Tests” (again in the admin panel) to make sure that everything is routed properly.</li>
</ol>
<p>A final note: I learned the hard way that you need to use valid email addresses everywhere. By default, your Hostgator account will reject any incoming emails that aren’t going to a valid address. My daily Quarantine Summaries weren’t showing up in my inbox even though I was getting filtered mail because I told Postini to send me messages from “do-not-reply@spontaneousnoise.com,” which didn’t exist. SMTP message tests failed because Postini tried to send from “test@spontaneousnoise.com,” which also doesn’t exist. Creating an alias for that address allowed the test to work.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Mail Server: Postfix, Courier, MySQL, SpamAssassin, Procmail, Maildrop, Postini]]></title>
            <link>https://workingconcept.com/blog/a-mail-server-postfix-courier-mysql-spamassassin-procmail-maildrop-postini</link>
            <guid>https://workingconcept.com/blog/a-mail-server-postfix-courier-mysql-spamassassin-procmail-maildrop-postini</guid>
            <pubDate>Mon, 19 Oct 2009 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Setting up a mail server hasn’t been a simple affair. Fortunately, <a href="http://articles.slicehost.com/email" target="_blank">PickledOnion’s email tutorials</a> gave me a jump start on configuring my Hardy slice as a mail server. After using Postfix and MySQL to get virtual mail delivery working, several offers for replica watches and discount meds reminded me that I needed junk filtering. Here I’ll detail the process that I went through to configure email handling on my slice.</p>
<h2>Categorizing Mail with SpamAssassin</h2>
<p>At the time, SpamAssassin seemed like the best way to filter out junk. I’d heard of it before and configured SpamAssassin settings on shared hosting accounts at <a href="http://hostgator.com/" target="_blank">Hostgator</a> and <a href="http://digitalspace.net/" target="_blank">DigitalSpace</a>. I’d used it and knew it worked; I just had to figure out how to set it up. I read a few warnings about SpamAssassin being a bit of a memory hog, but how bad could it be? I’ll come back to that later. It was easy enough to use aptitude install to download and install SpamAssassin, but it was <a href="http://townx.org/blog/elliot/simple_spamassassin_setup_with_postfix_and_dovecot_on_ubuntu_breezy" target="_blank">elliot’s tutorial</a> that helped me configure SpamAssassin and get it processing mail on my Ubuntu server.</p>
<p>I populated SpamAssassin’s whitelist and lowered the minimum SPAM score. Immediately SpamAssassin started doing a great job of telling me what was SPAM and what wasn’t. There were very few false positives, which were quickly remedied with more whitelisting.</p>
<p>The next problem was keeping SPAM from my inbox. SpamAssassin was doing a great job of categorizing SPAM, but I needed some way to avoid seeing the SPAM alongside my legitimate messages. Every new SPAM message to my inbox was an urgent reminder of two things:</p>
<ol>
<li>I needed to separate junk from real mail.</li>
<li>With my own server, I just have to do everything!</li>
</ol>
<h2>Sorting with Procmail (or at least an attempt)</h2>
<p><a href="http://www.webmin.com/" target="_blank">Webmin</a>, which has at times been extremely helpful and deserves its own post, had a menu item under SpamAssassin for Procmail delivery. It didn’t take much searching and reading to know that Procmail was just the filtering device that I was looking for. I installed Procmail and repeatedly configured /etc/procmailrc and restarted Postfix. Nothing ever happened. I made sure that my log file existed and had proper permissions. I checked my mail logs and found nothing helpful. I was still receiving mail that got filtered by SpamAssassin, but Procmail never did anything or logged anything. It was supposed to use SpamAssassin’s headers to determine whether the message was junk, and then either deliver to the inbox or to the Junk folder. Instead it did nothing.</p>
<h2>Filtering with Maildrop</h2>
<p>My understanding of virtual delivery was lacking and I couldn’t get Procmail to work. It seemed like most of the example Procmail scripts I found assumed that you were using local delivery with real users (as opposed to virtual users like I was using via MySQL). I convinced myself that Procmail didn’t play nice with virtual delivery (wrong) and looked for alternatives. Despite looking for the wrong reason, I found Maildrop and eventually got that installed and filtering mail.</p>
<p>Here’s how I configured maildrop and got it working:</p>
<p>/etc/maildroprc</p>
<pre><code class="language-bash">DEFAULT="$HOME/Maildir"
logfile "/var/log/maildrop.log"

SHELL="/bin/bash"
PATH=/bin:/usr/bin:/usr/local/bin:/usr/lib/courier/bin/
INBOX="/home/vmail/$DEFAULT"
DEFAULT="$INBOX"
SPAMFLD="$INBOX.Junk/"

# create the trash directory if it does not exist
`test -d "$SPAMFLD"`
if( $RETURNCODE == 1 )
{
`maildirmake "$SPAMFLD"`
}

# filter message through spamassassin's spamc agent
xfilter "/usr/bin/spamc -f"
if ( /^X-Spam-Status: Yes,/)
{
DELETE_THRESHOLD=9.0
/score=(\d+)/
if ($MATCH1 &gt;= $DELETE_THRESHOLD)
{
to /dev/null
}
else
{
to "$SPAMFLD"
}
}</code></pre>
<p>/etc/postfix/master.cf</p>
<pre><code class="language-bash">maildrop unix - n n - - pipe
flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}</code></pre>
<p>/etc/postfix/main.cf</p>
<pre><code class="language-bash">mailbox_command = /usr/bin/maildrop -d ${USER} ${RECIPIENT}
virtual_transport = maildrop</code></pre>
<p>It’s imporant to note here that virtual_transport was critical because I was using virtual users and inboxes. The maildrop config lines in master.cf, combined with virtual_transport in main.cf seemed to be the key elements that allowed maildrop to filter mail.</p>
<h2>Maxing Out the Slice</h2>
<p>Everything was finally working just like I wanted. The only minor detail was that my memory usage seemed high, and my virtual memory usage was — well — being used on occasion. Typically, I was running at about 230MB out of the available 256MB. In the Slicehost Campfire chatroom, I posted my memory usage statistics and promptly received some concerned feedback. My swap usage was over 100MB, which means that my memory usage was so high the system had to start dumping memory contents to the disk’s swap space. I was maxing out the 256MB of memory on my Slice, and I needed to tune some processes and avoid utilizing virtual memory.</p>
<p>I restricted my Apache limits a bit, knowing that none of my sites would generate heavy traffic. I reduced the number of SpamAssassin children running on the Slice. Both of these adjustments helped a bit, but my memory usage was still high. I restricted SSH, saslauthd, and Courier, but that proved to be pretty stupid. Mail.app would suffer from SSL errors and I’d get occasional authentication problems. I put those back quickly.</p>
<p>Weeks passed and I closely watched my memory usage. I didn’t want to sacrifice functionality, but my memory usage was high and occasionally pushed into swap territory; mostly as little as 80KB but sometimes up to 4-5MB. My top processes were Apache (which I couldn’t cut any further), SpamAssassin, and MySQL. Switching Apache for Lighttpd didn’t seem like a good idea for me. How could I cut down on my memory usage?</p>
<h2>Filtering Mail with Google Apps/Postini</h2>
<p>I read a helpful post by unicks in Joyent’s forums (now gone from the internet) that explained how <a href="http://www.google.com/a/help/intl/en/security/email.html" target="_blank">Google Apps/Postini</a> offered an excellent mail filtering service that’s fairly easy to implement. I could still have complete control over the physical mail on my server, but with the benefit of up-to-date, state-of-the-art junk and virus filtering. Most importantly, I could drop SpamAssassin and save some memory.</p>
<p>All I had to do to configure this was to edit my DNS settings and point my MX servers to Postini. I then restricted Postfix’s allowed domains to Postini, so any mail that comes through my inbox has first passed through one of the more sophisticated SPAM filtering services on the market. The service will be less than $20/year, which is also a huge plus.</p>
<h2>Regaining Memory</h2>
<p>Rather than paying to upgrade to a slice with more RAM, I’ve offloaded my SPAM filtering to Postini — which I trust more than myself to stay up-to-date and effective. My average physical memory usage now hovers at around 125MB, which is about 80MB less than my SpamAssassin setup took. My swap usage gets to about 80KB sometimes, but that doesn’t worry me too much.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ActionScript 3 Memory Optimization]]></title>
            <link>https://workingconcept.com/blog/actionscript-3-memory-optimization</link>
            <guid>https://workingconcept.com/blog/actionscript-3-memory-optimization</guid>
            <pubDate>Fri, 25 Sep 2009 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I was recently finishing up a kiosk project wherein a Flash projector would have to run fullscreen 24/7, and was having a terrible time pinpointing a memory leak that left the projector unresponsive after only a few hours. While I was able to improve memory usage, I couldn’t get the app to stay calm over an extended period. The end result was a workaround that had the projector restart itself after an inactive waiting period.</p>
<h2>The Problem</h2>
<p>I was working with a proprietary AS3 framework that relied on multiple SWFs and XML for configuration and content. The piece involved lots of lossless images (dynamically loaded) and only needed to run as a projector from a Windows machine. It also communicated with an external series of lights via a MAKE Controller board. Initial testing of the app went smoothly, but letting it run overnight turned it into a sluggish memory hog.</p>
<h2>Improving Memory Usage</h2>
<p>There were a few tips I followed from various sources to streamline the app’s memory usage. Ultimately they weren’t enough to solve the big problem, but I watched the numbers drop in Apple’s Activity Monitor.</p>
<ul>
<li>Remove unused event listeners.</li>
<li>Clear unused display objects by setting each obj = null.</li>
<li>Make sure you’ve deleted any child display objects with removeChildAt()—great in a loop.</li>
<li>Stop all Timers to free up the memory that they use. myTimer.stop();</li>
</ul>
<h2>Final Solution</h2>
<p>We came to two options: use a third-party product for compiling a more efficient Flash .exe (such as <a href="http://screenweaver.org/doku.php" target="_blank">Screenweaver</a>, <a href="https://web.archive.org/web/20090810211640/http://www.multidmedia.com/software/zinc/" target="_blank">Zinc</a>, or <a href="https://web.archive.org/web/20160206134901/http://www.screentime.com/software/flash-projector" target="_blank">MProjector</a>), or create a reliable workaround that would simply restart the kiosk app and avoid the memory leak in the first place. The client needed files delivered quickly and did not have extra budget, so we ended up choosing the latter. We avoided the problem by adding a timer that would restart the Flash piece after 10 minutes of inactivity. (This was done by using FSCommand to call a batch file, since we’re using windows, immediately closing the Flash app, and having the batch file execute the Flash app again.) This ended up working pretty well and allowed us to deliver everything on time.</p>
<p>There are more discussions about memory optimization with Flash projectors (and Flash in general), but I never found any silver bullets. Here are a handful of links that were helpful:</p>
<ul>
<li><a href="http://artinflex.blogspot.com/2010/12/executing-long-running-tasks-without.html" target="_blank">Similar Issue: Executing Long-Running Tasks</a></li>
<li><a href="http://gskinner.com/blog/archives/2006/06/as3_resource_ma.html" target="_blank">Grant Skinner on Resource Management</a></li>
<li><a href="http://www.websector.de/blog/2007/10/01/detecting-memory-leaks-in-flash-or-flex-applications-using-wsmonitor/">Detecting Memory Leaks in Flash and Flex</a></li>
<li><a href="http://www.actionscript.org/forums/showthread.php3?t=195367" target="_blank">Self-Restarting Projector</a></li>
<li><a href="http://www.knas.se/knasRestarter.aspx" target="_blank">Monitor and Restart Crashed Windows Programs</a></li>
</ul></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[IETester = Sigh of Relief]]></title>
            <link>https://workingconcept.com/blog/ietester-sigh-of-relief</link>
            <guid>https://workingconcept.com/blog/ietester-sigh-of-relief</guid>
            <pubDate>Thu, 20 Aug 2009 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I could be late to the party here, but I’ve just discovered <a href="http://www.my-debugbar.com/wiki/IETester/HomePage" target="_blank">IETester</a>, a piece of software that’s been missing from my life for some time now. It’s a single (Windows) application that lets you test a given website in IE5.5, IE6, IE7, and even IE8. It’s in alpha right now, but I’ve tried it and so far I’m thrilled.</p>
<p>Stop reading and <a href="http://www.my-debugbar.com/wiki/IETester/HomePage" target="_blank">just go get it</a>.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Espresso != TextMate]]></title>
            <link>https://workingconcept.com/blog/espresso-textmate</link>
            <guid>https://workingconcept.com/blog/espresso-textmate</guid>
            <pubDate>Wed, 19 Aug 2009 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ve had a chance to check out MacRabbit’s <a href="http://macrabbit.com/espresso/" target="_blank">Espresso</a>, and I was really impressed by its autocomplete functionality–specifically with <a href="https://codeigniter.com/" target="_blank">CodeIgniter</a> and ActionScript. Its snippets, project panel, and Safari-like text search are also very attractive, but there are a few yet-to-be-implemented features that make it impossible for me to switch from <a href="https://macromates.com/" target="_blank">TextMate</a>.</p>
<p>In order of importance:</p>
<ol>
<li>Project-wide search and replace with support for regular expressions.</li>
<li>The ability to duplicate a selection without copy and paste. (<kbd>⌘</kbd> + <kbd>D</kbd> in TextMate)</li>
<li>Multi-line selection/edit tool (<kbd>alt</kbd> + click/drag), and the related ‘Edit Each Line in Selection’ feature.</li>
</ol>
<p>Espresso has been freshly released at version 1.0, and I realize that it needs time to mature. It’ll take a while to build up the enormous feature set that TextMate offers, and it’ll be amazing if it can stay as simple as TextMate at the same time. I’d make the switch in a heartbeat if I wasn’t so dependent on TextMate’s awesomeness.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Planning Your Website]]></title>
            <link>https://workingconcept.com/blog/planning-your-website</link>
            <guid>https://workingconcept.com/blog/planning-your-website</guid>
            <pubDate>Sat, 08 Aug 2009 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>You’ve got a web project and you consider yourself somewhat tech-savvy, but you know you need to enlist some professional designers and programming nerds to get your site off the ground. For most clients, this is new territory and it’s hard to know exactly what to ask. Here are a few things you’ll want to consider…</p>
<h2>Know what you need, want, and would like to have. (In that order.)</h2>
<p>The most common misconception is that the internet is a magical place where things build themselves, and everything is possible. It may be magical, and there are lots of talented people doing amazing things on the web, but every cool thing you see starts with some kind of plan and takes time to build. The clearer you can define your project’s needs, the easier time you’ll have working with some professionals to scope things out and get to work. If you haven’t already, make a list of your must-have features/goals, the ones you want but aren’t necessary, and then things that would just be nice.</p>
<h2>A good website has a good plan behind it.</h2>
<p>The process from start to finish roughly follows this path: define goals, determine technical requirements, establish information architecture, create and revise design/look-and-feel, build, test, and launch. Each of these phases assumes some amount of revision, and the entire process could range from a few weeks to a number of months. It’s tempting to want to see how your site will look and wait until you can use it to revise the way it works, but that’s a sure way to make a project more expensive. Defining goals that can be measured, having good information architecture that models key aspects of site functionality, and listing technical requirements up front can save lots of time and money later in the project cycle.</p>
<h2>The sky is not the limit; it’s time and budget.</h2>
<p>If you’re new to web projects, pretend you’re building a house. They come in all shapes and sizes, they often meet the same basic needs, and you’ve been through enough neighborhoods to know that they can be vastly different from one another. Be prepared to share your budget and expected timeline, as they will be critical in determining the scope of the site. Ask to see a few bids where the design firm explores potential options that meet the site’s needs within the budget and timeline that you have to work with.</p>
<p>Hopefully this will demystify some of the process. If you still have a few questions, or if you’d like to start a conversation about your project, <a href="mailto:hello@workingconcept.com">drop me a line</a>!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Internet Explorer & jQuery Cycle Plugin Background Issue]]></title>
            <link>https://workingconcept.com/blog/internet-explorer-jquery-cycle-plugin-background-issue</link>
            <guid>https://workingconcept.com/blog/internet-explorer-jquery-cycle-plugin-background-issue</guid>
            <pubDate>Mon, 06 Jul 2009 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This is one of those awesome bugs that wasted more of my time than I’d like to admit, so hopefully this post can help someone in the future.</p>
<p>The problem: I’m working on a site that uses the jQuery Cycle plugin. The chunk that gets rotated for the slideshow is an HTML list-item that contains another list with nested list items. (li -&gt; ul -&gt; li, li, li). The root li kept getting a background color applied to it, even though I specified that it should be transparent.</p>
<p>The solution: disabling the Cycle plugin’s default behavior of applying a background to the slideshow container element when using IE on a Windows machine with cleartype enabled. Add this little combo to your Cycle options and things should be fine:</p>
<pre>cleartypeNoBg: true</pre>
<p>The key was mention of this undocumented feature in <a href="https://web.archive.org/web/20131020122129/http://osdir.com/ml/jQuery/2009-05/msg01343.html" target="_blank">this thread</a>.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sync it all with Dropbox!]]></title>
            <link>https://workingconcept.com/blog/sync-it-all-with-dropbox</link>
            <guid>https://workingconcept.com/blog/sync-it-all-with-dropbox</guid>
            <pubDate>Wed, 07 Jan 2009 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I work on more than one Mac. Working from Subversion and checking my email is a breeze, but pretty much everything else is annoying when it comes to sharing data. I don’t want to fork over the money for MobileMe because I’d only want it for syncing. I finally have a simple solution that I’m using to keep my <a href="https://agilebits.com/onepassword" target="_blank">1Password</a> keychain, Address Book contacts, and <a href="http://culturedcode.com/things/" target="_blank">Things</a> data all up to date. I first switched my 1Password data to the Agile Keychain format, chose to store the keychain in my Dropbox, and I was done! It worked so well I thought I’d try and use Dropbox for some other stuff.</p>
<p>I could finally have Address Book share the same data and sync any changes to my iPhone from my primary/home computer! It was as simple as turning the Address Book’s data folder into a symbolic link. The app acts like it normally would, but secretly the data it uses gets stored in the Dropbox and therefore updated each time the Address Book gets an edit. For me, it was two lines via the terminal:</p>
<pre><code class="language-bash">cd /Users/[username]/Library/Application\ Support/
ln -s /Users/[username]/Dropbox/SyncData/AddressBook/ ./AddressBook</code></pre>
<p>Since this worked just fine, I tried the same with Things. It stores its data in a single XML file, but I made the whole directory a symbolic link anyway:</p>
<pre><code class="language-bash">cd /Users/[username]/Library/Application\ Support/Cultured\ Code/
ln -s /Users/[username]/Dropbox/SyncData/Things/ ./Things</code></pre>
<p>The only catch here is that I need to remember to close Things when I’ll be moving to another computer. I ran into a problem once where I opened up my laptop and it wrote to the Things data before it had a chance to pull down updates. This was easily fixed by going to the Dropbox web panel, selecting a previous revision of that file, and restoring it. It immediately pushed the revision to all three machines. Impressive! So even if something gets messed up, Dropbox is quietly keeping revisions of everything. I love it.</p>
<p>I’d like to follow suit with my Safari bookmarks, but I only want to update one file (bookmarks.plist) rather than Safari’s entire support directory. I tried making only this file a symbolic link, but Safari is quick to overwrite it with a real file which destroys the link. I don’t want my browsing history, cookies, cache, etc. to get synced, but I’m not sure what else to do. If anybody out on the internet has an idea, feel free to leave a comment. (Also, if anybody besides me actually reads my posts, I’d be thrilled to find that out.)</p>
<p><strong>Update: I’ve been able to get some other apps syncing data as well, including <a href="http://www.billingsapp.com/" target="_blank">Billings</a>, <a href="https://www.marinersoftware.com/products/macjournal/" target="_blank">MacJournal</a>, and <a href="http://www.ergonis.com/products/typinator/" target="_blank">Typinator</a>.</strong></p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Slicehost!]]></title>
            <link>https://workingconcept.com/blog/slicehost</link>
            <guid>https://workingconcept.com/blog/slicehost</guid>
            <pubDate>Sun, 12 Oct 2008 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>In September, I switched from a Baby shared hosting account at <a href="http://hostgator.com/" target="_blank">HostGator</a> to my very own slice at <a href="http://slicehost.com/" target="_blank">Slicehost</a>. It’s been an eye-opening plunge into the world of unmanaged hosting, but the Slicehost support team has played a critical role in supporting me while I get my Slice running smoothly, securely, and efficiently. I’m maxing out a 256MB slice with LAMP, a mail server, DAV, and SVN.</p>
<p>The great relief is that every single problem I’ve had been my own, not the limitations or drawbacks of the hosting company. I was impressed with the responsiveness and offering of my HostGator account, but felt limited by not having DAV or SVN access. Slicehost, at least thus far, has simply followed through with its promise: rock-solid, high-performance service with unbeatable support. They’ve gone out of their way to help me troubleshoot and configure my Slice, understanding full well that I’ve been learning as I go. The risk of course is that your server is yours … to completely screw up. I’ve had a few late-night Postfix/Courier experiments go poorly, but there has always been someone around to help. (Which is amazing — I really mean <em>always</em>.)</p>
<p>I plan on posting some articles with what I’ve learned from my own experience. If you’re starting out with Slicehost, however, <a href="http://articles.slicehost.com/" target="_blank">PickledOnion’s tutorials</a> are a must.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Craft CMS and JAMstack]]></title>
            <link>https://workingconcept.com/blog/craft-cms-and-jamstack</link>
            <guid>https://workingconcept.com/blog/craft-cms-and-jamstack</guid>
            <pubDate>Thu, 26 Sep 2019 14:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/guillotine.mKFGOP1c_1969EB.webp" type="image/webp"><source src="https://workingconcept.com//_astro/guillotine.mKFGOP1c_ZtaaVA.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/guillotine.mKFGOP1c_LjJhU.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/guillotine.mKFGOP1c_Z5JBIk.jpg" decoding="async" loading="lazy" alt="guillotine illustration detail" width="1906" height="1072"> </picture> <figcaption> <p> Guillotine illustration detail courtesy of Bibliothèque nationale de France.  </p> </figcaption> </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>JAMstack is the Beatles. </p><p>At <a href="https://dotall.com/2019">Dot All</a> we clutched our hearts, outstretched our hands, and jumped around screaming and crying as JAMstack filled the air.<sup id="fnref1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup></p><p>You probably heard about that already, and if not you might want to start with an excellent summary by <a href="https://mattgrayisok.com/dotall-2019">Matt Gray</a> or <a href="https://putyourlightson.com/articles/takeaways-from-dotall-in-montreal-2019">Ben Croker</a>.</p><p>I'd like to clarify how headless Craft, serverless architecture, GraphQL and JAMstack relate and how they could play a part in however you build web projects.</p><p>Before that, Marion made a point <a href="https://devmode.fm/episodes/dot-all-2019-conference-recap-analysis-live-from-montreal">on the devMode podcast</a> that's worth repeating: Craft is not forcing you to change anything. If you build sites happily and productively with Twig, you can keep right on doing that and enjoy the exciting stuff that's coming in Craft 4. This headless, JAMstack-centric business is opt-in only and not something that requires you to change the way you work.</p><h2>My Interest in JAMstack</h2><p>I've been building modestly-sized, head<em>ful</em> Craft sites for years, and I'd like to build sites statically without any compromise for content authors. My clients shouldn't have to suddenly do without Live Preview or lose control over exactly when they publish updates. My technical interest comes down to hosting and building with components. I'd like to... </p><ul><li>stop provisioning, maintaining and supporting servers</li><li>dramatically reduce hosting costs while improving performance, uptime, and security</li><li>deploy static sites built with Vue, React, or Svelte (using projects like <a href="https://gridsome.org/">Gridsome</a>, <a href="https://www.gatsbyjs.org/">Gatsby</a> or <a href="https://sapper.svelte.dev/">Sapper</a>)</li><li>take advantage of component-centric tools like <a href="https://storybook.js.org/">Storybook</a> and <a href="https://www.figma.com/">Figma</a> to design and document work with greater clarity</li></ul><p>I haven't made that my new normal yet, but <strong>as of Craft 3.3 the major pieces are all there to be assembled</strong>. I'm pretty sure that's what we're all excited about.</p><p>But let's sort out what these things mean. They seem to have arrived all at once, and it's easy to confuse the pieces and how they fit together.</p><h2>Headless != JAMstack != GraphQL</h2><p><a href="https://dotall.com/sessions/comparing-jamstack-to-lamp">Andrew's talk</a> compared the LAMP stack we know to the JAMstack we want to. It's not an alien wonder, but a rearrangement of concepts and responsibilities we already know pretty well. Cool new things that have their own tradeoffs.</p><p>The LAMP/LEMP <del>initialisms</del> acronyms stand for software you inherently run on a server, while JAMstack's <strong>J</strong>avaScript, <strong>A</strong>PIs and <strong>M</strong>arkup don't require a server at all.</p><p>For this reason, one of the benefits of the JAMstack is how well-suited it is for serverless architecture. The term “serverless” is part lie because there are still servers, but mostly great because <em>you</em> don't have to manage them. Usually you point a slick SaaS offering to a repository, give it some minimal configuration, and it takes care of the rest.</p><p>We'll get to Gatsby and Gridsome in a moment, but it's important to know that they still require a server even though they build sites you can host on a CDN. Gatsby and Gridsome require a server at build time, which is before the site gets deployed. This is probably similar to how you're prepping CSS or JavaScript for deployment right now, except the site's pages are all built too. Craft obviously runs on a server, but it was born building pages on the fly at <em>request</em> time. Various forms of caching can make that as fast as a static site, but that's the key difference between Craft and static site generators: both need web servers to generate pages, but one builds before deploy and the other builds live, upon request.</p><p>The last Craft 3 install you touched can run headless right now without any changes to its server. In the not-too-distant future, you'll also be able to use headless Craft without needing to install <em>anything</em> on a server—that's <a href="https://craft.cloud/">Craft Cloud</a>.</p><p>Headless Craft just means Craft without Twig.<sup id="fnref2"><a href="{fn:2:url||}" class="footnote-ref">2</a> </sup>Craft reduces its role to content manager, with a UI for authors to manage content and APIs (ElementAPI and GraphQL) for developers to extract that content and decide how and where it's presented. Those developers aren't limited to using Twig to build a frontend, but anything at all that can interact with those APIs. This plays perfectly, and at just the right time, into serverless static sites.</p><p>Static sites aren't new, but hosts like <a href="https://www.netlify.com/">Netlify</a> and <a href="https://zeit.co/">Zeit</a> offer platforms that make them easy to work with. Building those sites can be fun with projects like Gatsby and Gridsome.</p><p>There are two reasons Gatsby and Gridsome get so much attention:</p><ol><li><strong>They'll take whatever content you've got.</strong> This can mean local markdown files, MySQL or Postgres, a GraphQL API, and more. It can also be any combination of sources. No matter how much there was or where it came from, it'll then be accessible in one uniform GraphQL collection.<sup id="fnref3"><a href="{fn:3:url||}" class="footnote-ref">3</a></sup></li><li><strong>They neatly package frontends using the things we know we should be.</strong> Performance and best practices are baked in: preconfigured webpack (!), server side rendering, critical CSS and prefetching to name a few. Plugins make trivial work of adding Tailwind or offline browsing and loads of other fancies. If you've ever tried to wire up just a few of these things yourself, you'll appreciate how they all just work right out of the box. </li></ol><p>It's reasonable to assume, based on Gatsby and Gridsome's popularity, that humanity has not been loving the tooling complexity required for a performant, well-rounded modern frontend. Each project takes data from a variety of sources, lets us work with Vue or React and whatever we want for styling, and offers preconfigured and easily-customizable tooling to generate a great static frontend.</p><h2>Live Preview</h2><p>Live Preview was a real challenge for static sites until Pixel & Tonic released preview targets with tokens. This says, “sure, let whoever you want into that preview pane, just have them give us this little token so we know it's safe to share your content.” It still takes some extra planning and effort to support Live Preview outside of Craft's comfortable Twig templates, but the preview tokens make it a lot simpler.</p><h2>Read/Write</h2><p>If you've contemplated a static Craft site, it probably took you ten seconds to wonder how content gets back <em>into</em> to Craft via Guest Entries, form posts, etc.</p><p>The GraphQL API doesn't yet support mutations, which is GraphQL-speak for <em>writing</em> stuff that's currently just read-only. But those are coming. You can still use the Element API, see how plugin developers might support headless interaction, or just send data to third-party services.</p><p>The bigger shift to prepare for right now, however, is adding more logic to your presentation layer. If you've ever submitted a contact form via AJAX, you already know exactly what this means: have axios or jQuery post the data, look at the response, and decide how to reflect that on the page. This is the exact same shift you'd make for building a member section or store, though you'll probably need to store persistent data in sessions or local storage. Once you make the shift in how you think of “dynamic” interaction, it'll probably make Vue or React seem more powerful and lead to a more manageable set of problems to solve.</p><h2>What Now?</h2><p>One nice thing about JAMstack's patchwork is that its pieces can easily be broken into bite-sized nuggets. </p><p><a href="https://dotall.com/sessions/progressively-enhance-your-craft-site-with-vue">Francesca's talk</a> offered a pragmatic way forward for those of us that are new to a lot of these things: don't feel pressured to build a whole project with Gatsby if that's overly daunting. Instead, sprinkle a little Vue or React into a project as progressive enhancement. </p><p>Or whip up a serverless microservice by using a Lambda function to <a href="https://nystudio107.com/blog/setting-up-your-own-image-transform-service">handle your image transforms</a> or send email notifications when forms are submitted. </p><p>You might discover a surprise love for Lambda functions and like AWS again, become weirdly passionate about web components, or maybe get comfortable with enough different parts that building a headless Craft site seems more inevitable than daunting.</p><p>Whatever you do, make stuff and have fun!</p><div class="footnotes"><hr><ol><li id="fn:1"><p>Symbolically. <a href="#fnref1" rev="footnote" class="footnote-backref">↩</a></p></li><li id="fn:2"><p>There are Twig JavaScript parsers, so technically you could write a static frontend with barebones Twig templates. But who wants to live like that? <a href="#fnref2" rev="footnote" class="footnote-backref">↩</a></p></li><li id="fn:3"><p>If, like me, you've mixed up Craft's GraphQL endpoint and Gridsome/Gatsby's GraphQL endpoint, remember that Craft is one source of content and Gridsome/Gatsby are designed to collect <em>multiple</em> sources of content and make them available in one (remarkably similar) GraphQL API. <a href="#fnref3" rev="footnote" class="footnote-backref">↩</a></p></li></ol></div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[On Visiting Peers]]></title>
            <link>https://workingconcept.com/blog/on-visiting-peers</link>
            <guid>https://workingconcept.com/blog/on-visiting-peers</guid>
            <pubDate>Sun, 24 Apr 2016 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/on-visiting-peers@2x.IOejuLbl_2vUbyF.webp" type="image/webp"><source src="https://workingconcept.com//_astro/on-visiting-peers@2x.IOejuLbl_ZbvJC.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/on-visiting-peers@2x.IOejuLbl_ZAO9Oa.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/on-visiting-peers@2x.IOejuLbl_1qfyOI.png" decoding="async" loading="lazy" alt width="1360" height="600"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The short version is that I finally went to a conference and thereupon learned that I should go to conferences.</p>
<p>The longer version is that I returned to Florida two weeks ago to attend <a href="http://peersconf.com/">Peers</a>, a conference I’d sum up as a therapeutic professional gathering of web entreprenerds. Despite the high concentration of likeminded people in my industry, I manufactured poor but sufficient excuses not to go in years past. This year a lack of scheduling conflicts and an abundance of curiosity led me to the far end of the blue baggage claim at the Tampa airport. That’s where I met the first folks in what would turn out to be a dazzling array of people, messenger bags, and earnest conversations about the stuff we all do.</p>
<p>I was not surprised to confirm the presence of fascinating backstory, hilarity, catharsis, experience, insight, and challenge. I <em>was</em> surprised, however, not to encounter douchebaggery or cliquishness of any sort. I braced myself for some of the unsavory stuff I’ve heard about tech conferences where attendees (usually women) get singled out by real life trolls and jerks, and was heartened—though not surprised—when a female presenter stood up at one point to share that she was happy to give a talk in a place where she wasn’t rudely treated like a novelty woman-developer but got to engage in conversations she was passionate about.</p>
<p>The spirit of the thing, the laid-back gathering of approachable web nerds, was the golden nugget at its core. It pervaded the workshops and talks and conversations, and I think much of the credit for that belongs to Jess D’Amico, the tireless champion and organizer of the event. As an over-thinking introvert, I don’t tend to breeze among foreign venues and introduce myself to strangers. Yet that I did, because keeping up was effortless thanks to untold effort from Jess and her crew, along with the attendees who unanimously chose to be amicable and sincere.</p>
<p>Another thing I didn’t expect to find was reassurance. More than one presenter had me deeply appreciating challenges and failures rather than fearing them, and it became obvious after only one evening that many of us were on similar quests experiencing similar joys and problems. As a solo nerd more or less making up his career, this was comforting and refreshing. It’s not often that I can find relief in something while being challenged by it, and my time at Peers very much hit both marks.</p>
<p>Judge not my simple revelations, dear internet, as I conclude with my humble Peers takeaways. These are for me, so don’t be surprised if you’ve already figured them out:</p>
<ol>
<li>Don’t make assumptions about people you’ve only known as an arrangement of pixels. You can avoid being wrong and being a jerk at the same time!</li>
<li>Don’t waste time being intimidated by people, but appreciate the work they do and be prepared to ask them questions. It’s way more interesting to hear what motivates them than to stare dumbfounded as you realize they wear pants just like you.</li>
<li>Invest real effort in remembering peoples’ names, remembering how it felt when a stranger bothered to remember yours.</li>
<li>Don’t complain about long flights. Someone with a fabulous accent will have inevitably journeyed much farther.</li>
</ol></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2017 Recap]]></title>
            <link>https://workingconcept.com/blog/2017-recap</link>
            <guid>https://workingconcept.com/blog/2017-recap</guid>
            <pubDate>Mon, 18 Dec 2017 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/2017@2x.DlRG8z1W_Z1GczEN.webp" type="image/webp"><source src="https://workingconcept.com//_astro/2017@2x.DlRG8z1W_665I0.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/2017@2x.DlRG8z1W_Z1eLAL0.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/2017@2x.DlRG8z1W_2bxtjw.png" decoding="async" loading="lazy" alt width="1360" height="600"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’d like to use this first post of 2017 to summarize the year, a new tradition that’ll ensure at least one post for every lap the earth makes ‘round the sun.</p>
<p>It’s been a busy year with a few big milestones for us, the biggest being a transition from <em>“</em>me” to <em>“</em>us.” Raina’s been working with me for more than three years, and starting this month she’s employee #1, entitled to all the perks and unpaid vacation we enjoy here at the company. We met working together, got married along the way, and continue to work well together, and I’m eager to see how Working Concept can grow and improve.</p>
<p>For those expecting bullets, here are the highlights:</p>
<ul>
<li>Helped behind the scenes with <a href="http://peersconf.com/2017">Peers 2017</a>, which came to Seattle in April for its fifth incarnation.</li>
<li>Published four Craft plugins: <a href="https://github.com/workingconcept/cloudflare-craft-plugin">Cloudflare</a>, <a href="https://github.com/workingconcept/versioneer-craft-plugin">Versioneer</a>, <a href="https://github.com/workingconcept/keycdn-craft-plugin">KeyCDN</a>, and <a href="https://github.com/workingconcept/bugsnag-craft3">Bugsnag</a> (for Craft 3). I write a bunch of Craft plugins and publish few, so it’s been good to get something out there.</li>
<li>Hit a <a href="https://developers.google.com/speed/pagespeed/insights/?url=https%3A%2F%2Fworkingconcept.com">perfect 100 for Google PageSpeed Insights</a>, after finally <a href="{entry:14@1:url||https://workingconcept.com/blog/closing-pagespeed-gap}">grappling with CriticalCSS and more intense caching</a>. If you’re an active client reading this, you’ve benefitted from this quest in some way.</li>
<li>Migrated <a href="https://blipbleepbloop.com/">BlipBleepBloop</a>, our tiny hosting operation, to much faster and more stable infrastructure without pricing changes.</li>
<li>Boosted overall project efficiency adding <a href="https://gulpjs.com/">Gulp</a> workflows to old and new work.</li>
<li>Got started with <a href="https://craftcms.com/3">Craft 3</a>, which hit RC2 just before I published this post. (If "RC" only reminds you of soda, just know it’s getting close to its big launch.)</li>
<li>Helped some exciting young software companies define and refine their visual identities. We’re excited to watch them grow!</li>
<li>Launched a 17-language site for distinctly different audiences spanning four continents.</li>
<li>Watched speed and SEO improvements bring an existing client more business.</li>
<li>Launched our very first pro-bono project for a <a href="http://wshelpline.org/">neighborhood non-profit</a> that helps neighbors in need get clothes, transportation, and help paying bills.</li>
<li>Ushered projects old and new into the era of https.</li>
<li>Started using <a href="https://capsulecrm.com/">a CRM</a>, which you’ve noticed if you brought up work and I "magically" remembered to follow up methodically. More on this below.</li>
<li>Spent quality time with <a href="https://laravel.com/">Laravel</a> and <a href="https://vuejs.org/">Vue</a>, leading to a few client projects and a handful of internal tools for monitoring project health, scheduling meetings, signing contracts, and automating Trello tasks.</li>
<li>Luxuriated in this bountiful age of text editors, occasionally setting aside Sublime Text 3 to work smarter with <a href="https://www.jetbrains.com/phpstorm/">PhpStorm</a> and sometimes cheerily with <a href="https://code.visualstudio.com/">VS Code</a>.</li>
<li>Committed to using <a href="https://www.bugsnag.com/">Bugsnag</a> and <a href="https://www.pingdom.com/">Pingdom</a> to stay vigilant with active projects.</li>
<li>Managed to be the second-most-useful moderater on the <a href="https://craftcms.stackexchange.com/">Craft CMS Stack Exchange site</a>. Stop by, ask an inappropriate question, and I may be the one to remove it!</li>
<li>De-integrated the CMS from some old Craft and ExpressionEngine projects that are better off static for the long haul.</li>
</ul>
<h2>Modest Marketing</h2>
<blockquote>
<p>How many times have you heard the statement “We like to let our work do the talking.” That’s modesty. And in marketing, there’s no place for modesty.</p>
<p><cite>Alex Goldfayn, <em>The Revenue Growth Habit</em></cite></p>
</blockquote>
<p>Despite my enthusiasm over <a href="http://a.co/7n0B07R">a helpful project management book</a>, the year’s greatest business development has been the notion of business development. <a href="http://a.co/h6smpFO">A different book that I <em>didn’t</em> like</a> helped me approach the idea of sales, for my business, without feeling my soul start to wither. I read it following some interesting discussion in a Slack group, and had my most moving annual epiphany.</p>
<p>But first, you should know I’ve been running my own business to accomplish two things: </p>
<ol>
<li>help people solve interesting problems</li>
<li>continue eating and sleeping both legally and indoors</li>
</ol>
<p>For a tiny company like mine, I’ve seen sales as a misguided effort to win money rather than do good work. I’d rather under-promise and over-deliver, I’ve been lucky enough to always have work, and I’ve had little need to sell anything. Since it launched, this site has continually communicated <em>less</em> about what we do. I’ve been getting away with it, not updating testimonials and case studies with glee. My epiphany, courtesy of Alex Goldfayn, is that I’ve been letting existing and unrealized clients down:</p>
<ul>
<li>Existing clients aren’t aware of an evolving skill set, sometimes surprised to learn we do a particular thing or that we’d solve a problem differently than we might have a year ago.</li>
<li>We’re primed to lose work with good, inactive clients when all they see is the site we built many years ago. If they want something fresh, there’s no reason to consider the maker of the <em>old</em> thing with only a few blog posts to indicate they’re alive.</li>
<li>We have to spend a significant amount of time compiling work samples for potential new clients, who must ask for them given the absence of a portfolio or case studies on the site.</li>
</ul>
<p>If you’re reading this (hi, and thank you!), you’ve fallen into a carefully-placed trap. I’d like to do a better job of conveying how we might be able to help without wasting your time. I’ll be using a CRM to make sure I follow up on any new work you might want to tackle. I’ll be updating this site to better communicate what we do and show examples. (<a href="https://workingconcept.com//haq">A secret HAQ page</a> was a recent start.) I have more ideas, and hopefully they’ll help us both do better work together in the year ahead. Happy almost-2018!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing Maintenance & Support Plans]]></title>
            <link>https://workingconcept.com/blog/introducing-maintenance-support-plans</link>
            <guid>https://workingconcept.com/blog/introducing-maintenance-support-plans</guid>
            <pubDate>Mon, 04 Mar 2019 16:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/maintenance-plans.E6hSZbuL_HGo5X.webp" type="image/webp"><source src="https://workingconcept.com//_astro/maintenance-plans.E6hSZbuL_IYsX2.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/maintenance-plans.E6hSZbuL_Z17BQKu.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/maintenance-plans.E6hSZbuL_ZsXxew.png" decoding="async" loading="lazy" alt="illustration of a shield and heartbeat" width="1360" height="600"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>We’re pleased and relieved to finally start offering <a href="https://workingconcept.com//maintenance-plans">formal maintenance plans</a> and the option of ongoing support. This is part of a commitment to improve the quality and care of all projects by making maintenance a business priority.</p>
<p>A strong launch is one thing, but keeping a site strong months or years later can be a challenge.</p>
<p>Things on the interweb change fast and long gaps between updates can be issues for security, speed and usability. We’re constantly improving how we build and deploy projects, so even getting back into older code can take a significant amount of time. We’re not the first to offer this kind of arrangement, but we hope we’ve found options that’ll work for every client and provide a sustainable way to keep every project happy and up to date.</p>
<p>For clients with older projects that have been with us for a long time, the idea may be uncomfortable. This is our fault, because like many in our industry we’ve learned the hard way that ongoing free support and availability has a cost. This cost either adds up day to day, or reveals itself when an old and untouched project needs work or experiences an emergency. While every project is different, there’s not a single site on the internet that isn’t dependent on hardware, software, and vast networks being maintained to keep it alive.</p>
<h2>Why now?</h2>
<p>Two key factors have contributed to our push to formalize plans. </p>
<p>The first is a growing body of work. We’ve launched more than a decade’s worth of projects, so we’ve had to figure out how to ensure every project stays up to date <em>especially</em> when it appears to be humming along without any needs. </p>
<p>We’ve always been happy to answer questions and keep supporting work we’ve done. It’s also becoming increasingly important to discern where to spend our finite supply of attention. Maintenance and support plans expose formerly vague or hidden costs and assign value in a way that’s clear, professional and sustainable.</p>
<p>Second is a much stronger proficiency with development tools and hosting infrastructure. A stroll back through some of this blog’s earlier posts will offer evidence of learning and diving deeper into the complexity of how a site exists and performs on the internet. While I wouldn’t dare mistake myself for a professional systems administrator, I’m comfortable with systems that years ago would have had me hesitate to offer support or guidance. </p>
<p>In other words: we aren’t just <em>offering</em> plans, we can actually manage and support them. We’ve put a lot of time into policies, practices, and packages that should ensure service from which everyone can benefit.</p>
<h2>Okay great, but I’m a client. You solved problems, updated software and answered questions for free for a long time!</h2>
<p>We’d honestly like to keep doing that because we like being helpful, but we’ll all end up disappointed if we keep on that way. Our support won’t be worth anything if our company isn’t around to provide it. We have to prioritize work that pays, which means maintenance isn’t a serious priority until we can include it in a stream of committed work. We don’t want to operate with a wink and a nudge, but with clear parameters and a fair cost.</p>
<h2>Is a maintenance plan required?</h2>
<p>No.</p>
<p>We don’t own or control any project; every one is in the hands of the client. We’re raising our standards and insisting that projects are kept up to date when we’re asked to work on them. (They <em>should</em> be monitored and kept healthy and we’d love to help with that too.) At the very least, we’ll expose the cost of software and workflow updates in any estimate we provide. That may not be a big deal particularly for newer projects, but there are plenty of options for older sites:</p>
<ol>
<li>We can help train someone from your team to manage updates. There’s nothing secret about what we do, we just insist that <em>somebody</em> cares for every project so it stays strong.</li>
<li>We can help you find another developer or agency that can handle maintenance.</li>
<li>If your site’s older and doesn’t see frequent updates, we may be able to flatten it by removing the machinery that might otherwise be problematic if left untouched. We’ve done this for a few clients, and while it’s not <em>perfect</em> it can dramatically reduce a site’s ongoing maintenance needs.</li>
<li>We can also help move you to a platform that includes hosting, like SquareSpace or Shopify or Wix. You’ll sacrifice some of the flexibility you enjoy having a fully custom project, but maintenance and support are included in the service fee.</li>
</ol>
<h2>What does "up to date" mean?</h2>
<p>Up to date means that a site’s code base is clean and checked into version control, that all minor software versions are current, and that any commercial software is properly licensed and entitled to support. Security patches and critical updates are applied throughout the software stack. Backup mechanisms should be utilized and periodically verified.</p>
<p>In non-technical terms, simple diligence should prevent the site from being a liability for the client. Best practices and redundancies can prevent problems and make it easier for others to get involved or help out.</p>
<p>Servers fail, people make mistakes, teams and developers change. None of these things needs to jeopardize a site.</p>
<h2>What does "monitored" mean?</h2>
<p>The level of monitoring depends on the plan, but at minimum it means perpetual one-minute checks from different geographical locations to make sure your public website is as fast and available as we’d expect. We’ll automatically be notified if there are issues, and clients will authorize us to file support tickets to quickly get them resolved. We’ll also report the site’s overall uptime metrics so we can all know how well (or not) the hosting setup is working out. Lastly, we’ll let you know about any upcoming SSL certificate or domain registration expirations. None of these things count as issues in <em>Maintenance & Support</em> plans. We’ll monitor and be proactive for any site we’re watching, period.</p>
<p>For more involved monitoring and support, we’ll also set up error monitoring in production and respond similarly if problems occur in the wild. If the web host type allows us to also collect server health metrics, we can include those as well.</p>
<h2>Why is this a good idea?</h2>
<blockquote>
<p>“An ounce of prevention is worth a pound of cure.”</p>
<cite>Benjamin Franklin</cite>
</blockquote>
<p>Benjamin Franklin didn’t operate a single website and he was referring to preventing fires in Philadelphia, but contingency planning is <em>always</em> a good idea. In this case, some diligent action can prevent inconvenient or even catastrophic business problems. Lost data and lost business can take a lot of time and resources to replace.</p>
<h2>What if I want to handle these things myself?</h2>
<p>You can, and we’ll even give you pointers to get started! We don’t offer magic or proprietary wizardry, just expertise at what we think are reasonable prices. We love working with all the bits and pieces that bring web projects to life and enjoy sharing that knowledge. Odds are you hired us because you didn’t have time to become a designer and/or web developer and do everything yourself. That’s the same reason you might consider trusting us to keep an eye on your site and keep it up to date. </p>
<h2>What’s it take to get started?</h2>
<p>That depends on what the project looks like.</p>
<p>Recently updated projects won’t need much, just a plan selection, initial payment, and onboarding.</p>
<p>Older projects will probably need to be caught up further with workflow and software updates.</p>
<p>Either way, it might help to read more on the <a href="https://workingconcept.com//maintenance-plans">Website Maintenance & Support Plans page</a> and chat with us about what it would take to get up and running.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Spiritual Shift from Gulp to Webpack]]></title>
            <link>https://workingconcept.com/blog/gulp-to-webpack</link>
            <guid>https://workingconcept.com/blog/gulp-to-webpack</guid>
            <pubDate>Wed, 02 Jan 2019 22:25:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/gulp-webpack.R9v7inID_1Dg2f7.webp" type="image/webp"><source src="https://workingconcept.com//_astro/gulp-webpack.R9v7inID_YGCU.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/gulp-webpack.R9v7inID_1gtBRq.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/gulp-webpack.R9v7inID_1hRfMB.png" decoding="async" loading="lazy" alt width="1300" height="600"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><a href="https://nystudio107.com/blog/an-annotated-webpack-4-config-for-frontend-web-development">Andrew Welch’s landmark webpack treatise</a> covers just about everything you’d need to get working with an organized, comprehensive webpack-based workflow. I know this because I’d recently made the transition from Gulp to webpack with only a fraction of what’s covered in his post. It took me a long time. If you’re looking for an up-close view of the machinery, Andrew’s article will make the most of your five minutes.</p>
<p>If you’re still not sure that webpack makes any sense, or how it can be okay to have 66 dependencies for a demonstration, join me here for a moment.</p>
<p>I’d started pawing at webpack a few times only to get confused or frustrated at the lack of a Gulp-like simplicity.</p>
<p>In hindsight, simplicity was the wrong thing to look for because <em>webpack is just more complicated</em>. </p>
<p>Making produtive use of it demanded that I rethink my approach to front end assets and how they come together. It took me a while to hammer out my humble workflow, and I’d like to share the critical shift in orientation that finally made all the technical hurdles worth the trouble.</p>
<h2>Webpack is for Modern JavaScript</h2>
<p>As much as I don’t want to admit it, Peter Jang’s <a href="https://medium.com/the-node-js-collection/modern-javascript-explained-for-dinosaurs-f695e9747b70">Modern JavaScript Explained for Dinosaurs</a> was the first article that put things into context for me. Webpack doesn’t just do stuff to files. It understands packages and dependencies and web performance. That’s massively important and distinctly different from Gulp.</p>
<p>Imagine a project that relies on jQuery, some plugins and scripts, and a Gulp workflow. For that project, webpack will feel like overkill. It probably is overkill. The retooling and complication may not be worth it for <em>every</em> project. </p>
<p>Another project may use ES6 and Babel, Vue/React, PostCSS, and incorporate Critical CSS and polyfills and asynchronous JavaScript loading. If you’ve built such a project without using webpack, odds are you’ve found yourself in a bit of a kerfuffle. These are great tools, and yet getting everything neatly packaged and cache-broken for production gets increasingly tedious. <strong>Once your expanding front end toolset starts to feel like a herd of cats, that’s when webpack’s majesty reveals itself.</strong> Whether you’ve experienced that pain or not, the first step is to understand how webpack is designed to solve problems differently than Gulp.</p>
<h2>A Conveyor Belt and a Production Manager</h2>
<p>Gulp and webpack are different animals.</p>
<p>Using Gulp is like having a conveyor belt where you can tinker with each of the stations. You drop stuff in at different points, it all moves down the line, and it’s dumped out somewhere.</p>
<p>Using webpack is more like hiring a production manager to listen to all your needs and wrangle all the stuff efficiently. Instead of adjusting conveyor belt stations, you tell the production manager where to find things and what you’d like to end up with. You spend most of your time going into great detail clarifying your instructions, and the manager gets it done.</p>
<p>This manager only understands JavaScript so you’ll have help convey what CSS and static files are, but the manager understands packages, dependencies, inputs and outputs. This is a big deal, because it’s not just acting on files but working more knowingly with code. The manager also has a network of friends (plugins) that can add expertise to the production department. If you’re all working well together, stuff will end up where you need it and how you need it, for development or production. It’s very fast, too.</p>
<h2>Suggestions</h2>
<p>Webpack doesn’t just want to compile your things and dump them out, it wants to help you solve production problems. "Help me help you," it says. Here are my tips for working with webpack:</p>
<h3>Don’t skim when you’re reading about <em>entry points</em>.</h3>
<p>The webpack magic starts and ends with <em>entry points</em>, so it’s important to understand what they are and how you can use them. The concept shouldn’t be too unfamiliar: <a href="https://webpack.js.org/concepts/entry-points/">entry points</a> are the built assets you’ll reference in your markup so the browser can work with your bundles.</p>
<p>Your current setup might have a global <code>app.js</code> that handles site-wide navigation and search functionality, for example, and then <code>forms.js</code> that you use only on pages with forms. Each could easily be an <em>entry point</em> in your webpack setup.</p>
<p>Where things can get more interesting is that a single entry point can load code asynchronously, dynamically split code, and usher in all kinds of fancy that doesn’t have an equivalent with your current setup.</p>
<p>But they don’t have to be complicated. The <em>entire</em> contents of an entry point might look like this:</p>
<pre><code class="language-js">import '../css/forms.css';</code></pre>
<p>That’s an imaginary <code>/src/js/forms.js</code> file containing only a CSS import. As I mentioned above, webpack understands JavaScript—but a common practice is to lump in CSS and have webpack split out and process JS and CSS separately. After the webpack magic is complete, it’ll have built <code>/dist/css/forms.bundle.css</code> which I can include on the front end. In this case I’d also have a <code>/dist/js/forms.bundle.js</code> I’d ignore.</p>
<p>I can, however, start including other JS code or writing JavaScript directly in that entry point and drop the resulting <code>/dist/js/forms.bundle.js</code> in my site’s markup.</p>
<p>Know what an entry point is and how you intend to use one or several of them in your project.</p>
<h3>Don’t bring files and expect to leave with some other files.</h3>
<p><strong>Bring packages and code and expect to leave with <em>bundles</em>.</strong></p>
<p>If you’re used to downloading JavaScript, putting it in a publicly-accessible folder, and adding script tags to your markup before relying on that code, your life is going to get way easier and your projects are going to get much tidier. </p>
<p>You might start whistling while you work.</p>
<p>For example, let’s say you’re using <a href="https://prismjs.com/">Prism</a> to add syntax highlighting to your code blocks.</p>
<p>Previously, you might have visited <a href="https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript">the download page</a> to customize and download a build with whatever features you’d want. You’d plop them in your web folder and add the JS and CSS to your markup:</p>
<pre><code class="language-html">&lt;script src="prism.js"&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="prism.css"&gt;</code></pre>
<p>You could have put them in a project source folder and had gulp minify and combine them as well, but ultimately you were putting the result in the web root one way or another. You would then have to manually grab new files for changes, whether it’s to Prism’s core code, a change of color theme, or different language support.</p>
<p>Including Prism’s JavaScript alone will have it get to work on your page’s code blocks, but you can also add a <code>data-manual</code> property to your script tag to prevent that and configure it via API. </p>
<pre><code class="language-html">&lt;script src="prism.js" data-manual&gt;&lt;/script&gt;</code></pre>
<p>You’d then write some code telling Prism what to do, putting it in <code>&lt;script&gt;</code> tags on your page or in another file (like <code>app.js</code>) that you include as well. This is the same approach as including a jQuery script tag, some jQuery plugin script tags, and then adding your own JavaScript file or script block to use jQuery and its plugins to make stuff happen.</p>
<p>Now let’s look at this Prism example with a webpack setup.</p>
<p>By virtue of using webpack, we’ve probably already included bundle references somewhere in our markup:</p>
<pre><code class="language-js">&lt;script src="app.bundle.js"&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="app.bundle.css"&gt;</code></pre>
<p>The <code>app.</code> filename is deliberate, because this bundle is actually a collection of different things and not just our Prism assets; each file is the result of webpack’s bundling process. That process likely includes combining and minifying files (among other things), resulting in <code>app.bundle.js</code> and <code>app.bundle.css</code>. You can choose whatever filenames you’d like, of course.</p>
<p>Webpack knows what to do with <a href="https://www.npmjs.com/">npm packages</a>, which you probably already know are a vast expanse of JavaScript repositories you can neatly require with <code>npm</code> or <code>yarn</code> at the command line.</p>
<p>That’s exactly what we’ll in the webpack equivalent of our example:</p>
<pre><code class="language-js">npm install prismjs</code></pre>
<p>npm now downloads Prism for you into your project’s <code>node_modules</code> directory. Unless you’ve done something horribly wrong, <code>node_modules</code> <em>shouldn’t</em> be web-accessible.</p>
<p>First we’ll want to load Prism into some JavaScript in our project:</p>
<pre><code class="language-js">import Prism from 'prismjs';</code></pre>
<p>Nothing happens with this alone, so we have to add one line that tells Prism to highlight all the code blocks it finds:</p>
<pre><code class="language-js">import Prism from 'prismjs';

Prism.highlightAll();</code></pre>
<p>That’s it! As long as this JavaScript is included in a webpack <em>entry point</em>, it’ll get bundled with everything else. No need to move anything else into your web root or update your markup. You also get a few more perks that come with package management:</p>
<ol>
<li>When you <code>npm install</code>, the package and its version are added to your <code>package.json</code> file. This will signal to other developers that your project uses Prism, and the package manager will look out for compatibility issues and make sure <em>any</em> project code can share the same version of Prism to keep things efficient.</li>
<li>Your repository is cleaner since dependencies are downloaded separately. (Commit your bundles, don’t commit your <code>node_modules</code>.) This can be helpful for licensing, too.</li>
</ol>
<p>Webpack will champion your dependencies and performance needs, so try to shift your thinking toward what <em>code</em> you need and where it should have an impact rather than what <em>files</em> you need and how you’ll manage them in your webroot and your markup.</p>
<h3>Be patient.</h3>
<p>I found many helpful articles on my quest that had totally incompatible approaches to solve similar problems. It’s complicated, there’s a whole lot you can do, and it’ll feel fantastic when all the pieces are working <em>for</em> you and speeding up your development process.</p>
<p>I’m far from an expert on the subject, but my prior confusion was genuine and long-lasting. Despite the significant effort to use webpack in my projects, my code is cleaner and my build process is much smoother and I’d probably have tried to make the move sooner if I’d known how much it would improve my development efforts in general.</p>
<p>I hope this is helpful! Please post a comment or <a href="mailto:hello@workingconcept.com">send me an email</a> if you have questions, comments, or corrections.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Git Annex and Tower]]></title>
            <link>https://workingconcept.com/blog/git-annex-and-tower</link>
            <guid>https://workingconcept.com/blog/git-annex-and-tower</guid>
            <pubDate>Tue, 24 Nov 2015 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Just as GitLab <a href="https://about.gitlab.com/2015/11/23/announcing-git-lfs-support-in-gitlab/">announced support for the more popular git-lfs</a>, I’ve figured out how to get <a href="http://www.git-tower.com/">Tower</a> working with <a href="https://about.gitlab.com/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/">Git Annex</a>.</p><p>If you’ve been using annex in your GitLab repositories, and your Tower commits fail with</p><blockquote><p>git: 'annex' is not a git command.</p></blockquote></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>…you’ve likely resorted to making commits from the command line. Your shell is configured to look in a few different places for other binaries, so <code>git</code> can look around for <code>git-annex</code>, which probably lives in <code>/usr/local/bin</code>. Tower is more limited to where it can look around, so you can fix this problem by installing a git binary to <code>/usr/local/bin</code> and telling Tower to use that one.</p><p>I already had Homebrew installed, so it was a matter of <code>brew install git</code>. I could confirm that <code>/usr/local/bin/git</code> existed, then select exactly that from Tower’s <em>Git Binary</em> menu in the <em>Git Config</em> pane.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/tower-git-binary.DwBUKu6e_11k3SY.webp" type="image/webp"><source src="https://workingconcept.com//_astro/tower-git-binary.DwBUKu6e_Z1FVeMc.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/tower-git-binary.DwBUKu6e_Z1PIweh.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/tower-git-binary.DwBUKu6e_Zk7VqI.png" decoding="async" loading="lazy" alt="Screenshot of Tower's Git Config settings" width="732" height="200"> </picture> <figcaption> <p> Select your /usr/local/bin Git binary.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Commits from tower should now work fine, just in time for you to move on from git-annex and set up for git-lfs.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Amazon Reviewer Widget]]></title>
            <link>https://workingconcept.com/blog/amazon-reviewer-widget</link>
            <guid>https://workingconcept.com/blog/amazon-reviewer-widget</guid>
            <pubDate>Mon, 03 Dec 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/amazon-reviewer-widget.Erjnwh05_Z100iT0.webp" type="image/webp"><source src="https://workingconcept.com//_astro/amazon-reviewer-widget.Erjnwh05_ZkNOUX.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/amazon-reviewer-widget.Erjnwh05_Z28ALxW.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/amazon-reviewer-widget.Erjnwh05_Z2kpqzH.png" decoding="async" loading="lazy" alt="Screenshot of the Amazon.com reviewer widget for the Mac OSX Dashboard; rank, helpful votes, number of reviews" width="250" height="83"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I often waste time checking to see whether any of my Amazon.com reviews have landed more helpful votes. For that reason, I hacked together a quick OSX Dashboard Widget that lets me watch obsessively without getting a browser involved. Enjoy<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>!</p><p><a href="https://assets.workingconcept.com/blog/Amazon%20Reviewer%20Profile.wdgt.zip">Amazon Reviewer Profile.wdgt.zip</a> (23KB)</p><div class="footnotes"><hr><ol><li id="fn:1"><p>This could be a bit too rough around the edges for you; there’s no icon, it could break if Amazon updates the profile page markup, and a long name or wildly different ranking might even be enough to make it ugly. You get no warranty, but I’d also be happy to clean it up if enough others actually use it. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p></li></ol></div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Page Layers]]></title>
            <link>https://workingconcept.com/blog/page-layers</link>
            <guid>https://workingconcept.com/blog/page-layers</guid>
            <pubDate>Tue, 08 Jan 2013 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>One discovery this week needn’t be a secret: it’s an app called <a href="http://www.pagelayers.com/">Page Layers</a>, and it’s already making my life easier.</p><p>Often I’ll need to put together mockups of an existing web page, and I’ll start by taking screenshots and breaking them up in Photoshop. This can take a significant chunk of time, and gets excessively annoying should the site’s content change during the Photoshop spree.</p><p>Page Layers solves both problems quickly and simply: feed it a URL and it’ll capture a screenshot like <a href="http://derailer.org/paparazzi/">Paparazzi</a> and similar programs. The critical difference is that it’ll allow you to save a layered PSD file, whose folder names and hierarchy come directly from the page markup. All text gets flattened, and if a particular element needs to be further separated it’s as simple as adding a <code>span</code> element before running through Page Layers. It’s, simple, smart, and surprisingly fast even on my mid-2009 MacBook Pro.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/page-layers.CM9EsRNI_79x9o.webp" type="image/webp"><source src="https://workingconcept.com//_astro/page-layers.CM9EsRNI_Z1K4olq.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/page-layers.CM9EsRNI_Z3r635.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/page-layers.CM9EsRNI_Z1p685Q.jpg" decoding="async" loading="lazy" alt="Screenshot of workingconcept.com homepage in Photoshop with automatically-generated items in the Layers panel" width="740" height="330"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>While there may already be a less primitive way to jump-start Photoshop mockups of existing pages, Page Layers has already been a helpful addition to my workflow. It fits a niche role, but works its reliable magic just as well as I’d hoped.</p><p>It’s available on the <a href="https://itunes.apple.com/us/app/page-layers/id437835477?mt=12">Mac App Store for $33.99</a>, which I’d call pricey if the time saved didn’t more than justify the price.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Return to Seattle]]></title>
            <link>https://workingconcept.com/blog/return-to-seattle</link>
            <guid>https://workingconcept.com/blog/return-to-seattle</guid>
            <pubDate>Thu, 21 Mar 2013 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/long-drive.CodZJtkL_ZQ2cWK.webp" type="image/webp"><source src="https://workingconcept.com//_astro/long-drive.CodZJtkL_Z1eIKR8.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/long-drive.CodZJtkL_Z2aqcyE.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/long-drive.CodZJtkL_Z2n6eS4.jpg" decoding="async" loading="lazy" alt="U.S. map depicting the route from Orlando to Seattle" width="662" height="352"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Working Concept is now officially back in Seattle! I’m excited to be setting up a studio space in the historic and really cool <a href="http://inscapearts.org/">Inscape Building</a> with <a href="http://www.linkedin.com/in/bryanwilsonseattle">Bryan Wilson</a> and <a href="http://sccottt.com/">Scott Thiessen</a>.</p><p>It’s exhilarating to be in good company and back on the west coast.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Bugify and Alfred]]></title>
            <link>https://workingconcept.com/blog/bugify-and-alfred</link>
            <guid>https://workingconcept.com/blog/bugify-and-alfred</guid>
            <pubDate>Mon, 07 Apr 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’m on a roll with Alfred: this time with a workflow for searching and browsing <a href="https://bugify.com/">Bugify</a> issues via the <a href="https://bugify.com/api">Bugify API</a>. I’ve been increasingly relying on Bugify, so it’s helpful to quickly find an issue and jump straight to it in a browser.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/bugify-alfred.BnKfKnCt_Z1MCxh3.webp" type="image/webp"><source src="https://workingconcept.com//_astro/bugify-alfred.BnKfKnCt_Z1bNj1X.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/bugify-alfred.BnKfKnCt_ZYkoN5.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/bugify-alfred.BnKfKnCt_1sNccj.png" decoding="async" loading="lazy" alt="Screenshot of Alfred displaying Bugify issues filtered by test keyword" width="528" height="513"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’d like to try it out, you’ll need to set the URL of your Bugify install along with your API key. I’m not sure whether there’s a better way to do this, so it’ll require two steps:</p><ol><li><code>setbugifyurl http://yourbugifyurl.com/</code> (with trailing slash)</li><li><code>setbugifykey YOUR_BUGIFY_API_KEY</code></li></ol><p>Once your connection details are stored, try any of the following:</p><ul><li><code>bugify projects</code> to list projects, then select one to view issues for that project</li><li><code>bugify users</code> to list users, where selecting one will list issues for that user</li><li><code>bugify filters</code> to list your custom filters and then view issues within them</li><li><code>bugify search KEYWORD</code> or <code>bugify KEYWORD</code> to search issues</li></ul><p>I whipped up some icons for issue statuses, and happily stole others from Bugify and <a href="http://www.entypo.com/">Entypo</a>.</p><p>Enjoy, and promise to be gentle if you need to point out anything I’ve done questionably.</p><p>Download <a href="https://assets.workingconcept.com/blog/Bugify.alfredworkflow.zip">Bugify.alfredworkflow</a> or check out <a href="https://github.com/mattstein/alfred-bugify-workflow">on GitHub</a>.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Recreating a Transparent Background for Logos and Type]]></title>
            <link>https://workingconcept.com/blog/recreating-a-transparent-background-for-logos-and-type</link>
            <guid>https://workingconcept.com/blog/recreating-a-transparent-background-for-logos-and-type</guid>
            <pubDate>Sun, 20 Mar 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>More than once, I’ve been stuck having to use images where a logo and/or some type is flattened onto a white background. The goal is to achieve something similar to the “Multiply” layer blending mode, but be able to save a PNG with a completely transparent background and no nasty, pixelated artifacts. About a year ago, I found a perfect Quartz-based app (or plugin, I forget which) that could remove the solid background from an image while preserving reflections, drop shadows, and the subtle parts of an image that have more complicated opacity. Unfortunately, I never bookmarked the author’s site nor did I write a blog post or a Tweet or anything that would have been useful.</p><p>After hours of searching for said app, I stumbled upon <a href="https://web.archive.org/web/20190407060628/http://mikes3d.com/extra/scripting-plugins/killwhite/" target="_blank">Mikeal Simburger’s KillWhite</a>, a Photoshop plugin (and 64-bit PixelBender plugin!) that does the same thing. And what a relief. It works just as well as the mysterious app I first used, and even better is that Mikeal is working on a version that will key out a selected color.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/alpha-example.BAXAIOlM_Z1WJSfb.webp" type="image/webp"><source src="https://workingconcept.com//_astro/alpha-example.BAXAIOlM_Z1lUE06.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/alpha-example.BAXAIOlM_Z19rJLd.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/alpha-example.BAXAIOlM_1iFQeb.png" decoding="async" loading="lazy" alt="Comparison of a logos on a white background, selected transparency, and Pixel Bender RemoteWhite Photoshop plugin" width="400" height="145"> </picture> <figcaption> <p> Comparison.  </p> </figcaption> </figure> </div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Date Span Alfred Workflow]]></title>
            <link>https://workingconcept.com/blog/date-span-alfred-workflow</link>
            <guid>https://workingconcept.com/blog/date-span-alfred-workflow</guid>
            <pubDate>Wed, 02 Apr 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>In an unprecedented frenzy of blogworthy activity, I’ve just created an Alfred workflow for quickly getting the range between two dates. After hours of contemplation, I decided to call it Date Span.</p><p>It uses PHP’s DateInterval to take a variety of datetime formats and output the range in various units. For example, give it a single date and it’ll calculate based on the current date and time:</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/alfred-datespan-future.Di0xNuAH_Z6Mk9L.webp" type="image/webp"><source src="https://workingconcept.com//_astro/alfred-datespan-future.Di0xNuAH_Z2jwLEC.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/alfred-datespan-future.Di0xNuAH_gp771.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/alfred-datespan-future.Di0xNuAH_2gv57U.png" decoding="async" loading="lazy" alt="Screenshot of Alfred using datespan keyword to return days, weeks, hours and minutes to 1/19/2038" width="1210" height="724"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Give it two dates (separated by <code> to </code>) and it’ll calculate the difference between them:</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/alfred-datespan-range.Df-Y0Ngu_1YoIMu.webp" type="image/webp"><source src="https://workingconcept.com//_astro/alfred-datespan-range.Df-Y0Ngu_Z316ID.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/alfred-datespan-range.Df-Y0Ngu_1F8Bek.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/alfred-datespan-range.Df-Y0Ngu_Z1n2z8n.png" decoding="async" loading="lazy" alt="Screenshot of Alfred using datespan keyword to display days, weeks, hours and minutes between two dates" width="1210" height="724"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>And just because it’s friendly, it’ll remind you if the date is in the past:</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/alfred-datespan-past.CwFlHr4g_Z1poQlE.webp" type="image/webp"><source src="https://workingconcept.com//_astro/alfred-datespan-past.CwFlHr4g_18Fz8Y.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/alfred-datespan-past.CwFlHr4g_x2V4r.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/alfred-datespan-past.CwFlHr4g_Z2v4t5B.png" decoding="async" loading="lazy" alt="Screenshot of Alfred displaying weeks, days, hours and minutes to a date in the past" width="1210" height="724"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’m not thrilled with the icon I whipped up for it, and I’ve not tested my theoretical bash-centric guess at timezone detection. And PHP 5.3+ is required.</p><p>Enjoy, and please complain about anything that’s stupid!</p><p>Download <a href="https://assets.workingconcept.com/blog/DateSpan.alfredworkflow.zip">DateSpan.alfredworkflow.zip</a> or check it out <a href="https://github.com/mattstein/alfred-datespan">on GitHub</a>.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2019 VPS Provider Update]]></title>
            <link>https://workingconcept.com/blog/2019-vps-provider-update</link>
            <guid>https://workingconcept.com/blog/2019-vps-provider-update</guid>
            <pubDate>Sat, 26 Oct 2019 21:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/2019-vps-update.DdaE2KCG_Z1yAwIh.svg" type="image/svg+xml"><source src="https://workingconcept.com//_astro/2019-vps-update.DdaE2KCG_Z1yAwIh.svg" type="image/svg+xml"><source src="https://workingconcept.com//_astro/2019-vps-update.DdaE2KCG_Z1yAwIh.svg" type="image/svg+xml">  <img src="https://workingconcept.com//_astro/2019-vps-update.DdaE2KCG_Z1yAwIh.svg" decoding="async" loading="lazy" alt width="1360" height="600"> </picture>  </figure> </div> <aside class="mx-auto px-6 md:px-0 max-w-md text-teal font-sans text-base leading-normal relative"> <span class="w-4 h-4 inline-block absolute md:-ml-8" style="top:0.75rem"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="max-w-full h-auto" fill="currentColor"> <path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path> </svg> </span> <div class="ml-8 md:ml-0"><p>This article contains affiliate links. If you find that unsavory, scroll to the bottom for plain ones.</p>
</div> </aside><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>It’s been almost one year since I embraced VPS hosting for projects and <a href="{entry:419@1:url||https://workingconcept.com/blog/2018-budget-vps-survey}">published some provider benchmarks I collected</a>. I thought I’d share an update with a few more benchmarks and how I’m hosting things now.<br></p><h2>Briefly</h2><p>I’ve been using <a href="https://forge.laravel.com">Laravel Forge</a> to provision servers, with <a href="https://m.do.co/c/f1391f41e5e8">Digital Ocean</a> or <a href="https://aws.amazon.com/">AWS</a> for client projects and <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a> + <a href="https://subnetlabs.com/billing/aff.php?aff=116">Impact VPS</a> + <a href="https://billing.virmach.com/aff.php?aff=8600">VirMach</a> for my own. I <a href="{entry:729@1:url||https://workingconcept.com/blog/forge-backups-restic-backblaze-b2}">tailored a quick restic + B2 setup for versioned backups</a> when provider snapshots aren’t available.</p><h2>How I’m Hosting Projects</h2><p>As much as I liked <a href="https://centminmod.com/">Centmin Mod</a> for its speed, active updates and ease of use, I was still spending too much time provisioning and updating servers. </p><p>I switched to <a href="https://forge.laravel.com">Laravel Forge</a> and haven’t looked back.</p><p>Forge provisions quickly, uses solid defaults, and is just configurable enough in ways I like. It has a lovely feature called <em>recipes</em> for running scripts on any number of servers, and provisions with with big name providers as well as the smaller ones I like to use and try out.</p><p>I’ve kept with <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a>, <a href="https://subnetlabs.com/billing/aff.php?aff=116">Impact VPS</a>, and <a href="https://billing.virmach.com/aff.php?aff=8600">VirMach</a> for most of my own projects because each has been inexpensive, reliable, and well-supported. I can get more CPU for less money than big-name providers, so I do. And in lieu of snapshot backups, I use <a href="{entry:729:url}">my own restic+B2 setup</a> that’s been quick to set up, rock solid and cheap. Backups are incremental and similar to Time Machine on macOS, minus the gratuitous time travel UI.</p><p>For client projects, I stick to <a href="https://m.do.co/c/f1391f41e5e8">Digital Ocean</a> or <a href="https://aws.amazon.com/">AWS</a> mostly for snapshot backups, built in monitoring, and ease access sharing when needed. SLAs and GDPR compliance are also a must, and the larger companies always have clear policies in place.</p><h2>Field Notes</h2><p>It seems like no matter the size of the host, random networking issues are a fact of life. No one host has perfect uptime or performance, so I’ve also given up on that as a realistic goal.</p><p>I’ve been mostly put out by <a href="https://www.ssdnodes.com/manage/aff.php?aff=686">SSD Nodes</a>. The performance value is real and their “10x” NVMe offering is eye-wateringly fast, I just can’t stomach the constant marketing hype and lackluster support. I’ve tried a few SSD Nodes servers and none has come with any perceivable catch; low (or zero) CPU steal, admirable uptime and response times, and things behave as I’d expect. It seems like once they sell you a decent VPS, everything else is as barebones as it can be. Tough to get excited about.</p><p><a href="https://clients.ionswitch.com/aff.php?aff=79">IonSwitch</a> deserves honorable mention. After I posted my last article Stan reached out privately and mentioned I might want to give them a try. Stan went out of his way to answer questions, share some of what he looks at as measures of strong performance, even shared some of what goes on behind the scenes. Always prompt and thoughtful responding to tickets, good value for great service. I used an IonSwitch VPS for production for a few months until an unfortunate series of networking incidents forced me to move elsewhere. Stan’s response was unfailingly prompt, understanding and unnecessarily generous.</p><p>I learned to watch out for CPU wait and steal which can indicate slowdowns from overcrowded hardware or noisy neighbors. I added <a href="https://www.serverhunter.com/">Server Hunter</a> and <a href="https://nodequery.com/">NodeQuery</a> to my toolkit and stopped myself from using <a href="https://www.netdata.cloud/">NetData</a> because that’s just too much information and I have other things I should be doing. </p><p>I learned individual core speed is critical for PHP (which makes sense), and that MySQL can scream with multiple cores.</p><h2>More Benchmarks</h2><p>I also benchmarked a few more servers since my roundup. I can’t help it, but I’m getting better at exercising restraint.<br></p><p>Note that I’ve usually benchmarked a VPS after finding a compelling deal, so in about half of these cases I was able to score a cheap VPS on sale and benchmark that. There was some hunting involved, but every deal was publicly available at some point.</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>The Candidates</h2></div><div class="table-block pl-6 copy overflow-x-auto max-w-full lg:mx-auto lg:pl-0 lg:max-w-lg"> <table> <thead> <tr> <th class="text-left" width key="0"> Provider+Plan </th><th class="text-left" width key="1"> Xeon CPU </th><th class="text-left" width key="2"> RAM </th><th class="text-left" width key="3"> Storage </th><th class="text-left" width key="4"> Cost </th><th class="text-left" width key="5"> Location </th> </tr> </thead> <tbody> <tr key="row-0"> <td key="row-0-column-0" class="text-left"> IonSwitch 2GB NVMe </td><td key="row-0-column-1" class="text-left"> 2×E5-2670 </td><td key="row-0-column-2" class="text-left"> 2 GB </td><td key="row-0-column-3" class="text-left"> 25 GB NVMe </td><td key="row-0-column-4" class="text-left"> $9/month </td><td key="row-0-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-1"> <td key="row-1-column-0" class="text-left"> IonSwitch 4GB </td><td key="row-1-column-1" class="text-left"> 4×E5-2690 </td><td key="row-1-column-2" class="text-left"> 4 GB </td><td key="row-1-column-3" class="text-left"> 50 GB SSD </td><td key="row-1-column-4" class="text-left"> $11.67/month </td><td key="row-1-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-2"> <td key="row-2-column-0" class="text-left"> Hyper Expert 2GB </td><td key="row-2-column-1" class="text-left"> 2×E5-2670 </td><td key="row-2-column-2" class="text-left"> 2 GB </td><td key="row-2-column-3" class="text-left"> 22 GB </td><td key="row-2-column-4" class="text-left"> $4.99/month </td><td key="row-2-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-3"> <td key="row-3-column-0" class="text-left"> VirMach 1GB </td><td key="row-3-column-1" class="text-left"> 1×? </td><td key="row-3-column-2" class="text-left"> 1 GB </td><td key="row-3-column-3" class="text-left"> 10 GB SSD </td><td key="row-3-column-4" class="text-left"> $1.31/month </td><td key="row-3-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-4"> <td key="row-4-column-0" class="text-left"> VirMach 3GB </td><td key="row-4-column-1" class="text-left"> 2×? </td><td key="row-4-column-2" class="text-left"> 3 GB </td><td key="row-4-column-3" class="text-left"> 40 GB SSD </td><td key="row-4-column-4" class="text-left"> $4.50/month </td><td key="row-4-column-5" class="text-left"> Dallas, TX </td> </tr><tr key="row-5"> <td key="row-5-column-0" class="text-left"> ITLDC 2GB VDS </td><td key="row-5-column-1" class="text-left"> 2×? </td><td key="row-5-column-2" class="text-left"> 2 GB </td><td key="row-5-column-3" class="text-left"> 15 GB SSD </td><td key="row-5-column-4" class="text-left"> $3.57/month </td><td key="row-5-column-5" class="text-left"> Los Angeles, CA </td> </tr><tr key="row-6"> <td key="row-6-column-0" class="text-left"> SolvedByData 2GB </td><td key="row-6-column-1" class="text-left"> 2×E3-1240 </td><td key="row-6-column-2" class="text-left"> 2 GB </td><td key="row-6-column-3" class="text-left"> 30 GB </td><td key="row-6-column-4" class="text-left"> $1.25/month </td><td key="row-6-column-5" class="text-left"> Los Angeles, CA </td> </tr><tr key="row-7"> <td key="row-7-column-0" class="text-left"> SolvedByData 6GB </td><td key="row-7-column-1" class="text-left"> 6×E3-1240 </td><td key="row-7-column-2" class="text-left"> 6 GB </td><td key="row-7-column-3" class="text-left"> 100 GB SSD </td><td key="row-7-column-4" class="text-left"> $3.75/month </td><td key="row-7-column-5" class="text-left"> Los Angeles, CA </td> </tr><tr key="row-8"> <td key="row-8-column-0" class="text-left"> SSD ﻿Nodes ﻿”10x” XL </td><td key="row-8-column-1" class="text-left"> 4×Gold 6140 </td><td key="row-8-column-2" class="text-left"> 16 GB </td><td key="row-8-column-3" class="text-left"> 80 GB NVMe </td><td key="row-8-column-4" class="text-left"> $13.99/month </td><td key="row-8-column-5" class="text-left"> Dallas, TX </td> </tr><tr key="row-9"> <td key="row-9-column-0" class="text-left"> BigPowerHosting 2GB </td><td key="row-9-column-1" class="text-left"> 3×E5-1650 </td><td key="row-9-column-2" class="text-left"> 2 GB </td><td key="row-9-column-3" class="text-left"> 40 GB SSD </td><td key="row-9-column-4" class="text-left"> $6.40/month </td><td key="row-9-column-5" class="text-left"> Los Angeles, CA </td> </tr><tr key="row-10"> <td key="row-10-column-0" class="text-left"> UpCloud 1GB </td><td key="row-10-column-1" class="text-left"> 1×Gold 6136 </td><td key="row-10-column-2" class="text-left"> 1 GB </td><td key="row-10-column-3" class="text-left"> 25 GB </td><td key="row-10-column-4" class="text-left"> $5/month </td><td key="row-10-column-5" class="text-left"> San Jose, CA </td> </tr><tr key="row-11"> <td key="row-11-column-0" class="text-left"> Data Packet 1GB </td><td key="row-11-column-1" class="text-left"> 16×E5-2670 </td><td key="row-11-column-2" class="text-left"> 1 GB </td><td key="row-11-column-3" class="text-left"> 123 GB </td><td key="row-11-column-4" class="text-left"> $4/month </td><td key="row-11-column-5" class="text-left"> Killeen, TX </td> </tr> </tbody> </table> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><aside><p>The virtualization type for VirMach and ITLDC’s servers didn't reveal the model number of the CPU.</p></aside></div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Charted Specs</h4> <figure class="chart-container"> <svg viewBox="0 0 810 794" width="810" height="794" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(203,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> $/Month </text>  <rect width="12" height="10" fill="#81c9bf" x="89" y="1"></rect> <text x="107" y="10" class="legend-label"> vCPU Cores </text>  <rect width="12" height="10" fill="#e16db3" x="202" y="1"></rect> <text x="220" y="10" class="legend-label"> RAM (GB) </text>  <rect width="12" height="10" fill="#e8cf3c" x="299" y="1"></rect> <text x="317" y="10" class="legend-label"> Storage (GB) </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="745" opacity="0.2"></rect><rect width="1" fill="#666" x="38%" y="30" height="745" opacity="0.2"></rect><rect width="1" fill="#666" x="50%" y="30" height="745" opacity="0.2"></rect><rect width="1" fill="#666" x="62%" y="30" height="745" opacity="0.2"></rect><rect width="1" fill="#666" x="73%" y="30" height="745" opacity="0.2"></rect><rect width="1" fill="#666" x="85%" y="30" height="745" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="745" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="67" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="129" class="label"> IonSwitch 4GB </text><text x="25%" y="191" class="label"> Hyper Expert 2GB </text><text x="25%" y="253" class="label"> VirMach 1GB </text><text x="25%" y="315" class="label"> VirMach 3GB </text><text x="25%" y="377" class="label"> ITLDC 2GB VDS </text><text x="25%" y="439" class="label"> SolvedByData 2GB </text><text x="25%" y="501" class="label"> SolvedByData 6GB </text><text x="25%" y="563" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="625" class="label"> BigPowerHosting 2GB </text><text x="25%" y="687" class="label"> UpCloud 1GB </text><text x="25%" y="749" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="789"> 0 </text><text class="interval-label" x="38%" y="789"> 25 </text><text class="interval-label" x="50%" y="789"> 50 </text><text class="interval-label" x="62%" y="789"> 75 </text><text class="interval-label" x="73%" y="789"> 100 </text><text class="interval-label" x="85%" y="789"> 125 </text><text class="interval-label" x="97%" y="789"> 150 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="4.2%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="62" key="3"></rect><rect width="11.666666666666666%" height="12" fill="#e8cf3c" x="27%" y="75" key="4"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="92" opacity="0.2"></rect>  <rect width="6.533333333333333%" height="12" fill="#00a1d5" x="27%" y="98" key="1"></rect><rect width="1.8666666666666667%" height="12" fill="#81c9bf" x="27%" y="111" key="2"></rect><rect width="1.8666666666666667%" height="12" fill="#e16db3" x="27%" y="124" key="3"></rect><rect width="23.333333333333332%" height="12" fill="#e8cf3c" x="27%" y="137" key="4"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="154" opacity="0.2"></rect>  <rect width="2.328666666666667%" height="12" fill="#00a1d5" x="27%" y="160" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="173" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="186" key="3"></rect><rect width="10.266666666666667%" height="12" fill="#e8cf3c" x="27%" y="199" key="4"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="216" opacity="0.2"></rect>  <rect width="0.6113333333333334%" height="12" fill="#00a1d5" x="27%" y="222" key="1"></rect><rect width="0.4666666666666667%" height="12" fill="#81c9bf" x="27%" y="235" key="2"></rect><rect width="0.4666666666666667%" height="12" fill="#e16db3" x="27%" y="248" key="3"></rect><rect width="4.666666666666667%" height="12" fill="#e8cf3c" x="27%" y="261" key="4"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="278" opacity="0.2"></rect>  <rect width="2.1%" height="12" fill="#00a1d5" x="27%" y="284" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="297" key="2"></rect><rect width="1.4000000000000001%" height="12" fill="#e16db3" x="27%" y="310" key="3"></rect><rect width="18.666666666666668%" height="12" fill="#e8cf3c" x="27%" y="323" key="4"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="340" opacity="0.2"></rect>  <rect width="1.666%" height="12" fill="#00a1d5" x="27%" y="346" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="359" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="372" key="3"></rect><rect width="7%" height="12" fill="#e8cf3c" x="27%" y="385" key="4"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="402" opacity="0.2"></rect>  <rect width="0.5833333333333334%" height="12" fill="#00a1d5" x="27%" y="408" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="421" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="434" key="3"></rect><rect width="14%" height="12" fill="#e8cf3c" x="27%" y="447" key="4"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="464" opacity="0.2"></rect>  <rect width="1.75%" height="12" fill="#00a1d5" x="27%" y="470" key="1"></rect><rect width="2.8000000000000003%" height="12" fill="#81c9bf" x="27%" y="483" key="2"></rect><rect width="2.8000000000000003%" height="12" fill="#e16db3" x="27%" y="496" key="3"></rect><rect width="46.666666666666664%" height="12" fill="#e8cf3c" x="27%" y="509" key="4"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="526" opacity="0.2"></rect>  <rect width="6.528666666666666%" height="12" fill="#00a1d5" x="27%" y="532" key="1"></rect><rect width="1.8666666666666667%" height="12" fill="#81c9bf" x="27%" y="545" key="2"></rect><rect width="7.466666666666667%" height="12" fill="#e16db3" x="27%" y="558" key="3"></rect><rect width="37.333333333333336%" height="12" fill="#e8cf3c" x="27%" y="571" key="4"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="588" opacity="0.2"></rect>  <rect width="2.9866666666666672%" height="12" fill="#00a1d5" x="27%" y="594" key="1"></rect><rect width="1.4000000000000001%" height="12" fill="#81c9bf" x="27%" y="607" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="620" key="3"></rect><rect width="18.666666666666668%" height="12" fill="#e8cf3c" x="27%" y="633" key="4"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="650" opacity="0.2"></rect>  <rect width="2.3333333333333335%" height="12" fill="#00a1d5" x="27%" y="656" key="1"></rect><rect width="0.4666666666666667%" height="12" fill="#81c9bf" x="27%" y="669" key="2"></rect><rect width="0.4666666666666667%" height="12" fill="#e16db3" x="27%" y="682" key="3"></rect><rect width="11.666666666666666%" height="12" fill="#e8cf3c" x="27%" y="695" key="4"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="712" opacity="0.2"></rect>  <rect width="1.8666666666666667%" height="12" fill="#00a1d5" x="27%" y="718" key="1"></rect><rect width="7.466666666666667%" height="12" fill="#81c9bf" x="27%" y="731" key="2"></rect><rect width="0.4666666666666667%" height="12" fill="#e16db3" x="27%" y="744" key="3"></rect><rect width="57.4%" height="12" fill="#e8cf3c" x="27%" y="757" key="4"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="774" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="31.7%" y="45.5" data-item-percent="0.06"> $9/month </text><text class="bar-label" x="28.433333333333334%" y="58.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.433333333333334%" y="71.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="39.166666666666664%" y="84.5" data-item-percent="0.16666666666666666"> 25 GB  </text><text class="bar-label" x="34.03333333333333%" y="107.5" data-item-percent="0.09333333333333334"> $14/month </text><text class="bar-label" x="29.366666666666667%" y="120.5" data-item-percent="0.02666666666666667"> 4 </text><text class="bar-label" x="29.366666666666667%" y="133.5" data-item-percent="0.02666666666666667"> 4 GB </text><text class="bar-label" x="50.83333333333333%" y="146.5" data-item-percent="0.3333333333333333"> 50 GB </text><text class="bar-label" x="29.828666666666667%" y="169.5" data-item-percent="0.03326666666666667"> $4.99/month </text><text class="bar-label" x="28.433333333333334%" y="182.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.433333333333334%" y="195.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="37.766666666666666%" y="208.5" data-item-percent="0.14666666666666667"> 22 GB </text><text class="bar-label" x="28.111333333333334%" y="231.5" data-item-percent="0.008733333333333334"> $1.31/month </text><text class="bar-label" x="27.966666666666665%" y="244.5" data-item-percent="0.006666666666666667"> 1 </text><text class="bar-label" x="27.966666666666665%" y="257.5" data-item-percent="0.006666666666666667"> 1 GB </text><text class="bar-label" x="32.16666666666667%" y="270.5" data-item-percent="0.06666666666666667"> 10 GB </text><text class="bar-label" x="29.6%" y="293.5" data-item-percent="0.03"> $4.50/month </text><text class="bar-label" x="28.433333333333334%" y="306.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.9%" y="319.5" data-item-percent="0.02"> 3 GB </text><text class="bar-label" x="46.16666666666667%" y="332.5" data-item-percent="0.26666666666666666"> 40 GB </text><text class="bar-label" x="29.166%" y="355.5" data-item-percent="0.023799999999999998"> $3.57/month </text><text class="bar-label" x="28.433333333333334%" y="368.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.433333333333334%" y="381.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="34.5%" y="394.5" data-item-percent="0.1"> 15 GB </text><text class="bar-label" x="28.083333333333332%" y="417.5" data-item-percent="0.008333333333333333"> $1.25/month </text><text class="bar-label" x="28.433333333333334%" y="430.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.433333333333334%" y="443.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="41.5%" y="456.5" data-item-percent="0.2"> 30 GB </text><text class="bar-label" x="29.25%" y="479.5" data-item-percent="0.025"> $3.75/month </text><text class="bar-label" x="30.3%" y="492.5" data-item-percent="0.04"> 6 </text><text class="bar-label" x="30.3%" y="505.5" data-item-percent="0.04"> 6 GB </text><text class="bar-label" x="74.16666666666666%" y="518.5" data-item-percent="0.6666666666666666"> 100 GB </text><text class="bar-label" x="34.028666666666666%" y="541.5" data-item-percent="0.09326666666666666"> $13.99/month </text><text class="bar-label" x="29.366666666666667%" y="554.5" data-item-percent="0.02666666666666667"> 4 </text><text class="bar-label" x="34.96666666666667%" y="567.5" data-item-percent="0.10666666666666667"> 16 GB </text><text class="bar-label" x="64.83333333333334%" y="580.5" data-item-percent="0.5333333333333333"> 80 GB </text><text class="bar-label" x="30.486666666666668%" y="603.5" data-item-percent="0.04266666666666667"> $6.40/month </text><text class="bar-label" x="28.9%" y="616.5" data-item-percent="0.02"> 3 </text><text class="bar-label" x="28.433333333333334%" y="629.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="46.16666666666667%" y="642.5" data-item-percent="0.26666666666666666"> 40 GB </text><text class="bar-label" x="29.833333333333332%" y="665.5" data-item-percent="0.03333333333333333"> $5/month </text><text class="bar-label" x="27.966666666666665%" y="678.5" data-item-percent="0.006666666666666667"> 1 </text><text class="bar-label" x="27.966666666666665%" y="691.5" data-item-percent="0.006666666666666667"> 1 GB </text><text class="bar-label" x="39.166666666666664%" y="704.5" data-item-percent="0.16666666666666666"> 25 GB </text><text class="bar-label" x="29.366666666666667%" y="727.5" data-item-percent="0.02666666666666667"> $4/month </text><text class="bar-label" x="34.96666666666667%" y="740.5" data-item-percent="0.10666666666666667"> 16 </text><text class="bar-label" x="27.966666666666665%" y="753.5" data-item-percent="0.006666666666666667"> 1 GB </text><text class="inverted-bar-label" x="83.9%" y="766.5" data-item-percent="0.82"> 123 GB </text> </g> </svg> <figcaption> <p>I should be including core speed, but I’m keeping the tables consistent for comparison.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Geekbench Multi-Core + UnixBench Scores</h4> <figure class="chart-container"> <svg viewBox="0 0 810 482" width="810" height="482" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(262,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> Geekbench Multi-Core </text>  <rect width="12" height="10" fill="#81c9bf" x="193" y="1"></rect> <text x="211" y="10" class="legend-label"> UnixBench </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="433" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="90" class="label"> IonSwitch 4GB </text><text x="25%" y="126" class="label"> Hyper Expert 2GB </text><text x="25%" y="162" class="label"> VirMach 1GB </text><text x="25%" y="198" class="label"> VirMach 3GB </text><text x="25%" y="234" class="label"> ITLDC 2GB VDS </text><text x="25%" y="270" class="label"> SolvedByData 2GB </text><text x="25%" y="306" class="label"> SolvedByData 6GB </text><text x="25%" y="342" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="378" class="label"> BigPowerHosting 2GB </text><text x="25%" y="414" class="label"> UpCloud 1GB </text><text x="25%" y="450" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="477"> 0 </text><text class="interval-label" x="41%" y="477"> 3800 </text><text class="interval-label" x="55%" y="477"> 7600 </text><text class="interval-label" x="69%" y="477"> 11400 </text><text class="interval-label" x="83%" y="477"> 15200 </text><text class="interval-label" x="97%" y="477"> 19000 </text><text class="interval-label" x="111%" y="477"> 22800 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="18.063684210526315%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="7.922526315789474%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="41.58736842105263%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="13.025894736842107%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="18.229473684210525%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="6.746894736842105%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="5.585263157894737%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="1.0702631578947368%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="11.284736842105263%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="3.298842105263158%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="22.96736842105263%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="5.934526315789474%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="22.912105263157898%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="6.3961578947368425%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="41.815789473684205%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="13.027368421052632%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect>  <rect width="43.90473684210526%" height="12" fill="#00a1d5" x="27%" y="324" key="1"></rect><rect width="11.754105263157895%" height="12" fill="#81c9bf" x="27%" y="337" key="2"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="354" opacity="0.2"></rect>  <rect width="33.054736842105264%" height="12" fill="#00a1d5" x="27%" y="360" key="1"></rect><rect width="9.267999999999999%" height="12" fill="#81c9bf" x="27%" y="373" key="2"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="390" opacity="0.2"></rect>  <rect width="14.828947368421051%" height="12" fill="#00a1d5" x="27%" y="396" key="1"></rect><rect width="5.671842105263158%" height="12" fill="#81c9bf" x="27%" y="409" key="2"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="426" opacity="0.2"></rect>  <rect width="69.47684210526316%" height="12" fill="#00a1d5" x="27%" y="432" key="1"></rect><rect width="17.403842105263156%" height="12" fill="#81c9bf" x="27%" y="445" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="462" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="45.56368421052632%" y="45.5" data-item-percent="0.25805263157894737"> 4903 </text><text class="bar-label" x="35.422526315789476%" y="58.5" data-item-percent="0.11317894736842106"> 2150.4 </text><text class="bar-label" x="69.08736842105263%" y="81.5" data-item-percent="0.5941052631578947"> 11288 </text><text class="bar-label" x="40.525894736842105%" y="94.5" data-item-percent="0.1860842105263158"> 3535.6 </text><text class="bar-label" x="45.729473684210525%" y="117.5" data-item-percent="0.26042105263157894"> 4948 </text><text class="bar-label" x="34.24689473684211%" y="130.5" data-item-percent="0.09638421052631578"> 1831.3 </text><text class="bar-label" x="33.08526315789474%" y="153.5" data-item-percent="0.07978947368421052"> 1516 </text><text class="bar-label" x="28.570263157894736%" y="166.5" data-item-percent="0.015289473684210526"> 290.5 </text><text class="bar-label" x="38.78473684210526%" y="189.5" data-item-percent="0.16121052631578947"> 3063 </text><text class="bar-label" x="30.79884210526316%" y="202.5" data-item-percent="0.04712631578947368"> 895.4 </text><text class="bar-label" x="50.467368421052626%" y="225.5" data-item-percent="0.32810526315789473"> 6234 </text><text class="bar-label" x="33.434526315789476%" y="238.5" data-item-percent="0.08477894736842105"> 1610.8 </text><text class="bar-label" x="50.4121052631579%" y="261.5" data-item-percent="0.3273157894736842"> 6219 </text><text class="bar-label" x="33.896157894736845%" y="274.5" data-item-percent="0.09137368421052632"> 1736.1 </text><text class="bar-label" x="69.3157894736842%" y="297.5" data-item-percent="0.5973684210526315"> 11350 </text><text class="bar-label" x="40.52736842105263%" y="310.5" data-item-percent="0.18610526315789475"> 3536 </text><text class="bar-label" x="71.40473684210525%" y="333.5" data-item-percent="0.6272105263157894"> 11917 </text><text class="bar-label" x="39.254105263157896%" y="346.5" data-item-percent="0.1679157894736842"> 3190.4 </text><text class="bar-label" x="60.554736842105264%" y="369.5" data-item-percent="0.47221052631578947"> 8972 </text><text class="bar-label" x="36.768%" y="382.5" data-item-percent="0.1324"> 2515.6 </text><text class="bar-label" x="42.328947368421055%" y="405.5" data-item-percent="0.2118421052631579"> 4025 </text><text class="bar-label" x="33.17184210526316%" y="418.5" data-item-percent="0.08102631578947368"> 1539.5 </text><text class="inverted-bar-label" x="95.97684210526316%" y="441.5" data-item-percent="0.9925263157894737"> 18858 </text><text class="bar-label" x="44.90384210526315%" y="454.5" data-item-percent="0.24862631578947367"> 4723.9 </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">PHP + MySQL Performance</h4> <figure class="chart-container"> <svg viewBox="0 0 810 482" width="810" height="482" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(346,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> PHP </text>  <rect width="12" height="10" fill="#81c9bf" x="57" y="1"></rect> <text x="75" y="10" class="legend-label"> MySQL </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="433" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="90" class="label"> IonSwitch 4GB </text><text x="25%" y="126" class="label"> Hyper Expert 2GB </text><text x="25%" y="162" class="label"> VirMach 1GB </text><text x="25%" y="198" class="label"> VirMach 3GB </text><text x="25%" y="234" class="label"> ITLDC 2GB VDS </text><text x="25%" y="270" class="label"> SolvedByData 2GB </text><text x="25%" y="306" class="label"> SolvedByData 6GB </text><text x="25%" y="342" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="378" class="label"> BigPowerHosting 2GB </text><text x="25%" y="414" class="label"> UpCloud 1GB </text><text x="25%" y="450" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="477"> 0 </text><text class="interval-label" x="41%" y="477"> 2 </text><text class="interval-label" x="55%" y="477"> 4 </text><text class="interval-label" x="69%" y="477"> 6 </text><text class="interval-label" x="83%" y="477"> 8 </text><text class="interval-label" x="97%" y="477"> 10 </text><text class="interval-label" x="111%" y="477"> 12 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="27.755%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="17.64%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="21.9219%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="13.041%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="26.8359%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="28.826000000000004%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="42.6069%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="53.227999999999994%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="35.347899999999996%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="55.454%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="18.892999999999997%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="13.607999999999999%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="34.722100000000005%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="14.245000000000001%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="19.9689%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="15.967000000000002%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect>  <rect width="22.101100000000002%" height="12" fill="#00a1d5" x="27%" y="324" key="1"></rect><rect width="11.479999999999999%" height="12" fill="#81c9bf" x="27%" y="337" key="2"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="354" opacity="0.2"></rect>  <rect width="19.642%" height="12" fill="#00a1d5" x="27%" y="360" key="1"></rect><rect width="16.548%" height="12" fill="#81c9bf" x="27%" y="373" key="2"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="390" opacity="0.2"></rect>  <rect width="18.6081%" height="12" fill="#00a1d5" x="27%" y="396" key="1"></rect><rect width="17.877999999999997%" height="12" fill="#81c9bf" x="27%" y="409" key="2"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="426" opacity="0.2"></rect>  <rect width="24.7471%" height="12" fill="#00a1d5" x="27%" y="432" key="1"></rect><rect width="13.341999999999999%" height="12" fill="#81c9bf" x="27%" y="445" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="462" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="55.254999999999995%" y="45.5" data-item-percent="0.39649999999999996"> 3.965 </text><text class="bar-label" x="45.14%" y="58.5" data-item-percent="0.252"> 2.52 </text><text class="bar-label" x="49.4219%" y="81.5" data-item-percent="0.31317"> 3.1317 </text><text class="bar-label" x="40.541%" y="94.5" data-item-percent="0.1863"> 1.863 </text><text class="bar-label" x="54.335899999999995%" y="117.5" data-item-percent="0.38337"> 3.8337 </text><text class="bar-label" x="56.32600000000001%" y="130.5" data-item-percent="0.41180000000000005"> 4.118 </text><text class="bar-label" x="70.1069%" y="153.5" data-item-percent="0.60867"> 6.0867 </text><text class="bar-label" x="80.728%" y="166.5" data-item-percent="0.7604"> 7.604 </text><text class="bar-label" x="62.847899999999996%" y="189.5" data-item-percent="0.5049699999999999"> 5.0497 </text><text class="bar-label" x="82.95400000000001%" y="202.5" data-item-percent="0.7922"> 7.922 </text><text class="bar-label" x="46.393%" y="225.5" data-item-percent="0.2699"> 2.699 </text><text class="bar-label" x="41.108%" y="238.5" data-item-percent="0.1944"> 1.944 </text><text class="bar-label" x="62.222100000000005%" y="261.5" data-item-percent="0.49603"> 4.9603 </text><text class="bar-label" x="41.745000000000005%" y="274.5" data-item-percent="0.20350000000000001"> 2.035 </text><text class="bar-label" x="47.468900000000005%" y="297.5" data-item-percent="0.28527"> 2.8527 </text><text class="bar-label" x="43.467%" y="310.5" data-item-percent="0.22810000000000002"> 2.281 </text><text class="bar-label" x="49.6011%" y="333.5" data-item-percent="0.31573"> 3.1573 </text><text class="bar-label" x="38.98%" y="346.5" data-item-percent="0.16399999999999998"> 1.64 </text><text class="bar-label" x="47.141999999999996%" y="369.5" data-item-percent="0.2806"> 2.806 </text><text class="bar-label" x="44.048%" y="382.5" data-item-percent="0.2364"> 2.364 </text><text class="bar-label" x="46.1081%" y="405.5" data-item-percent="0.26583"> 2.6583 </text><text class="bar-label" x="45.378%" y="418.5" data-item-percent="0.25539999999999996"> 2.554 </text><text class="bar-label" x="52.2471%" y="441.5" data-item-percent="0.35353"> 3.5353 </text><text class="bar-label" x="40.842%" y="454.5" data-item-percent="0.1906"> 1.906 </text> </g> </svg> <figcaption> <p>Lower is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Storage I/O</h4> <figure class="chart-container"> <svg viewBox="0 0 810 482" width="810" height="482" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(246,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> Random Read MB/s </text>  <rect width="12" height="10" fill="#81c9bf" x="161" y="1"></rect> <text x="179" y="10" class="legend-label"> Random Write MB/s </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="38%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="50%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="62%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="73%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="85%" y="30" height="433" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="433" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="90" class="label"> IonSwitch 4GB </text><text x="25%" y="126" class="label"> Hyper Expert 2GB </text><text x="25%" y="162" class="label"> VirMach 1GB </text><text x="25%" y="198" class="label"> VirMach 3GB </text><text x="25%" y="234" class="label"> ITLDC 2GB VDS </text><text x="25%" y="270" class="label"> SolvedByData 2GB </text><text x="25%" y="306" class="label"> SolvedByData 6GB </text><text x="25%" y="342" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="378" class="label"> BigPowerHosting 2GB </text><text x="25%" y="414" class="label"> UpCloud 1GB </text><text x="25%" y="450" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="477"> 0 </text><text class="interval-label" x="38%" y="477"> 250 </text><text class="interval-label" x="50%" y="477"> 500 </text><text class="interval-label" x="62%" y="477"> 750 </text><text class="interval-label" x="73%" y="477"> 1000 </text><text class="interval-label" x="85%" y="477"> 1250 </text><text class="interval-label" x="97%" y="477"> 1500 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="14.366100000000001%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="3.461966666666667%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="17.325793333333333%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="3.01518%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="17.171466666666664%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="41.195933333333336%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="4.405893333333333%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="3.3630333333333335%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="4.0338666666666665%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="13.991133333333334%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="25.177646666666664%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="23.969306666666668%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="1.79536%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="47.70733333333333%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="2.2762133333333336%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="49.751333333333335%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect>  <rect width="31.11588666666667%" height="12" fill="#00a1d5" x="27%" y="324" key="1"></rect><rect width="6.149686666666667%" height="12" fill="#81c9bf" x="27%" y="337" key="2"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="354" opacity="0.2"></rect>  <rect width="3.817846666666667%" height="12" fill="#00a1d5" x="27%" y="360" key="1"></rect><rect width="0.022291733333333334%" height="12" fill="#81c9bf" x="27%" y="373" key="2"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="390" opacity="0.2"></rect>  <rect width="15.268866666666666%" height="12" fill="#00a1d5" x="27%" y="396" key="1"></rect><rect width="31.612000000000002%" height="12" fill="#81c9bf" x="27%" y="409" key="2"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="426" opacity="0.2"></rect>  <rect width="11.892999999999999%" height="12" fill="#00a1d5" x="27%" y="432" key="1"></rect><rect width="34.3182%" height="12" fill="#81c9bf" x="27%" y="445" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="462" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="41.8661%" y="45.5" data-item-percent="0.20523000000000002"> 307.845 </text><text class="bar-label" x="30.96196666666667%" y="58.5" data-item-percent="0.04945666666666667"> 74.185 </text><text class="bar-label" x="44.82579333333334%" y="81.5" data-item-percent="0.24751133333333333"> 371.267 </text><text class="bar-label" x="30.51518%" y="94.5" data-item-percent="0.043074"> 64.611 </text><text class="bar-label" x="44.67146666666666%" y="117.5" data-item-percent="0.24530666666666665"> 367.96 </text><text class="bar-label" x="68.69593333333333%" y="130.5" data-item-percent="0.5885133333333333"> 882.77 </text><text class="bar-label" x="31.90589333333333%" y="153.5" data-item-percent="0.06294133333333334"> 94.412 </text><text class="bar-label" x="30.863033333333334%" y="166.5" data-item-percent="0.048043333333333334"> 72.065 </text><text class="bar-label" x="31.53386666666667%" y="189.5" data-item-percent="0.057626666666666666"> 86.44 </text><text class="bar-label" x="41.49113333333334%" y="202.5" data-item-percent="0.19987333333333335"> 299.81 </text><text class="bar-label" x="52.67764666666666%" y="225.5" data-item-percent="0.35968066666666665"> 539.521 </text><text class="bar-label" x="51.46930666666667%" y="238.5" data-item-percent="0.3424186666666667"> 513.628 </text><text class="bar-label" x="29.29536%" y="261.5" data-item-percent="0.025648"> 38.472 </text><text class="bar-label" x="75.20733333333334%" y="274.5" data-item-percent="0.6815333333333333"> 1022.3 </text><text class="bar-label" x="29.776213333333335%" y="297.5" data-item-percent="0.032517333333333336"> 48.776 </text><text class="bar-label" x="77.25133333333333%" y="310.5" data-item-percent="0.7107333333333333"> 1066.1 </text><text class="bar-label" x="58.61588666666667%" y="333.5" data-item-percent="0.44451266666666667"> 666.769 </text><text class="bar-label" x="33.64968666666667%" y="346.5" data-item-percent="0.08785266666666666"> 131.779 </text><text class="bar-label" x="31.317846666666668%" y="369.5" data-item-percent="0.054540666666666675"> 81.811 </text><text class="bar-label" x="27.522291733333333%" y="382.5" data-item-percent="0.00031845333333333333"> 0.47768 </text><text class="bar-label" x="42.76886666666667%" y="405.5" data-item-percent="0.21812666666666666"> 327.19 </text><text class="bar-label" x="59.112%" y="418.5" data-item-percent="0.4516"> 677.4 </text><text class="bar-label" x="39.393%" y="441.5" data-item-percent="0.1699"> 254.85 </text><text class="bar-label" x="61.8182%" y="454.5" data-item-percent="0.49026"> 735.39 </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Bandwidth</h4> <figure class="chart-container"> <svg viewBox="0 0 810 368" width="810" height="368" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="0" height="349" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="49.5" class="label"> IonSwitch 4GB </text><text x="25%" y="78.5" class="label"> Hyper Expert 2GB </text><text x="25%" y="107.5" class="label"> VirMach 1GB </text><text x="25%" y="136.5" class="label"> VirMach 3GB </text><text x="25%" y="165.5" class="label"> ITLDC 2GB VDS </text><text x="25%" y="194.5" class="label"> SolvedByData 2GB </text><text x="25%" y="223.5" class="label"> SolvedByData 6GB </text><text x="25%" y="252.5" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="281.5" class="label"> BigPowerHosting 2GB </text><text x="25%" y="310.5" class="label"> UpCloud 1GB </text><text x="25%" y="339.5" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="363"> 0 </text><text class="interval-label" x="41%" y="363"> 40 </text><text class="interval-label" x="55%" y="363"> 80 </text><text class="interval-label" x="69%" y="363"> 120 </text><text class="interval-label" x="83%" y="363"> 160 </text><text class="interval-label" x="97%" y="363"> 200 </text><text class="interval-label" x="111%" y="363"> 240 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="39.9%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="65.10000000000001%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="33.355%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="11.06%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="25.515000000000004%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="10.395%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="4.2%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="4.165%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect>  <rect width="52.15%" height="18" fill="#00a1d5" x="27%" y="238" key="1"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="261" opacity="0.2"></rect>  <rect width="33.355%" height="18" fill="#00a1d5" x="27%" y="267" key="1"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="290" opacity="0.2"></rect>  <rect width="31.7345%" height="18" fill="#00a1d5" x="27%" y="296" key="1"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="319" opacity="0.2"></rect>  <rect width="13.65%" height="18" fill="#00a1d5" x="27%" y="325" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="348" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="67.4%" y="18.56" data-item-percent="0.57"> 114 MB/s </text><text class="inverted-bar-label" x="91.60000000000001%" y="47.56" data-item-percent="0.93"> 186 MB/s </text><text class="bar-label" x="60.855%" y="76.56" data-item-percent="0.4765"> 95.3 MB/s </text><text class="bar-label" x="38.56%" y="105.56" data-item-percent="0.158"> 31.6 MB/s </text><text class="bar-label" x="53.015%" y="134.56" data-item-percent="0.36450000000000005"> 72.9 MB/s </text><text class="bar-label" x="37.894999999999996%" y="163.56" data-item-percent="0.1485"> 29.7 MB/s </text><text class="bar-label" x="31.7%" y="192.56" data-item-percent="0.06"> 12 MB/s </text><text class="bar-label" x="31.665%" y="221.56" data-item-percent="0.059500000000000004"> 11.9 MB/s </text><text class="bar-label" x="79.65%" y="250.56" data-item-percent="0.745"> 149 MB/s </text><text class="bar-label" x="60.855%" y="279.56" data-item-percent="0.4765"> 95.3 MB/s </text><text class="bar-label" x="59.2345%" y="308.56" data-item-percent="0.45335000000000003"> 90.67 MB/s </text><text class="bar-label" x="41.15%" y="337.56" data-item-percent="0.195"> 39 MB/s </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Measurable Value per Dollar</h2></div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Megabytes of RAM per Dollar</h4> <figure class="chart-container"> <svg viewBox="0 0 810 368" width="810" height="368" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="0" height="349" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="49.5" class="label"> IonSwitch 4GB </text><text x="25%" y="78.5" class="label"> Hyper Expert 2GB </text><text x="25%" y="107.5" class="label"> VirMach 1GB </text><text x="25%" y="136.5" class="label"> VirMach 3GB </text><text x="25%" y="165.5" class="label"> ITLDC 2GB VDS </text><text x="25%" y="194.5" class="label"> SolvedByData 2GB </text><text x="25%" y="223.5" class="label"> SolvedByData 6GB </text><text x="25%" y="252.5" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="281.5" class="label"> BigPowerHosting 2GB </text><text x="25%" y="310.5" class="label"> UpCloud 1GB </text><text x="25%" y="339.5" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="363"> 0 </text><text class="interval-label" x="41%" y="363"> 400 </text><text class="interval-label" x="55%" y="363"> 800 </text><text class="interval-label" x="69%" y="363"> 1200 </text><text class="interval-label" x="83%" y="363"> 1600 </text><text class="interval-label" x="97%" y="363"> 2000 </text><text class="interval-label" x="111%" y="363"> 2400 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="7.945%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="4.97%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="14.35%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="27.335%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="23.87%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="20.055%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="57.33%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="57.33%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect>  <rect width="40.985%" height="18" fill="#00a1d5" x="27%" y="238" key="1"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="261" opacity="0.2"></rect>  <rect width="11.200000000000001%" height="18" fill="#00a1d5" x="27%" y="267" key="1"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="290" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="296" key="1"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="319" opacity="0.2"></rect>  <rect width="8.96%" height="18" fill="#00a1d5" x="27%" y="325" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="348" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="35.445%" y="18.56" data-item-percent="0.1135"> 227 MB </text><text class="bar-label" x="32.47%" y="47.56" data-item-percent="0.071"> 142 MB </text><text class="bar-label" x="41.85%" y="76.56" data-item-percent="0.205"> 410 MB </text><text class="bar-label" x="54.835%" y="105.56" data-item-percent="0.3905"> 781 MB </text><text class="bar-label" x="51.370000000000005%" y="134.56" data-item-percent="0.341"> 682 MB </text><text class="bar-label" x="47.555%" y="163.56" data-item-percent="0.2865"> 573 MB </text><text class="inverted-bar-label" x="83.83%" y="192.56" data-item-percent="0.819"> 1638 MB </text><text class="inverted-bar-label" x="83.83%" y="221.56" data-item-percent="0.819"> 1638 MB </text><text class="bar-label" x="68.485%" y="250.56" data-item-percent="0.5855"> 1171 MB </text><text class="bar-label" x="38.7%" y="279.56" data-item-percent="0.16"> 320 MB </text><text class="bar-label" x="34.64%" y="308.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="36.46%" y="337.56" data-item-percent="0.128"> 256 MB </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Geekbench Points per Dollar</h4> <figure class="chart-container"> <svg viewBox="0 0 810 368" width="810" height="368" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="349" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="0" height="349" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> IonSwitch 2GB NVMe </text><text x="25%" y="49.5" class="label"> IonSwitch 4GB </text><text x="25%" y="78.5" class="label"> Hyper Expert 2GB </text><text x="25%" y="107.5" class="label"> VirMach 1GB </text><text x="25%" y="136.5" class="label"> VirMach 3GB </text><text x="25%" y="165.5" class="label"> ITLDC 2GB VDS </text><text x="25%" y="194.5" class="label"> SolvedByData 2GB </text><text x="25%" y="223.5" class="label"> SolvedByData 6GB </text><text x="25%" y="252.5" class="label"> SSD ﻿Nodes ﻿”10x” XL </text><text x="25%" y="281.5" class="label"> BigPowerHosting 2GB </text><text x="25%" y="310.5" class="label"> UpCloud 1GB </text><text x="25%" y="339.5" class="label"> Data Packet 1GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="363"> 0 </text><text class="interval-label" x="41%" y="363"> 1000 </text><text class="interval-label" x="55%" y="363"> 2000 </text><text class="interval-label" x="69%" y="363"> 3000 </text><text class="interval-label" x="83%" y="363"> 4000 </text><text class="interval-label" x="97%" y="363"> 5000 </text><text class="interval-label" x="111%" y="363"> 6000 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="7.616%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="11.284%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="13.873999999999999%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="16.198%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="9.520000000000001%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="24.444%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="69.65%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="42.364%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect>  <rect width="11.214%" height="18" fill="#00a1d5" x="27%" y="238" key="1"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="261" opacity="0.2"></rect>  <rect width="19.614%" height="18" fill="#00a1d5" x="27%" y="267" key="1"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="290" opacity="0.2"></rect>  <rect width="11.27%" height="18" fill="#00a1d5" x="27%" y="296" key="1"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="319" opacity="0.2"></rect>  <rect width="65.996%" height="18" fill="#00a1d5" x="27%" y="325" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="348" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="35.116%" y="18.56" data-item-percent="0.1088"> 544 </text><text class="bar-label" x="38.784%" y="47.56" data-item-percent="0.1612"> 806 </text><text class="bar-label" x="41.373999999999995%" y="76.56" data-item-percent="0.1982"> 991 </text><text class="bar-label" x="43.698%" y="105.56" data-item-percent="0.2314"> 1157 </text><text class="bar-label" x="37.02%" y="134.56" data-item-percent="0.136"> 680 </text><text class="bar-label" x="51.944%" y="163.56" data-item-percent="0.3492"> 1746 </text><text class="inverted-bar-label" x="96.15%" y="192.56" data-item-percent="0.995"> 4975 </text><text class="bar-label" x="69.864%" y="221.56" data-item-percent="0.6052"> 3026 </text><text class="bar-label" x="38.714%" y="250.56" data-item-percent="0.1602"> 801 </text><text class="bar-label" x="47.114000000000004%" y="279.56" data-item-percent="0.2802"> 1401 </text><text class="bar-label" x="38.769999999999996%" y="308.56" data-item-percent="0.161"> 805 </text><text class="inverted-bar-label" x="92.496%" y="337.56" data-item-percent="0.9428"> 4714 </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Closing Thoughts</h2><p>Nothing profound, just some random notes:</p><ul><li>My crazy cheap Black Friday <a href="https://billing.virmach.com/aff.php?aff=8600">VirMach</a> VPS has been surprisingly stable.</li><li><a href="https://itldc.com/?from=30507">ITLDC</a> offered a stable, well-performing server with a control panel that was strange and uncomfortable.</li><li><a href="https://upcloud.com/signup/?promo=KYQ5A6">UpCloud</a> really did outperform <a href="https://m.do.co/c/f1391f41e5e8">Digital Ocean</a> on an equivalent VPS, but marginally so compared to other not-big-name providers. (<a href="https://clientarea.ramnode.com/aff.php?aff=2496">RamNode</a> being one I’d gladly rely on.)</li><li><a href="https://clients.solvedbydata.com/aff.php?aff=1441">SolvedByData</a> offered really strong CPU, but the outages, 100Mbps uplink and support vibe would keep me from seriously relying on them as a provider. My experience with BigPowerHosting was similar—and apparently they’re gone from the internet now.</li><li><a href="https://datapacket.net/members/aff.php?aff=51">Data Packet</a> is the latest host I’ve tried and I’m convinced there’s something downright scandalous going on there, I just haven’t figured out what it is yet. That’s just too many cores for the price.</li></ul><p>Thanks for reading, and consider leaving a comment or sending me a message somewhere if you have questions, suggestions or objections!</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Non-Affiliate Links</h2><p>No trickery:</p><ul><li><a href="https://www.digitalocean.com/">Digital Ocean</a></li><li><a href="https://www.hyperexpert.com/">Hyper Expert</a></li><li><a href="https://impactvps.com/">Impact VPS</a></li><li><a href="https://ionswitch.com/">IonSwitch</a></li><li><a href="https://ramnode.com/">RamNode</a></li><li><a href="https://www.ssdnodes.com/">SSD Nodes</a></li><li><a href="https://virmach.com/">VirMach</a></li><li><a href="https://datapacket.net/">Data Packet</a></li><li><a href="https://itldc.com/en/">ITLDC</a></li><li><a href="https://www.solvedbydata.com/">SolvedByData</a></li></ul></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Firefox 4 Windows: Unwanted Red Box-Shadow]]></title>
            <link>https://workingconcept.com/blog/firefox-4-windows-unwanted-red-box-shadow</link>
            <guid>https://workingconcept.com/blog/firefox-4-windows-unwanted-red-box-shadow</guid>
            <pubDate>Wed, 25 May 2011 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’m not sure why, but in Windows (only!) Firefox 4.0.1 seems to think it’s cool to put a red box-shadow on any input field of the "email" type. Just leaving a note here in case it’s helpful – it’s easy enough to fix:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#FFCB6B">input</span><span style="color:#89DDFF">[</span><span style="color:#C792EA">type</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">email</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">]</span><span style="color:#89DDFF"> {</span><span style="color:#EEFFFF"> </span></span>
<span class="line"><span style="color:#B2CCD6">    box-shadow</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> none</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre>  </figure> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sublime Text 2 Orientation]]></title>
            <link>https://workingconcept.com/blog/sublime-text-2-orientation</link>
            <guid>https://workingconcept.com/blog/sublime-text-2-orientation</guid>
            <pubDate>Fri, 23 Nov 2012 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I started using <a href="http://www.sublimetext.com/2" target="_blank">Sublime Text 2</a> in March after getting excited about its speed, easy package control, and good reputation. I spent some time this week going through the entire <a href="https://tutsplus.com/course/improve-workflow-in-sublime-text-2/" target="_blank">Tuts+ “Perfect Workflow in Sublime Text 2”</a> course by Jeffrey Way and realized how much I’ve been missing. I would strongly recommend this free course whether you’re interested in Sublime Text 2 or are using it with the nagging feeling that you should level up.</p>
<p>I’ll sum up my favorites (old and new), but it’s seriously worth your time having Jeffrey <em>show</em> you each in more detail.</p>
<h2>It’s all text</h2>
<p>The preferences are stored and edited in JSON files. The packages are uncompressed folders that simply live neatly in Sublime’s Application Support folder. (Both of these things, by the way, make it very handy to sync everything with Dropbox.) The project-wide search returns results as a flat text file in a new tab. Sublime is all about text and the keyboard, with very little UI that requires or uses anything else. You can rely on the mouse less as you get more comfortable, without the frustration or slowdown of jumping right into the terminal for text editing.</p>
<h2>Ditch the sidebar forever with <kbd>⌘</kbd>+<kbd>P</kbd></h2>
<p>Never hunt through files and folders again, just type the name of what you’re looking for whether it’s a path, a method, or … well anything. Tack on an @ symbol to a filename and all methods will be listed so you can jump in exactly where you’d like.</p>
<h2>Do (almost) anything with the Command Palette <kbd>⌘</kbd>+<kbd>⇧</kbd>+<kbd>P</kbd></h2>
<p>Anything Sublime can do will be accessible here so you can stay off the mouse even if you don’t have all the shortcuts down. Change syntax on the current file, install or remove a package, access snippets or build options – really just anything. It’s <a href="http://www.alfredapp.com" target="_blank">Alfred</a>, but in a text editor.</p>
<h2>Multiple Cursors & Incremental Search</h2>
<p>I shouldn’t admit to you, dear reader, that I was happily working away in Sublime Text 2 completely oblivious to its multiple cursors. Sure, you can <kbd>⌘</kbd>+click several things manually and get a new cursor for each one. But that’s little beans.</p>
<p>You’ve already noticed that every time you select a word, every instance of that exact same thing is highlighted. Hit <kbd>⌘</kbd>+<kbd>D</kbd> and the next occurrence of that word will be selected with its own cursor. Repeat until you’ve got enough of them selected, then just type to change them all at once.</p>
<p>But it gets better: say you want to select <em>every</em> occurrence of that word instead of hitting <kbd>⌘</kbd>+<kbd>D</kbd> repeatedly. Let’s do that. Start by placing your cursor inside any given word, then hit <kbd>⌘</kbd>+<kbd>D</kbd> once to select the word. Now hit <kbd>Control</kbd>+<kbd>⌘</kbd>+<kbd>G</kbd> and every occurrence will be selected with its own cursor. Simple and extremely useful.</p>
<h2>Snippets</h2>
<p>We all know that snippets are essential time-savers, and you probably already know that Tools → New Snippet will give you a (simple) template to edit and save as a .sublime-snippet file in your User folder. Jeffrey gets into setting up GitHub Gist integration and a few other helpful pointers.</p>
<h2>Package: Package Control</h2>
<p>Install <a href="http://wbond.net/sublime_packages/package_control">Will Bond’s Package Manager</a> first, and install it now if you haven’t already. Most Sublime packages are current and available through this channel, making it an absolute must if you want package discovery/installation/update/removal to take only a few keystrokes and a few seconds.</p>
<h2>Package: Zen Coding</h2>
<p>Use a simple, logical shorthand to quickly generate HTML structure. It’s more compelling to see this in action, but the idea is that you can write something like <code>div#nav&gt;ul&gt;li*5&gt;a</code> to hit tab and get…</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">div</span><span style="color:#C792EA"> id</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">nav</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">  &#x3C;</span><span style="color:#F07178">ul</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;</span><span style="color:#F07178">li</span><span style="color:#89DDFF">>&#x3C;</span><span style="color:#F07178">a</span><span style="color:#C792EA"> href</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">""</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">a</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">li</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;</span><span style="color:#F07178">li</span><span style="color:#89DDFF">>&#x3C;</span><span style="color:#F07178">a</span><span style="color:#C792EA"> href</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">""</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">a</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">li</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;</span><span style="color:#F07178">li</span><span style="color:#89DDFF">>&#x3C;</span><span style="color:#F07178">a</span><span style="color:#C792EA"> href</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">""</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">a</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">li</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;</span><span style="color:#F07178">li</span><span style="color:#89DDFF">>&#x3C;</span><span style="color:#F07178">a</span><span style="color:#C792EA"> href</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">""</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">a</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">li</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;</span><span style="color:#F07178">li</span><span style="color:#89DDFF">>&#x3C;</span><span style="color:#F07178">a</span><span style="color:#C792EA"> href</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">""</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">a</span><span style="color:#89DDFF">>&#x3C;/</span><span style="color:#F07178">li</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">  &#x3C;/</span><span style="color:#F07178">ul</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">div</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Obviously you can do quite more than this, and it applies to CSS authoring as well. </p>
<h2>Package: Fetch Files and Packages</h2>
<p>Commissioned by Tuts+, this package lets you define preset file and package shortcuts so you can get a project running quickly. Simply put, it takes a predefined URL and downloads it (extracting, if necessary) to whatever directory you specify from within Sublime. I added shortcuts for jQuery, variations of the Twitter Bootstrap, the HTML5 Boilerplate, and CodeIgniter.</p>
<h2>Package: Fast Folder & File Creation</h2>
<p>I’m not sure why this wasn’t built into Sublime in the first place, but the AdvancedNewFile package lets you use <kbd>⌘</kbd>+<kbd>⌥</kbd>+<kbd>N</kbd> to type a folder/file path and hit return to create it. This saves having to use the standard OS GUI to save a newly-created file.</p>
<h2>Package: Sidebar Enhancements</h2>
<p>I started using this a while ago, but in light of my newer discoveries it’ll certainly become more trivial. Anyway, use Sidebar Enhancements to get more right-click options in your sidebar.</p>
<h2>Package: Sublime Linter</h2>
<p>A great package that’ll check your syntax as you write, or as you save depending on how you set it up. You may have to do a little bit more work setting up linters depending on what languages you use, but the workflow improvement is surely going to be worth it. Unless you never make typos or mistakes. In that case, please contact me because I would like to talk about hiring you for everything I ever do.</p>
<h2>Package: Sublime + Marked</h2>
<p>I tend to use <a href="http://bywordapp.com" target="_blank">Byword</a> for writing posts like this in Markdown, but this part of Jeffrey’s course led me to better appreciate the build system. To me, “build” always meant compile source code into a binary executable of some kind, be it a Cocoa app or a Flash/Air program. Here, Jeffrey demonstrates how you can author your Markdown and then set up a custom build, effectively launching <a href="http://markedapp.com" target="_blank">Marked</a> (an inexpensive Markdown viewer) with <kbd>⌘</kbd>+<kbd>B</kbd> to view your work in HTML.</p>
<h2>Bonus Package: Less Compiler</h2>
<p>After seeing Jeffrey’s Markdown+Marked screencast, it dawned on me that I should be compiling Less from Sublime. (Wipe that look off your face, it just never occurred to me okay?) Sure enough, there was a package for that: <a href="https://github.com/berfarah/LESS-build-sublime" target="_blank">LESS-build-sublime</a>. Do some work, <kbd>⌘</kbd>+<kbd>B</kbd>, and compile that .less into .css. Easy.</p>
<p>Much of this may be review, or overly simplistic for those wizards who have mastered Sublime Text 2. This is one of those posts I’ll probably be referring back to myself, and hopefully you’ve gotten something out of it as well!</p>
<p>Is there something you’ve stumbled on that you can’t live without? Let me know!</p>
<p><strong>Update:</strong> I forgot to mention in my original post that it was <a href="http://www.mutuallyhuman.com/blog/2012/10/18/configuring-sublime-text-2/" target="_blank">Ross Hunter’s post</a> that kicked off my latest quest to improve my Sublime workflow, and it’s probably to his credit that I ended up spending a few hours with the Tuts+ series. Thanks Ross!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Synology Gmail Backup]]></title>
            <link>https://workingconcept.com/blog/synology-gmail-backup</link>
            <guid>https://workingconcept.com/blog/synology-gmail-backup</guid>
            <pubDate>Tue, 01 Apr 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><em>January 2015 update: I reinstalled GMVault again, and David’s comment below was worth detailing in a new post. <a href="{entry:46@1:url||https://workingconcept.com/blog/synology-gmail-backup-again}">Read the updated post</a>.</em></p>
<p><em>Update: I kept getting IMAP abort errors to the tune of <code>Gmvault ssl socket error: EOF. Connection lost, reconnect.</code>, and a <a href="https://github.com/gaubert/gmvault/issues/154">GitHub thread</a> suggested I turn off IMAP compression in gmvault_defaults.conf. That fixed the problem for me, and now I’ve got completely clean, error-free backups.</em></p>
<p>A <a href="http://amzn.com/B00FY6DV3S">Synology DS214</a> recently made its way into our home, and I’ve been busy devising ways to grow our new private cloud. After pulling and crimping some CAT6, installing some <a href="http://amzn.com/B00EHBERSE">nice drives</a>, and a rather effortless setup thanks to DSM 5.0 (Synology’s own surprisingly nice software), I’ve got a headless server with plenty of RAID-protected disk space and dirt cheap, off-site Glacier backups. It does a bunch of cool things out of the box, and I decided my first challenge would be to automate Google Apps email backups.</p>
<h2>Background</h2>
<p>I have separate Google Apps accounts for personal and business use, and for business I’ve been using <a href="https://www.backupify.com/">Backupify</a> for almost a year and a half. For $3/month, Backupify required zero setup and faithfully kept all my Google Apps data backed up and available to restore at a moment’s notice. There’s nothing I’d change about the service, I just did the math and decided I’d take matters into my own hands. For “fun.” And with all the savings, I can afford an extra two-thirds of a cup of coffee every month!</p>
<p>Now here’s the best part: I managed to do this without installing ipkg. That’s a well-known and well-documented bootstrap for Synology devices that gives you access to a whole world of updated, third-party Linux packages. I’m still attempting to avoid it, only because I’ve read that firmware updates can require re-integrating ipkg, and I’m wary of jumping in after earlier regrets over iPhone jailbreaking. In other words, I’m a baby.</p>
<p>But avoid I can, because DSM 5.0 comes with cron, shell access, and one-click installers for Python 2.7 and Python 3. This means that all I have to do is get <a href="http://gmvault.org/">GMVault</a> running and move on to the next challenge!</p>
<p><a href="http://jimmybonney.com/articles/install_gmvault_on_a_synology_nas_follow_up/">Jimmy Bonney’s post</a> got me most of the way there, and a <a href="http://jimmybonney.com/articles/install_gmvault_on_a_synology_nas_follow_up/#comment-750956234">comment from David Cumps</a> and some old-fashioned trial and error saw me through the rest of the way.</p>
<h2>The Setup</h2>
<p>I have shared folders for work and personal use, and my setup in each is identical. There could be a better way to do this, but so far all’s working well. For each shared folder, I have…</p>
<ul><li><strong>mail-backup/</strong>: a directory that GMVault can use for its repository, which is a series of dated year-month folders containing two files per message: JSON meta data and a compressed package with the message body and attachments</li><li><strong>utilities/</strong>: the directory I used to install GMVault and where I’ve placed backup scripts for cron</li><li><strong>utilities/log/mail-backup/</strong>: a directory that gets dated plain text logs every time the backup scripts run</li></ul>
<p>I’ve cobbled together two shell scripts, one that runs daily and syncs in quick mode, and one that runs monthly to ensure a full sync.</p>
<p><strong>email-backup-full.sh</strong>: Mark and timestamp the beginning and end of the sync process, print environment variables at runtime (helpful for debugging permissions issues if you don’t run as root), and log gmvault’s output while it attempts to sync.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#546E7A;font-style:italic">#!/bin/sh</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">NOW</span><span style="color:#89DDFF">=$(</span><span style="color:#FFCB6B">date</span><span style="color:#C3E88D"> +</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">%Y-%m-%d</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">)</span></span>
<span class="line"><span style="color:#EEFFFF">LOGFILE</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">/volume1/yourfolder/utilities/log/mail-backup/log-</span><span style="color:#EEFFFF">$NOW</span><span style="color:#C3E88D">.log</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#EEFFFF">CURTIME</span><span style="color:#89DDFF">=$(</span><span style="color:#FFCB6B">date</span><span style="color:#C3E88D"> +</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">%r</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#EEFFFF">$CURTIME</span><span style="color:#C3E88D">: Starting email sync...</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">printf</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">\n</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"></span>
<span class="line"><span style="color:#82AAFF">set</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">sh</span><span style="color:#C3E88D"> /volume1/yourfolder/utilities/gmvault_env/bin/gmvault</span><span style="color:#C3E88D"> sync</span><span style="color:#C3E88D"> --emails-only</span><span style="color:#C3E88D"> -t</span><span style="color:#C3E88D"> full</span><span style="color:#C3E88D"> --db-dir</span><span style="color:#C3E88D"> /volume1/yourfolder/mail-backup/</span><span style="color:#C3E88D"> you@gmail.com</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">CURTIME</span><span style="color:#89DDFF">=$(</span><span style="color:#FFCB6B">date</span><span style="color:#C3E88D"> +</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">%r</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#82AAFF">printf</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">\n</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#EEFFFF">$CURTIME</span><span style="color:#C3E88D">: email sync finished.</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>email-backup-quick.sh</strong>: Ditto the first script, just put gmvault in quick mode instead of full.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#546E7A;font-style:italic">#!/bin/sh</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">NOW</span><span style="color:#89DDFF">=$(</span><span style="color:#FFCB6B">date</span><span style="color:#C3E88D"> +</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">%Y-%m-%d</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">)</span></span>
<span class="line"><span style="color:#EEFFFF">LOGFILE</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">/volume1/yourfolder/utilities/log/mail-backup/log-</span><span style="color:#EEFFFF">$NOW</span><span style="color:#C3E88D">.log</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#EEFFFF">CURTIME</span><span style="color:#89DDFF">=$(</span><span style="color:#FFCB6B">date</span><span style="color:#C3E88D"> +</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">%r</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#EEFFFF">$CURTIME</span><span style="color:#C3E88D">: Starting email sync...</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">printf</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">\n</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"></span>
<span class="line"><span style="color:#82AAFF">set</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">sh</span><span style="color:#C3E88D"> /volume1/yourfolder/utilities/gmvault_env/bin/gmvault</span><span style="color:#C3E88D"> sync</span><span style="color:#C3E88D"> --emails-only</span><span style="color:#C3E88D"> -t</span><span style="color:#C3E88D"> quick</span><span style="color:#C3E88D"> --db-dir</span><span style="color:#C3E88D"> /volume1/yourfolder/mail-backup/</span><span style="color:#C3E88D"> you@gmail.com</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">CURTIME</span><span style="color:#89DDFF">=$(</span><span style="color:#FFCB6B">date</span><span style="color:#C3E88D"> +</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">%r</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#82AAFF">printf</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">\n</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#EEFFFF">$CURTIME</span><span style="color:#C3E88D">: email sync finished.</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span>
<span class="line"><span style="color:#82AAFF">echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">------------------------------------</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> >></span><span style="color:#EEFFFF"> $LOGFILE</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Process</h2>
<ol><li>Install Python 2.7 (labeled “Python”) from the DSM Package Center.</li><li>Create directories in a shared folder for your backup repository, logs, and a place to set up GMVault. (Mine are <code>mail-backup</code>, <code>utilities/log/mail-backup</code>, and <code>utilities</code> respectively.)</li><li>SSH as root into your utilities directory, and <code>curl -O https://raw.githubusercontent.com/pypa/virtualenv/master/virtualenv.py -k</code> to install a virtualenv<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>.</li><li><code>python virtualenv.py gmvault_env</code> to install GMVault in the virtualenv from the previous step.<sup id="fnref1:2"><a href="{fn:2:url||}" class="footnote-ref">2</a></sup></li><li><code>sh /volume1/yourfolder/utilities/gmvault_env/bin/gmvault</code> should now run GMVault and give you an error for having too few arguments.</li><li>Place your full and quick backup scripts somewhere—I put mine right in the <code>utilities</code> folder. Remember the full path to each script for step 8.</li><li>Authenticate with Google Apps once, running some variant of <code>sh /volume1/yourfolder/utilities/gmvault_env/bin/gmvault sync --emails-only -t quick --db-dir /volume1/yourfolder/mail-backup/</code>. Read, hit enter, paste the unique URL into your browser, allow access from the browser, and you can end the gmvault instance prematurely once the authentication’s done. Auth tokens will be stored in <code>/root/.gmvault</code>, along with gmvault defaults that you can edit.</li><li>Schedule these scripts to run automatically with Task Scheduler, which you’ll find in the Control Panel. Create a new User-defined script and give it a name, run as root, and in the User-defined script textarea add: <code>sh /volume1/yourfolder/utilities/backup-email-full.sh</code>. Schedule it to run when you’d like (or temporarily at short intervals to test), then repeat with the <code>backup-email-quick.sh</code> script.</li></ol>
<p>That’s it! I’ll probably try and figure out how to detect failures and fire off an email, but at the moment I’ve got working daily backups of two Google Apps email accounts. The initial sync for each ~4GB of mail took 3-4 hours, and subsequent quick syncs only take a few minutes. As a bonus, when you allow GMVault to compress messages the total size of the backup will be a fraction of what Google reports for your disk usage.</p>
<p>Check out <a href="http://gmvault.org/in_depth.html#backup">GMVault’s options</a> for more ideas, and let me know if you improve my humble first attempts to get this working! As usual, my Python and shell scripting is a bit on the infantile side.</p>
<div class="footnotes"><hr><ol><li id="fn:1"><p>As of January 2015, I had to specifically use <code>wget https://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.10.1.tar.gz</code> and <code>tar xzf virtualenv-1.10.1.tar.gz</code> to get an older version of virtualenv that worked. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p></li><li id="fn:2"><p>I ended up on a side quest here, running <code>easy_install pip</code>, <code>pip install virtualenv</code>, <code>virtualenv --no-site-packages gmvault-1.7-beta</code>, and finally <code>easy_install gmvault</code>, but I’m not sure that having pip or easy_installing gmvault contributed anything aside from confusion. Trying to run any <code>gmvault</code> without prepending <code>sh</code> would (and still does) result in an error: “env: bash: No such file or directory”. <a href="{fnref1:2:url||}" rev="footnote" class="footnote-backref">↩</a></p></li></ol></div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[DDEV and Craft Plugin Development]]></title>
            <link>https://workingconcept.com/blog/ddev-craft-plugin-development</link>
            <guid>https://workingconcept.com/blog/ddev-craft-plugin-development</guid>
            <pubDate>Thu, 17 Oct 2019 15:58:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This is an update to <a href="{entry:421@1:url||https://workingconcept.com/blog/mamp-pro-to-ddev}">an earlier post about working with DDEV</a>.</p><p>One limitation was that symbolic links didn’t work with local, out-of-project file references—like when you’d have composer use a local Craft CMS plugin that's not part of the project structure:<br></p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>~/projects/my-current-project &#x3C;-- where I'm working</span></span>
<span class="line"><span>~/projects/foo-craft-plugin &#x3C;-- local plugin to be symlinked</span></span>
<span class="line"><span>~/projects/my-current-project/plugin &#x3C;-- symlink works on Mac, not DDEV container</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I solved this the hard way by running a script that used fswatch to detect changes to <code>foo-craft-plugin</code> and rsync them into my local project tree (<code>my-current-project/plugin)</code>.</p><p>The easy way, it turns out, is to just mount that plugin directory into the container by creating <code>.ddev/docker-compose.mounts.yaml</code> and mapping the local path to one in the container:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#F07178">version</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">3.6</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#F07178">services</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">  web</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">    volumes</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">$HOME/projects/foo-craft-plugin:/var/www/html/plugin</span><span style="color:#89DDFF">"</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>After a quick <code>ddev restart</code> the volume will be mounted inside the container.</p><p>It's important to remember is that the local (Mac) filesystem doesn't know about this arrangement, so <strong>composer updates need to be initiated from inside the container.</strong></p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">➜</span><span style="color:#C3E88D"> ddev</span><span style="color:#C3E88D"> ssh</span><span style="color:#EEFFFF">    </span></span>
<span class="line"><span style="color:#FFCB6B">me@project:/var/www/html$</span><span style="color:#C3E88D"> composer</span><span style="color:#C3E88D"> update</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This <a href="https://stackoverflow.com/a/57432155/897279">may not be all that new</a>, but I’m glad I finally stumbled onto it!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Into the Woods with Statamic]]></title>
            <link>https://workingconcept.com/blog/into-the-woods-with-statamic</link>
            <guid>https://workingconcept.com/blog/into-the-woods-with-statamic</guid>
            <pubDate>Fri, 12 Oct 2012 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/into-the-woods@2x.C2UBoRJG_Z1f8Tln.webp" type="image/webp"><source src="https://workingconcept.com//_astro/into-the-woods@2x.C2UBoRJG_Z1dPOtj.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/into-the-woods@2x.C2UBoRJG_1YIXB6.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/into-the-woods@2x.C2UBoRJG_242xAR.jpg" decoding="async" loading="lazy" alt width="1480" height="660"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>A post from <a href="https://web.archive.org/web/20170304132913/https://alpha.app.net/typaulhus">@typaulhus</a> on ADN was the first I heard of this “<a href="https://statamic.com">Statamic</a>,” and after a brief period of skepticism I’m head-over-heals for it. Statamic has officially replaced ExpressionEngine as CMS of choice for this humble little site. Why is it so exciting?</p>
<ul>
<li>No database. Fast, easy to deploy, and I can keep <em>everything</em> in Git without doing anything clever.</li>
<li>Content is stored in Markdown files. This is a huge plus because I write just about everything longer than a paragraph in Markdown and then I have to figure out how to get it into that other thing. I can skip that step.</li>
<li>Very clever design. It’s very clear that Jack and Mubashar wanted to get away from EE’s bloat while taking advantage of its templating convenience and … well … Structure. It feels perfectly focused as a CMS for small sites and blogs, while being flexible enough to work for web creators that have ideas beyond a WordPress theme.</li>
<li>Gorgeous admin panel. Blazing fast and perfect on an iPad? Yes and yes. If you think that’s shallow, log in to EE’s admin panel on your iPad. I’ll wait.</li>
</ul>
<p>The migration from EE went smoothly and happened more or less in one evening. Here’s how I did it…</p>
<h2>Step 1: move comments to Disqus.</h2>
<p>I was using native EE comments, and the easiest path here was to create a Disqus account for the site and import my previous comments. Ryan Battles posted <a href="https://web.archive.org/web/20150209064747/http://ee-spotlight.com/tutorials/importing_expressionengine_comments_into_disqus">a template tutorial for exporting</a> a <a href="http://help.disqus.com/customer/portal/articles/472150">WXR file for Disqus</a>. It’s as easy as creating two templates with just a little bit of required consideration.</p>
<p>I foolishly waited quite a while and even opened a support ticket before finding <a href="http://import.disqus.com/">import.disqus.com</a>, which barfed on a syntax error in my XML. I waited another 24 hours and got an import error with no message, and then frustratedly hacked my way through two ExpressionEngine addons and two more Disqus test accounts before I got everything straightened out. For the record, I’d recommend going with <a href="http://devot-ee.com/add-ons/cx-disqus-comments">CX Disqus Comments</a> if you also run into problems.</p>
<h2>Step 2: get EE posts into Markdown files.</h2>
<p>Once again, somebody else did most of the work for me. This time it was Derek Jones with the <a href="https://github.com/EllisLab/download-content">Download Content</a> plugin for ExpressionEngine. I created a template that listed all my posts, each with a link that would download that post in a Markdown format I specified<sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>. I just command-clicked on each item (silly and effective) and ended up with a folder of posts ready for Statamic.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>{if member_group == 1}</span></span>
<span class="line"><span>    {exp:channel:entries </span></span>
<span class="line"><span>        channel="blog" </span></span>
<span class="line"><span>        limit="999"</span></span>
<span class="line"><span>    }</span></span>
<span class="line"><span>    &#x3C;a href="https://workingconcept.com//post-downloader/{entry_id}">{title}&#x3C;/a></span></span>
<span class="line"><span>    &#x3C;br/></span></span>
<span class="line"><span>    {/exp:channel:entries}</span></span>
<span class="line"><span>{/if}</span></span></code></pre> <figcaption class="-my-4"> <p>post-list (template)</p>
 </figcaption> </figure><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>{if member_group == 1}</span></span>
<span class="line"><span>{exp:channel:entries </span></span>
<span class="line"><span>    channel="blog" </span></span>
<span class="line"><span>    limit="1" </span></span>
<span class="line"><span>    entry_id="{segment_2}"</span></span>
<span class="line"><span>}</span></span>
<span class="line"><span>{exp:download_content filename="{entry_date format='%Y-%m-%d'}-{url_title}.md" parse="inward"}</span></span>
<span class="line"><span>---</span></span>
<span class="line"><span>title: "{title}"</span></span>
<span class="line"><span>featured: no</span></span>
<span class="line"><span>---</span></span>
<span class="line"><span></span></span>
<span class="line"><span>{blog-summary}</span></span>
<span class="line"><span>{blog-body}</span></span>
<span class="line"><span></span></span>
<span class="line"><span>{/exp:download_content}</span></span>
<span class="line"><span>{/exp:channel:entries}</span></span>
<span class="line"><span>{/if}</span></span></code></pre> <figcaption class="-my-4"> <p>post-downloader (template)</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Step 3: make that new theme.</h2>
<p>I copied "london-wild" and went to town with it. As you can guess from my site, half my work was just deleting things I didn’t use. Coming from EE, the theming process felt very familiar. Imagine ExpressionEngine with native Structure bits, but <a href="http://mustache.github.com/">mustachey</a>-looking.</p>
<p>The only sacrifices I had to make were dropping an email contact form (for now) and doing without numbered pagination.</p>
<h2>Step 4: consider the URLs.</h2>
<p>My blog posts went from /blog/entry/title to /blog/title, so I added a line to .htaccess for redirects:</p>
<pre>Redirect 301 /blog/entry/ /blog/
</pre>
<p>Since I had only six pages of blog posts, I added quick-and-dirty redirects for EE’s pagination links too: </p>
<pre>Redirect 301 /blog/P6 /blog?page=2
Redirect 301 /blog/P12 /blog?page=3
Redirect 301 /blog/P18 /blog?page=4
Redirect 301 /blog/P24 /blog?page=5
Redirect 301 /blog/P30 /blog?page=6
</pre>
<h2>Step 5: launch.</h2>
<p>The easiest part. Moved all the EE stuff into a doomed folder, dropped all my files right into the doc root via Git. And then I … here’s the thing … and then I was done. I hope you have a rocking time with Statamic too!</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>Note that my blog posts were previously split into two fields, <code>blog-summary</code> and <code>blog-body</code>. Yours will almost certainly be different. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Statamic killwhite]]></title>
            <link>https://workingconcept.com/blog/statamic-killwhite</link>
            <guid>https://workingconcept.com/blog/statamic-killwhite</guid>
            <pubDate>Sat, 17 May 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>My Statamic-generated page titles have been a mess because of extra whitespace and line breaks, which I eventually decided was unacceptable. I took inspiration from a Twig extension for <a href="https://github.com/mattstein/statamic-killwhite">killwhite</a>, which removes excessive spaces and trims out tabs and returns.</p>
<p>So this...</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#EEFFFF">	{{ if title != 'Home' AND title != '' }}{{ title }} | {{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ if taxonomy_name }}{{ taxonomy_name|title }} | {{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ if get:page }}Page {{ get:page }} | {{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ _site_name }}{{ if title == 'Home' }}: {{ _site_tagline }}{{ endif }}</span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>...which might have generated...</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#EEFFFF">	Some Post |     </span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">	Working Concept</span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>...just gets wrapped with a <code>{{ killwhite }}</code> tag...</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">{{ killwhite }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ if title != 'Home' AND title != '' }}{{ title }} | {{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ if taxonomy_name }}{{ taxonomy_name|title }} | {{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ if get:page }}Page {{ get:page }} | {{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">	{{ _site_name }}{{ if title == 'Home' }}: {{ _site_tagline }}{{ endif }}</span></span>
<span class="line"><span style="color:#EEFFFF">{{ /killwhite }}</span><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>...and the result is beauty we can all appreciate:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">Some Post | Working Concept</span><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">title</span><span style="color:#89DDFF">></span></span></code></pre>  </figure> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Working with Storybook Components in Dark Mode]]></title>
            <link>https://workingconcept.com/blog/storybook-components-dark-mode</link>
            <guid>https://workingconcept.com/blog/storybook-components-dark-mode</guid>
            <pubDate>Sun, 29 Dec 2019 20:30:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/storybook-dark-mode.Drz-Pz0a_1qX9oD.svg" type="image/svg+xml"><source src="https://workingconcept.com//_astro/storybook-dark-mode.Drz-Pz0a_1qX9oD.svg" type="image/svg+xml"><source src="https://workingconcept.com//_astro/storybook-dark-mode.Drz-Pz0a_1qX9oD.svg" type="image/svg+xml">  <img src="https://workingconcept.com//_astro/storybook-dark-mode.Drz-Pz0a_1qX9oD.svg" decoding="async" loading="lazy" alt width="800" height="420"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I got Storybook working again for my Gatsby JSX components and found a nice, simple way of switching between dark and normal styles.</p><h2>Background</h2><p><a href="https://storybook.js.org/">Storybook</a> is a lovely tool for developing and cataloging UI components. You can add it to your codebase, write brief “stories” for components to document and mock data to feed them, and spin up a nice browser UI. You can also generate static output to host wherever you’d like.</p><p>I’ve added Storybook to projects that use Twig, Vue, or React. Two reasons:</p><ol><li><strong>It’s a nice way to work.</strong><br>I can use Storybook like a workbench and focus on one component at a time without concern for the CMS. Hot module reloading (HMR) keeps things fast and fluid. Storybook’s tools make it quick to mess with inputs, modify the viewport, and run accessibility checks. (Among <a href="https://storybook.js.org/addons/">other things</a>.)</li><li><strong>It’s a useful way to review work in progress.</strong><br>Storybook URLs can jump straight to specific components and variations, facilitating discussion about new UI outside the context of the production site. Stories accumulate into an always-current reference, which is particularly useful planning with designers and content authors.</li></ol><p>The dark mode bandwagon is a bumpier ride.</p><p>If you’ve added dark mode CSS to a project, you might have found it clunky checking your work toggling system or browser settings. Like checking print stylesheets, it’s inefficient enough to add drudgery and pull you out of The Zone.</p><p>There’s already <a href="https://github.com/hipstersmoothie/storybook-dark-mode">an addon for switching the Storybook <em>interface</em> to and from dark mode</a>, but I want to simulate dark mode <em>for the components themselves</em>.</p><p>Storybook has an event system you can subscribe and react to, and this dark mode addon fires an event when the UI mode changes. All that’s left is to let the component preview pane know what mode to display.</p><p>I had to make two changes to my project: one to my strategy for applying dark mode styles, and another to the Storybook setup.</p><p>First, I switched away from using a media query to apply dark classes with <code>@media (prefers-color-scheme: dark)</code>. Instead, I decided I’d detect that setting with JavaScript and apply a body class. This makes JavaScript a requirement, but it means I can honor the visitor’s browser setting by default and expose the dark scheme as an option on the site.<br></p><p>Now that I could toggle dark mode with JavaScript, the next step was ... to toggle dark mode with JavaScript. In Storybook, this meant listening for a change to the dark mode UI preference and responding accordingly. This was a matter of adding a few lines to <code>.storybook/config.js</code>, simple enough I nailed it on my very first try. &#x1f389;<br></p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <img src="https://workingconcept.com//_astro/dark-mode-toggle.CmUatkhL.gif" width="1816" height="1450" decoding="async" loading="lazy" alt="Screen capture showing Storybook interface with a title component, toggled into and out of dark mode by clicking a menu item."> <figcaption> <p> Toggling dark mode on and off for the UI and component pane.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The change for the site was nearly the same. Let’s take a look.</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>The Steps</h2><h4>1. Set up Storybook.</h4><p><a href="https://storybook.js.org/docs/guides/quick-start-guide/">Add Storybook to your project</a> and write a story for your favorite component.</p><h4>2. Install the dark mode addon.</h4><p><a href="https://github.com/hipstersmoothie/storybook-dark-mode">github.com/hipstersmoothie/storybook-dark-mode</a></p><h4>3. Write dark mode styles.</h4><p>You’ll need styles meant for dark mode, applied when the document’s body class is <code>.dark</code>. I have some PostCSS that looks like this:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#FFCB6B">body</span><span style="color:#89DDFF">.</span><span style="color:#FFCB6B">dark</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#EEFFFF">  @apply bg-oxford-blue</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#EEFFFF">  @apply text-light-gray</span><span style="color:#89DDFF">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">  .text-slate {</span></span>
<span class="line"><span style="color:#EEFFFF">    @apply text-light-gray</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">  pre</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#FFCB6B">  code</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#FFCB6B">  code</span><span style="color:#89DDFF">[</span><span style="color:#C792EA">class</span><span style="color:#89DDFF">*=</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">language-</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">],</span></span>
<span class="line"><span style="color:#FFCB6B">  pre</span><span style="color:#89DDFF">[</span><span style="color:#C792EA">class</span><span style="color:#89DDFF">*=</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">language-</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">]</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#EEFFFF">    @apply bg-black</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"><span style="color:#EEFFFF">  </span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  /* There's more, but nothing that’d surprise you. */</span></span>
<span class="line"><span style="color:#EEFFFF">}</span></span></code></pre> <figcaption class="-my-4"> <p>Global dark mode styles with Tailwind utility classes.</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Tailwind isn’t important here, but side note if you use Tailwind: there’s <a href="https://github.com/ChanceArthur/tailwindcss-dark-mode">a plugin for adding dark mode utility classes</a>. I haven’t tried it yet, but its strategy is similar requiring a <code>mode-dark</code> class applied to the <code>html</code> element.</p><h4>4. Respond to Storybook UI mode changes.</h4><p>Add the following import and snippet to <code>.storybook/config.js</code>:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#89DDFF;font-style:italic">import</span><span style="color:#EEFFFF"> addons </span><span style="color:#89DDFF;font-style:italic">from</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">@storybook/addons</span><span style="color:#89DDFF">'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">// get an instance to the communication channel for the manager and preview</span></span>
<span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> channel </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF"> addons</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">getChannel</span><span style="color:#EEFFFF">()</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">// switch body class for story along with interface theme</span></span>
<span class="line"><span style="color:#EEFFFF">channel</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">on</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">DARK_MODE</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> isDark</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  if</span><span style="color:#F07178"> (</span><span style="color:#EEFFFF">isDark</span><span style="color:#F07178">) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    document</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">body</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">classList</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">add</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">dark</span><span style="color:#89DDFF">'</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#89DDFF;font-style:italic"> else</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#EEFFFF">    document</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">body</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">classList</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">remove</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">dark</span><span style="color:#89DDFF">'</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"><span style="color:#89DDFF">}</span><span style="color:#EEFFFF">)</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This will toggle a <code>dark</code> class on the body of the component preview pane when the Storybook UI theme changes.</p><h4>5. Add the dark mode class with your site/app.</h4><p>Make sure your project handles the body class, too!</p><p>This step is optional for the purpose of this post, but if you forget you may wonder why your dark mode styles never work outside Storybook.</p><p>I chose to do this in my layout component, passing the desired body class to Helmet:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> Layout </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> ({</span><span style="color:#EEFFFF;font-style:italic"> children</span><span style="color:#89DDFF"> })</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // make sure `window` exists, then check whether the visitor prefers dark mode  </span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> prefersDark</span><span style="color:#89DDFF"> =</span></span>
<span class="line"><span style="color:#89DDFF">    typeof</span><span style="color:#EEFFFF"> window</span><span style="color:#89DDFF"> !==</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">undefined</span><span style="color:#89DDFF">'</span></span>
<span class="line"><span style="color:#89DDFF">      ?</span><span style="color:#EEFFFF"> window</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">matchMedia</span><span style="color:#89DDFF"> &#x26;&#x26;</span></span>
<span class="line"><span style="color:#EEFFFF">        window</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">matchMedia</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">(prefers-color-scheme: dark)</span><span style="color:#89DDFF">'</span><span style="color:#F07178">)</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">matches</span></span>
<span class="line"><span style="color:#89DDFF">      :</span><span style="color:#FF9CAC"> false</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // give Helmet a body class to apply</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#F07178"> (</span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;></span></span>
<span class="line"><span style="color:#89DDFF">      &#x3C;</span><span style="color:#FFCB6B">Helmet</span></span>
<span class="line"><span style="color:#C792EA">        bodyAttributes</span><span style="color:#89DDFF">={{</span></span>
<span class="line"><span style="color:#F07178">          class</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> prefersDark </span><span style="color:#89DDFF">?</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">dark</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> :</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">normal</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        }}</span></span>
<span class="line"><span style="color:#89DDFF">      /></span></span>
<span class="line"><span style="color:#89DDFF">      &#x3C;</span><span style="color:#FFCB6B">Header</span><span style="color:#89DDFF"> /></span></span>
<span class="line"><span style="color:#89DDFF">      {</span><span style="color:#EEFFFF">children</span><span style="color:#89DDFF">}</span></span>
<span class="line"><span style="color:#89DDFF">      &#x3C;</span><span style="color:#FFCB6B">Footer</span><span style="color:#89DDFF"> /></span></span>
<span class="line"><span style="color:#89DDFF">    &#x3C;/></span></span>
<span class="line"><span style="color:#F07178">  )</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre> <figcaption class="-my-4"> <p>The check for <code>window</code> is important in this context because it won’t exist during server side rendering (SSR).</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>There’s a similar example, classing the <code>html</code> element, in the tailwindcss-dark-mode project: <a href="https://github.com/ChanceArthur/tailwindcss-dark-mode/blob/master/prefers-dark.js">prefers-dark.js</a>.</p><p>This strategy relies on the style cascade, so I’m not sure how useful it will be in a CSS-in-JS world but I’ll deal with that when I get there.</p><p>I’m hoping <em>somebody’s</em> as thrilled as I am to toggle back and forth easily!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sublime Markdown to Sparrow Message]]></title>
            <link>https://workingconcept.com/blog/sublime-markdown-to-sparrow-message</link>
            <guid>https://workingconcept.com/blog/sublime-markdown-to-sparrow-message</guid>
            <pubDate>Sun, 27 Oct 2013 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/sublime-markdown-sparrow.Bxez8VEk_ZmU4NO.webp" type="image/webp"><source src="https://workingconcept.com//_astro/sublime-markdown-sparrow.Bxez8VEk_2sg1iy.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/sublime-markdown-sparrow.Bxez8VEk_ZW9MhT.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/sublime-markdown-sparrow.Bxez8VEk_mAhtm.png" decoding="async" loading="lazy" alt="Screenshot of markdown document in one window and rendered Sparrow draft message in the other" width="1131" height="656"> </picture> <figcaption> <p> Export Markdown as HTML to Sparrow!  </p> </figcaption> </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Background</h2>
<p>It’s rare that I have a Markdown-related thought for which <a href="http://brettterpstra.com/">Brett Terpstra</a> hasn’t already made some elegant thing, but for once I think I’ve done it!</p>
<p>After buying just about every Markdown writing app there is, I’ve finally circled back to Sublime Text 3 with <a href="http://brettterpstra.com/projects/markdownediting/">Terpstra’s Markdown Editing package</a> and <a href="http://marked2app.com/">Marked 2</a>.</p>
<p><a href="http://www.macstories.net/stories/editorial-for-ipad-review/">Federico Vittici’s epic review of Editorial</a>, a <a href="http://omz-software.com/editorial/">really impressive iOS text editor</a>, prompted me to give it a try, and one particular workflow has been tickling my fancy ever since: compose your message in Markdown, then in two taps pipe the rendered HTML message to Mail.app. Wonderful!</p>
<p>Naturally, I wanted to be able to do this on the desktop as well. To my complete surprise, Mr. Terpstra had not yet graced us with a solution for this, and neither did the internet at large. Months later, I <a href="http://stackoverflow.com/questions/19611831/sublime-text-markdown-to-html-email">posed an iffy question on Stack Overflow</a>: can I use Python to get Markdown from Sublime Text 3 rendered to an email client? User <em>fanti</em> swooped in almost immediately with an alarmingly thorough answer. So I had no choice but to pick up the quest.</p>
<h2>Disclaimer</h2>
<p>I’ve only ever hacked away at Python and AppleScript, and I am no authority on either one. I’m simply sharing my progress with the hope that you’ll learn something from my baby steps or just get your Markdown-to-email workflow started.</p>
<p>As usual, if your computer explodes or the universe collapses on itself because of anything here, that’s your problem. No warranty.</p>
<h2>Overview</h2>
<p>We’re building a Sublime Text 3 build system so that we can hit ⌘+B and ship our Markdown off to <a href="https://web.archive.org/web/20141201142101/http://sparrowmailapp.com/mac.php">Sparrow</a><sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>.</p>
<h2>Step 1: Install markdown2 module, set up converter script.</h2>
<p>We’re going to have Python convert Markdown into HTML, so we need to install Python’s markdown2 module (thanks again, <a href="http://stackoverflow.com/questions/1213690/what-is-the-most-compatible-way-to-install-python-modules-on-a-mac">Stack Overflow</a>!):</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">sudo</span><span style="color:#C3E88D"> easy_install</span><span style="color:#C3E88D"> pip</span></span></code></pre>  </figure><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>sudo pip install markdown2</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Now, save a file called <strong>convert.py</strong> somewhere handy – I keep mine with other scripts in my Dropbox folder:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#89DDFF;font-style:italic">import</span><span style="color:#EEFFFF"> subprocess</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">import</span><span style="color:#EEFFFF"> sys</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">import</span><span style="color:#EEFFFF"> markdown2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">with</span><span style="color:#82AAFF"> open</span><span style="color:#89DDFF">(</span><span style="color:#82AAFF">sys</span><span style="color:#89DDFF">.</span><span style="color:#F07178">argv</span><span style="color:#89DDFF">[</span><span style="color:#F78C6C">1</span><span style="color:#89DDFF">])</span><span style="color:#89DDFF;font-style:italic"> as</span><span style="color:#EEFFFF"> f</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#EEFFFF">    unprocessed </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF"> f</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">read</span><span style="color:#89DDFF">()</span></span>
<span class="line"><span style="color:#EEFFFF">    processed </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF"> markdown2</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">markdown</span><span style="color:#89DDFF">(</span><span style="color:#82AAFF">unprocessed</span><span style="color:#89DDFF">).</span><span style="color:#82AAFF">replace</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">"</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> '</span><span style="color:#EEFFFF">\\</span><span style="color:#C3E88D">"</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">).</span><span style="color:#82AAFF">encode</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">utf8</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">    script </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> """</span><span style="color:#C3E88D">tell application "Sparrow"</span></span>
<span class="line"><span style="color:#C3E88D">        activate</span></span>
<span class="line"><span style="color:#C3E88D">        compose</span></span>
<span class="line"><span style="color:#C3E88D">        set theMessage to make new outgoing message with properties { htmlContent: "</span><span style="color:#F78C6C">%s</span><span style="color:#C3E88D">" }</span></span>
<span class="line"><span style="color:#C3E88D">        tell theMessage</span></span>
<span class="line"><span style="color:#C3E88D">            compose</span></span>
<span class="line"><span style="color:#C3E88D">        end tell</span></span>
<span class="line"><span style="color:#C3E88D">    end tell</span><span style="color:#89DDFF">"""</span><span style="color:#89DDFF"> %</span><span style="color:#EEFFFF"> processed</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">    p </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF"> subprocess</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">Popen</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">/usr/bin/osascript</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> stdin</span><span style="color:#89DDFF">=</span><span style="color:#82AAFF">subprocess</span><span style="color:#89DDFF">.</span><span style="color:#F07178">PIPE</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> stdout</span><span style="color:#89DDFF">=</span><span style="color:#82AAFF">subprocess</span><span style="color:#89DDFF">.</span><span style="color:#F07178">PIPE</span><span style="color:#89DDFF">)</span></span>
<span class="line"><span style="color:#EEFFFF">    p</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">communicate</span><span style="color:#89DDFF">(</span><span style="color:#82AAFF">script</span><span style="color:#89DDFF">)</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This is where all the magic happens, and it took me a few hours to get it working properly with lots of Googling and successive <del>failures</del> learning opportunities.</p>
<p>We’re reading the specified text file, using markdown2 to convert it to HTML, escaping the result, then creating and calling ActionScript that gets piped to Sparrow. Even for uninitiated, this probably makes sense.</p>
<p>If you’ve never used Python before, be careful copying and pasting these examples! Python loves – needs, even – tabs and not spaces, so <strong>be sure to keep tabs preserved exactly as they are</strong>.</p>
<h2>Step 2: Create the new Sublime Text build system.</h2>
<p>Go to Tools → Build System → New Build System and add your new build:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#89DDFF">    "</span><span style="color:#C3E88D">cmd</span><span style="color:#89DDFF">"</span><span style="color:#F07178">: [</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">python</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">-u</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">/path/to/your/convert.py</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">$file</span><span style="color:#89DDFF">"</span><span style="color:#F07178">]</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    "</span><span style="color:#C3E88D">selector</span><span style="color:#89DDFF">"</span><span style="color:#F07178">: </span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">source.markdown</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    "</span><span style="color:#C3E88D">path</span><span style="color:#89DDFF">"</span><span style="color:#F07178">: </span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">/usr/bin</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Save this file in Sublimes Packages/User folder with a name like <strong>MarkdownToEmail.sublime-build</strong>. This will make the build system available within Sublime Text, where after a restart you’ll find it under Tools → Build System → MarkdownToEmail.</p>
<p>Note that my Python path is /usr/bin/python – you can check yours by opening Terminal and running…</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#82AAFF">which</span><span style="color:#C3E88D"> python</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>…and your full python path will be printed for either verification or a new round of confusion.</p>
<h2>Step 3: Try it!</h2>
<p>Now you’ve got everything in place to compose a message in Sublime Text. Set your new build system and hit <kbd>⌘</kbd>+<kbd>B</kbd> to render HTML in a new Sparrow message window. Hopefully it works, and if not you’ll want to do some troubleshooting:</p>
<ul>
<li>Are your paths all correct?</li>
<li>Can you run convert.py from the command line, outside of Sublime Text?</li>
<li>Did my feeble convert.py choke on a character or encoding that I hadn’t planned for or tested?</li>
</ul>
<h2>Other Ideas</h2>
<p>This could clearly be improved. Ideas I’ve had thus far… </p>
<ul>
<li>Populate the subject with the name of the file, or even use YAML to set a subject, recipients, etc.</li>
<li>Add an option to automatically delete the original Markdown once it becomes an email message.</li>
<li>Figure out how to get this working with Mail.app and clients that other people actually use.</li>
</ul>
<p>Let me know if you have any more ideas, run into problems, or if you’ve got any tips or objections!</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p>What if you don’t use Sparrow? Probably a good call since it’s just waiting around to die, but I finally learned the answer to this: check the AppleScript Dictionary. Open the AppleScript Editor application, then choose File and Open Dictionary. This is a perfect reference of every app on your machine that has an AppleScript API, along with documentation for each one. This is ultimately how I found Sparrow’s htmlContent property. See if your client has some kind of support for passing in message text in HTML, and if so you can edit convert.py to suit. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting Up a Craft Site with Webfaction]]></title>
            <link>https://workingconcept.com/blog/webfaction-craft</link>
            <guid>https://workingconcept.com/blog/webfaction-craft</guid>
            <pubDate>Thu, 14 May 2015 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>It’s no secret that I’m a fan of <a href="https://www.webfaction.com/?aid=52204">Webfaction</a>. For me, the $9.50/month standard service has replaced crappy shared hosts and even some Media Temple DV servers. Webfaction combines the ease of a shared platform—think Media Temple (gs) or your typical $3/month host—with the speed and flexibility of a VPS. The handful of Craft sites I’ve set up on Webfaction fly, with great speed and solid uptime.</p><p>If you’re new to Webfaction, there’s a learning curve that can make your first site frustrating to set up. The control panel will undoubtedly be different from what you’re used to, even if it turns out to be really simple. (You may eventually like it, even!) I’ll attempt to help you make sense of things and get your first site up and running in short time.</p><p>The key thing to understand is what a <em>Website</em> is in Webfaction’s terms: an <em>Application</em> assigned to one or more <em>Domain Names</em>. This means that to create our Craft site, we’ll set up the pieces and link them together. You can read more about Webfaction’s unique, organized setup in their <a href="http://docs.webfaction.com/user-guide/websites.html">Applications and Websites</a> guide.</p><h2>Pros</h2><ul><li>Bang for your buck: the fastest, most stable hosting I’ve found for the least amount of money.</li><li>You don’t need to be a sysadmin: you don’t maintain server software, worry about security, or bother with networking issues. Smart, qualified people do that.</li><li>Support tickets are addressed quickly and thoughtfully, no “did you turn it off and on again?” run-around.</li><li>MySQL is particularly fast since it runs in a separate container with its own (free) memory.</li><li>Different geographic locations to choose from: US, Europe, Asia (hint: choose the one closest to your primary audience).</li><li>You can pay to upgrade your resources, so there’s some peace of mind knowing you can scale if/when you need to.</li><li>It’s possible to SSH in and install software on the server, so things are more flexible than you might be used to with a shared platform. More like a VPS in that way.</li><li>The platform is organized and pretty clever, with modern software versions and some thoughtful app install shortcuts. (But no tacky 1-Click garbage!)</li></ul><h2>Cons</h2><ul><li>No phone or live chat support, which could be a shock if you’re coming from Media Temple.</li><li>You’re limited to one set of control panel login credentials, which can make for a funny little shuffle if you’re sharing the account with a client.</li><li>You’ll have to deal with a minor learning curve as you set up your first site.</li></ul><h2>What You’ll Need</h2><ul><li>a Craft site you’ve built, ready to check out from GitHub or Bitbucket</li><li>a MySQL dump of said Craft site</li><li>a spirit of adventure</li></ul><h2>Getting Down to Business</h2><p>What we’re going to do is check out our project repository and point a symbolic link to it. The folder structure will end up like this…</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>~/craft-site</span></span>
<span class="line"><span>    /.git</span></span>
<span class="line"><span>    /craft</span></span>
<span class="line"><span>    /public</span></span>
<span class="line"><span>~/webapps/craft → ~/craft-site/public</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>1. Create your Webfaction account</strong>, using <a href="https://www.webfaction.com/?aid=52204">my referral link</a> if you’re a kind soul, <a href="https://www.webfaction.com/">or not</a> if you’re sick of those dirtbags that sprinkle referral links everywhere. Use PayPal or a credit card to plunk down $9.50 and get full access. <strong>Choose your username wisely, as it’ll be the basename of your account and used as your site preview URL (i.e. username.webfactional.com).</strong></p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-start-trial.CnHpRetu_Z2qTXJQ.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-start-trial.CnHpRetu_qwHye.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-start-trial.CnHpRetu_Z23Hwt4.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-start-trial.CnHpRetu_Z3Bysa.png" decoding="async" loading="lazy" alt="Screenshot of Webfaction's signup form" width="1074" height="635"> </picture> <figcaption> <p> You can figure this out.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>2. Clone a copy of your Craft site</strong> by SSHing into your account (<code>ssh username@username.webfactional.com</code>) and creating a project folder. You’ll start at <code>/home/username</code>, so <code>mkdir craft-site</code> to create <code>/home/username/craft-site</code>. Step into this folder with <code>cd craft-site</code> and then <code>git clone</code> your Craft project here. (Something like <code>git clone git@bitbucket.org:acme/acme-craft.git .</code>.)</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-clone.BApZZk34_Z1qS5Gt.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-clone.BApZZk34_V2Jqh.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-clone.BApZZk34_LfrYc.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-clone.BApZZk34_2hQ2LK.png" decoding="async" loading="lazy" alt="Screenshot of cloning project repository from the command line" width="717" height="562"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>3. Create a new <em>Application</em> in the Webfaction control panel.</strong> You can do this by choosing <em>Domains/Websites</em> → <em>Applications</em> → and <em>Add new application</em>. Pick a sensible name, choose <em>Symbolic link</em> for the App category, and choose the latest PHP option among the <em>Symbolic link to static/cgi/php5x</em> variants. For Extra info, specify the path of the public folder for your Craft project. In this case, it’d be <code>/home/username/craft-site/public</code>.<sup id="fnref1"><a href="{fn:1:url||}">1</a></sup></p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_ZR8caR.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_ZcVIcP.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_Z20IEOO.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_Z2cxjQz.png" decoding="async" loading="lazy" alt="Screenshot of application configuration in the Webfaction control panel" width="1075" height="963"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>4. Create a new MySQL database</strong> under <em>Databases</em> → <em>Databases</em> → <em>Add new database</em>. Pick a name, use MySQL, leave Encoding alone, and create a new user for the database and squirrel away the connection details.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-mysql.CYsSfzkI_Z1PKGLK.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-mysql.CYsSfzkI_Z1PI9aC.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-mysql.CYsSfzkI_1wJvxz.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-mysql.CYsSfzkI_SYWA2.png" decoding="async" loading="lazy" alt="Screenshot of adding a new MySQL database in the Webfaction control panel" width="1074" height="635"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>5. Import your MySQL dump</strong> by using Webfaction’s phpMyAdmin or connecting a desktop client like Sequel Pro through an SSH tunnel.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-load-mysql.DBDhVsZc_Z1WQ0W5.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-load-mysql.DBDhVsZc_14UhkI.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-load-mysql.DBDhVsZc_Z2h78vf.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-load-mysql.DBDhVsZc_Zf6b41.png" decoding="async" loading="lazy" alt="Screenshot of SQL dump import in phpMyAdmin" width="1076" height="793"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>6. Add your domains</strong> under <em>Domains/Websites</em> → <em>Domains</em> → <em>Add new domain</em>. These won’t do anything yet, we’re just telling Webfaction we’ll want to use them. If your website is acme.com, here you’ll want to add <code>acme.com www.acme.com</code>.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-domains.Digo3plT_Z4qXC9.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-domains.Digo3plT_zJukS.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-domains.Digo3plT_Z1d2rh6.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-domains.Digo3plT_Z1oQ6iQ.png" decoding="async" loading="lazy" alt="Screenshot of adding new domain names in the Webfaction control panel" width="1074" height="403"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>7. Finally, <strong>add a <em>Website</em></strong> from <em>Domains/Websites</em> → <em>Websites</em> → <em>Add new website</em>. Pick another name and skip to domains, where you’ll use the type-ahead field to select the domains you just added in step 5. Choose <em>Add an applicaton</em> → <em>Reuse an existing application</em>, and pick the one you created in step 3. This is a good time to also assign your preview domain (<code>username.webfactional.com</code>), if you want to take a look at your site before pointing your domains to Webfaction. Save and you’re done!</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_ZR8caR.webp" type="image/webp"><source src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_ZcVIcP.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_Z20IEOO.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/webfaction-website.DVj9k3Z-_Z2cxjQz.png" decoding="async" loading="lazy" alt="Screenshot of creating a new Website in the Webfaction control panel" width="1075" height="963"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Now that the setup is finished, you’ll want to introduce Craft to your new environment details by editing<code>db.php</code> and <code>general.php</code> in the <code>craft/config/</code> directory. I’ll assume you’re already using <a href="http://buildwithcraft.com/docs/multi-environment-configs">multi-environment configs</a> and you know how to do this.</p><p>Did I miss or fail to illuminate anything in this post? Let me know and I’ll get it updated! </p><div class="footnotes"><hr><ol><li id="fn:1"><p>Rather than create a new <em>Application</em> that amounts to a docroot+subfolder, we’re telling Webfaction to point a symbolic link to Craft’s <code>public</code> folder and serve that. Like any CMS that keeps its business end out of the web root, Craft is happy this way and we also get to maintain clean Git pulls without restructuring anything in the hosting environment. <a href="#fnref1" rev="footnote" class="footnote-backref">↩</a></p></li></ol></div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MAMP Pro to DDEV]]></title>
            <link>https://workingconcept.com/blog/mamp-pro-to-ddev</link>
            <guid>https://workingconcept.com/blog/mamp-pro-to-ddev</guid>
            <pubDate>Fri, 16 Nov 2018 08:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/mamp-ddev.H0bc55Wa_23p0Q2.webp" type="image/webp"><source src="https://workingconcept.com//_astro/mamp-ddev.H0bc55Wa_bb4ld.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/mamp-ddev.H0bc55Wa_1RNmDy.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/mamp-ddev.H0bc55Wa_ZD0fzK.png" decoding="async" loading="lazy" alt="MAMP Pro to DDEV with Docker" width="1360" height="600"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’re like me, you’ve used MAMP Pro for years because you work on a Mac and it’s easy to get a PHP site running in a few clicks. </p><p>You first approached the command line as one would a wounded bear, but you’ve increasingly found it to be inviting and liberating. You’ve poked at Vagrant, been wooed at the <em>idea</em> of Docker, and you’ve soldiered on with MAMP because you haven’t found a way past it that sticks day to day. You may be wary of those who espouse command-line-first tools, because the tingle of excitement is normally followed by hours of stumbling over broken dependencies of which zero were obvious.</p><p>Take heart! I’ve finally found my local development champion in <a href="https://ddev.readthedocs.io/en/stable/">DDEV</a> and I’ve been happily using it for more than a month. Let me reiterate: happily. More than a month. Not a single moment of weakness when I had to go back to MAMP just to do that one thing. DDEV isn’t perfect, but it’s been a welcome MAMP replacement and I’m going to try and convince you to try it.</p><h2>DDEV?</h2><p>DDEV isn’t a prepackaged Mac app like MAMP, but a command line tool that manages Docker containers for development. </p><p>Like MAMP, it’s easy to work with and it comes with the innards you’ll need for working with LAMP/LEMP apps. Unlike MAMP, configuration lives quietly within each project (which you can version if you want!) and the machinery is all powered by Docker containers. That difference is important because those containers are more efficient than individual virtual machines like you might run with VirtualBox and Vagrant. They’re not as efficient as MAMP, but each project container runs its own software stack and can be fully customized—very much <em>unlike</em> MAMP. These containers are also fully isolated, so (like MAMP) they don’t have to care about whatever software you’re running natively on your Mac.</p><p>DDEV is free and actively maintained, as are similar systems like <a href="https://docs.devwithlando.io/">Lando</a>, <a href="https://docksal.io/">Docksal</a> and <a href="http://devilbox.org/">Devilbox</a>. The purpose of this post isn’t to compare options, but I’ll at least mention that I stuck with DDEV because almost every problem I encountered had an answer, whether by design or documentation or mention in a support issue. It required the fewest changes to my workflow and could be tailored most easily wherever I was forced to evolve. Each of the above was easy to get started with, and DDEV <em>kept making sense</em> as I went.</p><p>Pure Docker containers (or docker-compose directives) would be great, but I haven’t developed enough proficiency to actually ditch MAMP and use Docker that closely day to day. DDEV provided just enough to get me over that wall. If you’ve already considered using Docker for local development, spin up a DDEV project and examine the difference between the yaml you define and the <em>docker-compose.yaml</em> DDEV generates for you. That difference, very precisely, is DDEV’s appeal: it abstracts what might otherwise be a bit too complicated for daily use.</p><h2>DDEV Pros</h2><ul><li>Great for Craft CMS, Statamic, Laravel, and ExpressionEngine projects. Pick Apache or nginx in your config, choose a PHP version (back to 5.6), and <code>ddev start</code> to spin up the project.</li><li>Assumes you’ll want to be working locally (and offline) just like MAMP, and automatically attempts to manage <em>/etc/hosts</em> changes for your <em>*.ddev.local</em> or custom development domain.</li><li>The standard PHP setup happens to come with all Craft requirements, including ImageMagick and GD. Default Apache + nginx setups will play nice with clean URLs, and you can season to taste easily with your own <em>.htaccess</em>, <em>nginx.conf</em>, and/or <em>php.ini</em>.</li><li>Easy database management: quick commands for taking snapshots, importing dumps, and launching PHPMyAdmin or Sequel Pro.</li><li>Crazy ability to customize if you’re willing to experiment a bit with Docker and Linux. Also a nice way to learn about Docker since you can peek behind the DDEV curtain to see how it works, and further customization will mean getting closer to Docker and not some odd contraption a Gandalfish character maintains from a remote mountaintop with spotty satellite internet.</li><li><a href="https://github.com/drud/ddev-ui">An alpha GUI</a> if you just can’t break the habit.</li><li>Regularly updated, pretty thoroughly documented, and supported by friendly and responsive folks.</li><li>Working on a project and actually getting stuff done without MAMP will make you feel a keen sense of triumph.</li></ul><h2>DDEV Cons</h2><ul><li>Hyperkit, which is required to run Docker on a Mac, has often taken an absurd amount of CPU on both (Mojave) Macs I use regularly. Your mileage may vary, and I’m counting on this eventually being improved.</li><li>Docker downloads entire images as it needs them, so it can quickly take more disk space than MAMP if you tend to customize your environments. This can be managed, but it could be a deal-killer if you work on a laptop without ample free space.</li><li>If you develop Craft 3 plugins and use symbolic links that reach outside your current project, the complexities of volume mounts mean you’ll want to rsync files into your project. (Or deal with either an incredible performance hit or a solution I’ve not yet found.)</li><li><del>Though every site gets https support by default, the self-signed certificate means you’ll have to live with browsers being cranky. This is a huge drag when you’re working with an app like PageLayers that doesn’t allow you to ignore self-signed certificates.</del> No longer true <a href="https://github.com/drud/ddev/releases/tag/v1.8.0">since DDEV 1.8.0</a>.</li></ul><h2>What it Looks Like</h2><p>Once you’ve installed DDEV and Docker for Mac, you’ll run <code>ddev config</code> from your project root to initialize some .yaml in a project-relative <em>.ddev</em> folder.</p><p>Rather than loading MAMP and switching on its server, you’ll just <code>ddev start</code> from a project root. If you forget how to access your site or database, just <code>ddev describe</code>:</p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <img src="https://workingconcept.com//_astro/ddev-start.Y9NzfGxt.gif" width="1890" height="966" decoding="async" loading="lazy"> <figcaption> <p> ddev start and ddev describe  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>You can use <code>ddev stop</code> to shut down that project, or <code>ddev restart</code> to turn it off and on again. While you’re working, it can be handy to run <code>ddev snapshot</code> and <code>ddev restore-snapshot</code> for jumping between database states.</p><p>There are a few more utilities that can be useful as well…</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">$</span><span style="color:#C3E88D"> ddev</span><span style="color:#C3E88D"> -h</span></span>
<span class="line"><span style="color:#FFCB6B">This</span><span style="color:#C3E88D"> Command</span><span style="color:#C3E88D"> Line</span><span style="color:#C3E88D"> Interface</span><span style="color:#EEFFFF"> (CLI) gives you the ability to interact with the ddev to create a development environment.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Usage:</span></span>
<span class="line"><span style="color:#FFCB6B">  ddev</span><span style="color:#EEFFFF"> [command]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Available</span><span style="color:#C3E88D"> Commands:</span></span>
<span class="line"><span style="color:#FFCB6B">  auth-pantheon</span><span style="color:#C3E88D">    Provide</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> machine</span><span style="color:#C3E88D"> token</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> global</span><span style="color:#C3E88D"> pantheon</span><span style="color:#C3E88D"> auth.</span></span>
<span class="line"><span style="color:#FFCB6B">  config</span><span style="color:#C3E88D">           Create</span><span style="color:#C3E88D"> or</span><span style="color:#C3E88D"> modify</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> ddev</span><span style="color:#C3E88D"> project</span><span style="color:#C3E88D"> configuration</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> current</span><span style="color:#C3E88D"> directory</span></span>
<span class="line"><span style="color:#FFCB6B">  describe</span><span style="color:#C3E88D">         Get</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> detailed</span><span style="color:#C3E88D"> description</span><span style="color:#C3E88D"> of</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> running</span><span style="color:#C3E88D"> ddev</span><span style="color:#C3E88D"> project.</span></span>
<span class="line"><span style="color:#82AAFF">  exec</span><span style="color:#C3E88D">             Execute</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> shell</span><span style="color:#C3E88D"> command</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> container</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> service.</span><span style="color:#C3E88D"> Uses</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> web</span><span style="color:#C3E88D"> service</span><span style="color:#C3E88D"> by</span><span style="color:#C3E88D"> default.</span></span>
<span class="line"><span style="color:#FFCB6B">  help</span><span style="color:#C3E88D">             Help</span><span style="color:#C3E88D"> about</span><span style="color:#C3E88D"> any</span><span style="color:#C3E88D"> command</span></span>
<span class="line"><span style="color:#FFCB6B">  hostname</span><span style="color:#C3E88D">         Manage</span><span style="color:#C3E88D"> your</span><span style="color:#C3E88D"> hostfile</span><span style="color:#C3E88D"> entries.</span></span>
<span class="line"><span style="color:#FFCB6B">  import-db</span><span style="color:#C3E88D">        Pull</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> database</span><span style="color:#C3E88D"> of</span><span style="color:#C3E88D"> an</span><span style="color:#C3E88D"> existing</span><span style="color:#C3E88D"> project</span><span style="color:#C3E88D"> to</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> dev</span><span style="color:#C3E88D"> environment.</span></span>
<span class="line"><span style="color:#FFCB6B">  import-files</span><span style="color:#C3E88D">     Pull</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> uploaded</span><span style="color:#C3E88D"> files</span><span style="color:#C3E88D"> directory</span><span style="color:#C3E88D"> of</span><span style="color:#C3E88D"> an</span><span style="color:#C3E88D"> existing</span><span style="color:#C3E88D"> project</span><span style="color:#C3E88D"> to</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> default</span><span style="color:#C3E88D"> public</span><span style="color:#C3E88D"> upload</span><span style="color:#C3E88D"> directory</span><span style="color:#C3E88D"> of</span><span style="color:#C3E88D"> your</span><span style="color:#C3E88D"> project.</span></span>
<span class="line"><span style="color:#FFCB6B">  list</span><span style="color:#C3E88D">             List</span><span style="color:#C3E88D"> projects</span></span>
<span class="line"><span style="color:#FFCB6B">  logs</span><span style="color:#C3E88D">             Get</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> logs</span><span style="color:#C3E88D"> from</span><span style="color:#C3E88D"> your</span><span style="color:#C3E88D"> running</span><span style="color:#C3E88D"> services.</span></span>
<span class="line"><span style="color:#FFCB6B">  pull</span><span style="color:#C3E88D">             Pull</span><span style="color:#C3E88D"> files</span><span style="color:#C3E88D"> and</span><span style="color:#C3E88D"> database</span><span style="color:#C3E88D"> using</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> configured</span><span style="color:#C3E88D"> provider</span><span style="color:#C3E88D"> plugin.</span></span>
<span class="line"><span style="color:#FFCB6B">  remove</span><span style="color:#C3E88D">           Remove</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> development</span><span style="color:#C3E88D"> environment</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> project.</span></span>
<span class="line"><span style="color:#FFCB6B">  restart</span><span style="color:#C3E88D">          Restart</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> development</span><span style="color:#C3E88D"> environment</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> project.</span></span>
<span class="line"><span style="color:#FFCB6B">  restore-snapshot</span><span style="color:#C3E88D"> Restore</span><span style="color:#C3E88D"> a</span><span style="color:#C3E88D"> project</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">s database to the provided snapshot version.</span></span>
<span class="line"><span style="color:#C3E88D">  sequelpro        This command is not available since sequel pro.app is not installed</span></span>
<span class="line"><span style="color:#C3E88D">  snapshot         Create a database snapshot for one or more projects.</span></span>
<span class="line"><span style="color:#C3E88D">  ssh              Starts a shell session in the container for a service. Uses web service by default.</span></span>
<span class="line"><span style="color:#C3E88D">  start            Start a ddev project.</span></span>
<span class="line"><span style="color:#C3E88D">  stop             Stop the development environment for a project.</span></span>
<span class="line"><span style="color:#C3E88D">  version          print ddev version and component versions</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C3E88D">Flags:</span></span>
<span class="line"><span style="color:#C3E88D">  -h, --help          help for ddev</span></span>
<span class="line"><span style="color:#C3E88D">  -j, --json-output   If true, user-oriented output will be in JSON format.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C3E88D">Use "ddev [command] --help" for more information about a command.</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If looking at <code>ddev -h</code> output makes you uncomfortable, you can go and download the <a href="https://github.com/drud/ddev-ui">alpha DDEV UI</a>. It’s not nearly as full-featured as MAMP’s UI, but it’ll let you see all your projects with their status and give you the ability to start, restart, and stop each one.</p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/ddev-ui.CGOhpR-F_Z1bWCf7.webp" type="image/webp"><source src="https://workingconcept.com//_astro/ddev-ui.CGOhpR-F_210z40.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/ddev-ui.CGOhpR-F_Z1mygrA.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/ddev-ui.CGOhpR-F_1bOf82.png" decoding="async" loading="lazy" alt="Screenshot of DDEV alpha UI." width="1986" height="1474"> </picture> <figcaption> <p> DDEV&#39;s alpha UI, for those that must.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’re still reading I’ll assume this is theoretically interesting. Let’s look a little closer at what’s going on and start up some Craft projects.</p><h2>Getting Started</h2><p>You’ll need to install Docker and DDEV. If you’re on a Mac with <a href="https://brew.sh/">Homebrew</a> installed, you can run <code>brew cask install docker</code> and then <code>brew tap drud/ddev && brew install ddev</code>.</p><p>If that went well, choose an existing project and open a command prompt at its root. In VS Code, this is a quick <kbd>^</kbd>+<kbd>~</kbd>. Initialize your project with <code>ddev config</code>, which will ask for three things:</p><ol><li>The project name, which defaults to the parent folder and is used for a <em>*.ddev.local</em> domain.</li><li>The web root.</li><li>The project type, which is <code>php</code> unless you’re using any of the listed options.</li></ol><p>This will create a <em>.ddev</em> folder within your project:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>.ddev</span></span>
<span class="line"><span>- .gitignore</span></span>
<span class="line"><span>- config.yaml</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The config file is pretty simple, with a commented section afterwards (which I’ve omitted here) explaining some bits and pieces you may want to customize.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#F07178">APIVersion</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> v1.3.0</span></span>
<span class="line"><span style="color:#F07178">name</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> my-project</span></span>
<span class="line"><span style="color:#F07178">type</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> php</span></span>
<span class="line"><span style="color:#F07178">docroot</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> public</span></span>
<span class="line"><span style="color:#F07178">php_version</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">7.1</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#F07178">webserver_type</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> nginx-fpm</span></span>
<span class="line"><span style="color:#F07178">router_http_port</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">80</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#F07178">router_https_port</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">443</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#F07178">xdebug_enabled</span><span style="color:#89DDFF">:</span><span style="color:#FF9CAC"> false</span></span>
<span class="line"><span style="color:#F07178">additional_hostnames</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> []</span></span>
<span class="line"><span style="color:#F07178">additional_fqdns</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> []</span></span>
<span class="line"><span style="color:#F07178">provider</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> default</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Not too scary, right? Run <code>ddev start</code> to get the project running. DDEV will ask for your Mac password if it needs to edit /etc/hosts for a custom local domain.</p><p>You’ll probably want to import a database. Drop a .sql or .sql.gz file someplace near your project and use <code>ddev import-db</code> and interactively provide a path. You can also do this in one command with <code>ddev import-db --src=db.sql</code>, or open PHPMyAdmin with the link listed in <code>ddev describe</code> if you’d just rather use a GUI.</p><p>If you’re curious about what’s going on behind the scenes, look no further than the <em>docker-compose.yaml</em> file that was quietly created in your <em>.ddev</em> folder. You may already know what this is, and appreciate that DDEV generated it for you. If you’ve never seen any of this docker-compose business before, this is a configuration fed more directly to Docker that tells it what to do. You don’t have to look at or change anything here, and you probably <em>shouldn’t</em> change any part of it, but it’s a fun place to look if you want to get a better feel for how everything works.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/ddev-docker-compose.iW_NEnqW_2eb4Vf.webp" type="image/webp"><source src="https://workingconcept.com//_astro/ddev-docker-compose.iW_NEnqW_2jddEw.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/ddev-docker-compose.iW_NEnqW_2s2wmj.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/ddev-docker-compose.iW_NEnqW_1x0khR.png" decoding="async" loading="lazy" alt="Screenshot of docker-compose.yaml generated by DDEV" width="1498" height="902"> </picture> <figcaption> <p> This is what DDEV wrote to Docker while you weren&#39;t looking.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Sample Craft 3 Setup</h2><p>I added a few more things to my standard Craft 3 project setup. It takes a few more seconds to copy in files and make an edit, but it’s still quicker to get a project going than it would be with MAMP. I keep the <em>.ddev</em> folder versioned (without changing its gitignore) so another developer can check out the project, <code>ddev start</code>, and be on her way—so these steps only apply to when I’m first adding DDEV to a codebase.</p><p>First, I add a simple setup script hook. Tell config.yaml to run a shell script after everything’s started:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#F07178">hooks</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">  post-start</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#89DDFF">  -</span><span style="color:#F07178"> exec</span><span style="color:#89DDFF">:</span><span style="color:#C3E88D"> .ddev/scripts/setup-craft-3.sh</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This will run <em>setup-craft-3.sh</em>, which I’ve dropped into <em>.ddev/scripts</em>:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#546E7A;font-style:italic">#!/bin/bash</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic"># We're running from \web and need to move up a level</span></span>
<span class="line"><span style="color:#82AAFF">cd</span><span style="color:#C3E88D"> ..</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic"># Search for .env to see whether we've already finished local setup</span></span>
<span class="line"><span style="color:#EEFFFF">ENV_FILE</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">.env</span><span style="color:#89DDFF">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">if</span><span style="color:#89DDFF"> [</span><span style="color:#89DDFF"> -f</span><span style="color:#89DDFF"> "</span><span style="color:#EEFFFF">$ENV_FILE</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF"> ]</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">then</span></span>
<span class="line"><span style="color:#82AAFF">    echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">Environment file found. Leaving it alone!</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">else</span></span>
<span class="line"><span style="color:#82AAFF">    echo</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">Environment file not found. Setting up project.</span><span style="color:#89DDFF">"</span></span>
<span class="line"><span style="color:#FFCB6B">    npm</span><span style="color:#C3E88D"> install</span></span>
<span class="line"><span style="color:#FFCB6B">    composer</span><span style="color:#C3E88D"> install</span></span>
<span class="line"><span style="color:#FFCB6B">    cp</span><span style="color:#C3E88D"> .ddev/.env.ddev.example</span><span style="color:#C3E88D"> .env</span></span>
<span class="line"><span style="color:#FFCB6B">    ./craft</span><span style="color:#C3E88D"> setup/security-key</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">fi</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I keep Craft 3 environment settings in a <em>.env</em> file, and this script just checks to see whether one already exists. If so, it doesn’t do anything. If not, it copies one into place and sets Craft’s security key.</p><p>Contents of <em>.ddev/.env.ddev.example</em>:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span># The environment Craft is currently running in ('dev', 'staging', 'production', etc.)</span></span>
<span class="line"><span>ENVIRONMENT="dev"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The secure key Craft will use for hashing and encrypting data</span></span>
<span class="line"><span>SECURITY_KEY=""</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The database driver that will be used ('mysql' or 'pgsql')</span></span>
<span class="line"><span>DB_DRIVER="mysql"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The database server name or IP address (usually this is 'localhost' or '127.0.0.1')</span></span>
<span class="line"><span>DB_SERVER="db"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The database username to connect with</span></span>
<span class="line"><span>DB_USER="db"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The database password to connect with</span></span>
<span class="line"><span>DB_PASSWORD="db"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The name of the database to select</span></span>
<span class="line"><span>DB_DATABASE="db"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The database schema that will be used (PostgreSQL only)</span></span>
<span class="line"><span>DB_SCHEMA="public"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># The prefix that should be added to generated table names (only necessary if multiple things are sharing the same database)</span></span>
<span class="line"><span>DB_TABLE_PREFIX=""</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><em>Edit 12/9:</em> Note that there’s no <code>DB_PORT</code> specified here. DDEV chooses a new MySQL port every time your project spins up, so we need to let it expose its own <code>DB_PORT</code> environment variable (as it does by default).</p><p>Even more excitingly, I add a file specifically named <em>docker-compose.environment.yaml</em>:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#F07178">version</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">3.6</span><span style="color:#89DDFF">'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F07178">services</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">  web</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">    environment</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> ENVIRONMENT=local</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_DRIVER=mysql</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_SERVER=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_USER=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_PASSWORD=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_DATABASE=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_SCHEMA=public</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> DB_TABLE_PREFIX=</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">#      - REDIS_HOST=$DDEV_HOSTNAME</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This sets environment variables for the server, which you can of course customize however you’d like. Note the last commented-out line, which just reminds me to enable redis for environments that use it.</p><p>There’s redundancy in the .env and environment variables above, but this is all you need to add to have your Craft 3 projects up and running smoothly with DDEV! A modern Laravel or Statamic project would look very similar.</p><p>You can go on to specify whether you need apache or nginx, provide a custom nginx config, and more. I’ll just <a href="https://ddev.readthedocs.io/en/latest/users/extend/customization-extendibility/">set this right here</a>.</p><h2>Sample Craft 2 Setup</h2><p>Older projects may not rely on environment variables for setup, but hopefully you’ve joined me in embracing multi-environment configurations for Craft 2, ExpressionEngine, and that old custom thing you clobbered together.</p><p>Craft 2 still works great with PHP7+, so you can add <em>docker-compose.environment.yaml</em> to your <em>.ddev</em> folder to supply database settings:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#F07178">version</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">3.6</span><span style="color:#89DDFF">'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F07178">services</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">  web</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#F07178">    environment</span><span style="color:#89DDFF">:</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> MYSQL_HOST=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> MYSQL_USER=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> MYSQL_PASSWORD=db</span></span>
<span class="line"><span style="color:#89DDFF">      -</span><span style="color:#C3E88D"> MYSQL_DATABASE=db</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>One more step, however, is to grab those settings using <code>getenv()</code> in <em>craft/config/db.php</em>. Mine look mostly like this:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="php"><code><span class="line"><span style="color:#89DDFF">&#x3C;?</span><span style="color:#EEFFFF">php</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">return</span><span style="color:#89DDFF"> [</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">*</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">server</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">      =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_HOST</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">)</span><span style="color:#89DDFF">     ?:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">localhost</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">database</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_DATABASE</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">)</span><span style="color:#89DDFF"> ?:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">local-db-name</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">user</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">        =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_USER</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">)</span><span style="color:#89DDFF">     ?:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">local-db-user</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">password</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_PASSWORD</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">)</span><span style="color:#89DDFF"> ?:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">local-db-password</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">tablePrefix</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">craft</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">        // to be compatible with MySQL 5.7, requires 2.6.2949 or greater</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">initSQLs</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">     =></span><span style="color:#89DDFF"> [</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">SET SESSION sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">],</span><span style="color:#EEFFFF">   </span></span>
<span class="line"><span style="color:#89DDFF">    ],</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">REVIEW</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">user</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">        =></span><span style="color:#89DDFF"> ''</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">password</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#89DDFF"> ''</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">database</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#89DDFF"> ''</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    ],</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">STAGING</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">user</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">        =></span><span style="color:#89DDFF"> ''</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">password</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#89DDFF"> ''</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">database</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#89DDFF"> ''</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    ],</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">PRODUCTION</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">user</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">        =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_USER</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">password</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_PASSWORD</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">database</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">    =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">MYSQL_DATABASE</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">    ],</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">];</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Ancient Projects</h2><p>There might be a dusty old project or two that have you eyeing MAMP again, but don’t do it! You can edit your DDEV project configuration to go back in time to PHP 5.6 with <code>php_version: "5.6"</code>, and if you’re lucky enough to be using multi-environment configuration you can set up <em>yourproject.ddev.local</em> with DDEV’s database settings. If you haven’t noticed already, the database name, username, and password are all <code>db</code>. Sort of easy to remember, and okay since we’re just working locally.</p><h2>DDEV So Far</h2><p>I’ve actually enjoyed working with DDEV. If after all this glowing praise you’re left wondering why you <em>wouldn’t</em> want to use it instead of MAMP, I’ll give you my top two reasons:</p><ol><li><strong>Hyperkit’s performance can be a dumpster fire.</strong> Emphasis on fire, which may actually start if you’re working on a laptop wearing highly flammable pants. CPU usage can be nuts. I’ve had the best luck limiting Docker to 2 CPUs, giving it 10GB of memory, and 1GB of swap. Also limiting mounted directories to <em>~/.composer</em>, <em>~/.ddev</em>, and <em>~/Documents/git</em> (where projects live) and making sure Docker’s storage uses a .raw disk image.</li><li><strong>You can’t neatly symlink Craft 3 plugins outside your project via composer.</strong> This is because of how file mounts work. You can symlink plugins from within the project just fine, but if you’re working in ~/Projects/foo with DDEV your composer symlink to ~/Projects/craft-plugin won’t work. I’ve either temporarily moved the plugin into the project or put rsync on watch.</li></ol><p>I’m thinking that each issue will be smoothed out one way or another, and overall I’m still happy I’ve made the switch. Let me know what <em>you</em> think if you end up trying it!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2018 Budget VPS Survey]]></title>
            <link>https://workingconcept.com/blog/2018-budget-vps-survey</link>
            <guid>https://workingconcept.com/blog/2018-budget-vps-survey</guid>
            <pubDate>Tue, 30 Oct 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/vps-illustration.DOpMiRB-_1yx6yq.webp" type="image/webp"><source src="https://workingconcept.com//_astro/vps-illustration.DOpMiRB-_1yzEay.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/vps-illustration.DOpMiRB-_Z88NUb.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/vps-illustration.DOpMiRB-_ZKSmRI.png" decoding="async" loading="lazy" alt width="1360" height="600"> </picture>  </figure> </div> <aside class="mx-auto px-6 md:px-0 max-w-md text-teal font-sans text-base leading-normal relative"> <span class="w-4 h-4 inline-block absolute md:-ml-8" style="top:0.75rem"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="max-w-full h-auto" fill="currentColor"> <path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path> </svg> </span> <div class="ml-8 md:ml-0"><p>This post contains affiliate links to the providers I’ve reviewed. It took many hours and I’d love to offset hosting costs if you find my work helpful. If that feels skeezy I’ve left non-affiliate links at the bottom.</p>
</div> </aside><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Every few years, GoDaddy acquires my web host of choice and I embark upon a technical pilgrimage to greener pastures. Webfaction has been the latest victim and I’ve decided to run my own VPS and shop around for the right software and provider.</p><p>Things have been good with <a href="https://www.webfaction.com/?aid=52204">Webfaction</a>, and I’ve defended its shared platform which stands apart from any other I’ve experienced. Every now and then noisy neighbors would be an issue, but sites sped along <em>nearly</em> as if hosted on a VPS with plenty of memory and CPU. (This includes shared service and the first two cloud levels, about a dozen accounts. I would have included it in the benchmarks below, but the sustained CPU usage had the test processes automatically dropped before they could finish.) Some servers were more stable than others, but most were unfailingly fast and rarely down. But if there’s one thing I can’t forgive, it’s being acquired by GoDaddy.</p><p>My humble sites don’t see a lot of traffic, so a single server instance behind Cloudflare works great. <strong>My goal is to find the fastest VPS I can, as close to me as possible, at &lt;=$20/month with at least 99% uptime.</strong> There’s a lot of subjectivity and my approach is hardly scientific, but I enjoy comparing what’s out there and hope my notes might be interesting or useful.</p><h2>Software</h2><ul><li><del>ServerPilot</del></li><li><del>RunCloud</del></li><li><del>Laravel Forge</del></li><li><del>Moss</del></li><li><del>Cloudways</del></li><li>Centmin Mod &#x1f44d;</li></ul><p>After trying (and retrying) a few provisioning/management services I decided I’m ready for more direct control over the VPS. I’m finally comfortable enough with Linux to ditch a web-based GUI and keep things lean. <a href="https://centminmod.com/">Centmin Mod</a> lulled me away from Ubuntu into CentOS. It’s an actively-maintained suite of shell scripts and tools that provision and maintain a CentOS box with lots of fine-tuning for speed and common web server usage. Most importantly for me, it provides a reassuring amount of structure, optimization, and community support when I might otherwise feel like I’m in too deep. George’s notes, benchmarks, and comments are all insightful and often reassuring.</p><h2>Service Provider Goals</h2><p>I had an experimental 512MB RamNode VPS a few years ago and got used to noticing its 100% uptime as I checked on other “more serious” servers. I’ve since deployed projects on cool platforms like AWS, Digital Ocean and fortrabbit, but thought it’d be fun to look around and see where I could maximize performance per dollar. There are tradeoffs with cheaper hosts: monitoring, backups, instant provisioning and hourly billing are all nice (if not essential) and rarely offered by budget providers. Since I’m experimenting with my own projects and not client sites, I’m comfortable working those things out for the sake of learning. I ended up trying out servers from <a href="https://clientarea.ramnode.com/aff.php?aff=2496">RamNode</a>, <a href="https://www.ssdnodes.com/manage/aff.php?aff=686">SSD Nodes</a>, <a href="https://my.hostus.us/aff.php?aff=2294">HostUS</a> and <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a>, comparing against <a href="https://m.do.co/c/f1391f41e5e8">Digital Ocean</a>, <a href="https://www.vultr.com/?ref=7572777">Vultr</a> and <a href="https://aws.amazon.com/">AWS</a> to see how things would stack up.</p><p>My interests are…</p><ol><li><strong>Memory, CPU, and fast storage</strong>, because I want sites and apps to be as fast as they can.</li><li><strong>Network bandwidth</strong>, in case the usual trickle ever becomes a flood.</li><li><strong>Cost</strong>, as I’m ruthlessly cheap with my own hosting and my low-traffic sites don’t warrant exciting infrastructure spend.</li><li><strong>Location/latency</strong>. I’m in Seattle and short pings make everything <em>feel</em> fast.</li><li><strong>Support</strong>, which should be competent and able to respond to coherently-submitted tickets within at least a few hours.</li><li><strong>Stability</strong>: reasonable load and strong uptime.</li></ol><h2>Cheap VPS Challengers</h2><p>I wasn’t trying to find servers that’d closely match each other feature for feature, but compare different things and see what I could learn. Let’s see how various &lt;=$20/month options stack up first, then we can compare on performance later.</p></div><div class="table-block pl-6 copy overflow-x-auto max-w-full lg:mx-auto lg:pl-0 lg:max-w-lg"> <table> <thead> <tr> <th class="text-left" width key="0"> Provider+Plan </th><th class="text-left" width key="1"> Xeon CPU </th><th class="text-left" width key="2"> RAM </th><th class="text-left" width key="3"> Storage </th><th class="text-left" width key="4"> Cost </th><th class="text-left" width key="5"> Location </th> </tr> </thead> <tbody> <tr key="row-0"> <td key="row-0-column-0" class="text-left"> RamNode 2GB OpenVZ </td><td key="row-0-column-1" class="text-left"> 2×E5-2630 </td><td key="row-0-column-2" class="text-left"> 2GB </td><td key="row-0-column-3" class="text-left"> 60GB SSD </td><td key="row-0-column-4" class="text-left"> $6.65/month* </td><td key="row-0-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-1"> <td key="row-1-column-0" class="text-left"> RamNode 2GB NVMe </td><td key="row-1-column-1" class="text-left"> 2×E3-1240 </td><td key="row-1-column-2" class="text-left"> 2GB </td><td key="row-1-column-3" class="text-left"> 25GB SSD </td><td key="row-1-column-4" class="text-left"> $12/month </td><td key="row-1-column-5" class="text-left"> Los Angeles, CA </td> </tr><tr key="row-2"> <td key="row-2-column-0" class="text-left"> SSD Nodes KVM / X-Large </td><td key="row-2-column-1" class="text-left"> 4×Skylake </td><td key="row-2-column-2" class="text-left"> 16GB </td><td key="row-2-column-3" class="text-left"> 80GB SSD </td><td key="row-2-column-4" class="text-left"> $9.99/month* </td><td key="row-2-column-5" class="text-left"> Dallas, TX </td> </tr><tr key="row-3"> <td key="row-3-column-0" class="text-left"> HostUS 4GB OpenVZ </td><td key="row-3-column-1" class="text-left"> 4×L5640 </td><td key="row-3-column-2" class="text-left"> 4GB </td><td key="row-3-column-3" class="text-left"> 150GB HDD </td><td key="row-3-column-4" class="text-left"> $9.56/month </td><td key="row-3-column-5" class="text-left"> Los Angeles, CA </td> </tr><tr key="row-4"> <td key="row-4-column-0" class="text-left"> Hyper Expert 12GB KVM </td><td key="row-4-column-1" class="text-left"> 8×E5-2670 </td><td key="row-4-column-2" class="text-left"> 12GB </td><td key="row-4-column-3" class="text-left"> 80GB SSD </td><td key="row-4-column-4" class="text-left"> $18.89/month*  </td><td key="row-4-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-5"> <td key="row-5-column-0" class="text-left"> Digital Ocean 2GB </td><td key="row-5-column-1" class="text-left"> 1×E5-2650 </td><td key="row-5-column-2" class="text-left"> 2GB </td><td key="row-5-column-3" class="text-left"> 50GB </td><td key="row-5-column-4" class="text-left"> $10/month </td><td key="row-5-column-5" class="text-left"> San Francisco, CA </td> </tr><tr key="row-6"> <td key="row-6-column-0" class="text-left"> Digital Ocean 4GB </td><td key="row-6-column-1" class="text-left"> 2×E5-2650 </td><td key="row-6-column-2" class="text-left"> 4GB </td><td key="row-6-column-3" class="text-left"> 80GB SSD </td><td key="row-6-column-4" class="text-left"> $20/month </td><td key="row-6-column-5" class="text-left"> San Francisco, CA </td> </tr><tr key="row-7"> <td key="row-7-column-0" class="text-left"> Linode 2GB </td><td key="row-7-column-1" class="text-left"> 1×Gold 6148 </td><td key="row-7-column-2" class="text-left"> 2GB </td><td key="row-7-column-3" class="text-left"> 50GB </td><td key="row-7-column-4" class="text-left"> $10/month </td><td key="row-7-column-5" class="text-left"> Fremont, CA </td> </tr><tr key="row-8"> <td key="row-8-column-0" class="text-left"> Linode 4GB </td><td key="row-8-column-1" class="text-left"> 2×E5-2697 </td><td key="row-8-column-2" class="text-left"> 4GB </td><td key="row-8-column-3" class="text-left"> 80GB </td><td key="row-8-column-4" class="text-left"> $20/month </td><td key="row-8-column-5" class="text-left"> Fremont, CA </td> </tr><tr key="row-9"> <td key="row-9-column-0" class="text-left"> Vultr 2GB </td><td key="row-9-column-1" class="text-left"> 1×Skylake </td><td key="row-9-column-2" class="text-left"> 2GB </td><td key="row-9-column-3" class="text-left"> 40GB </td><td key="row-9-column-4" class="text-left"> $10/month </td><td key="row-9-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-10"> <td key="row-10-column-0" class="text-left"> Vultr 4GB </td><td key="row-10-column-1" class="text-left"> 2×Skylake </td><td key="row-10-column-2" class="text-left"> 4GB </td><td key="row-10-column-3" class="text-left"> 60GB </td><td key="row-10-column-4" class="text-left"> $20/month </td><td key="row-10-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-11"> <td key="row-11-column-0" class="text-left"> AWS Lightsail 2GB </td><td key="row-11-column-1" class="text-left"> 1×E5-2676 </td><td key="row-11-column-2" class="text-left"> 2GB </td><td key="row-11-column-3" class="text-left"> 60GB </td><td key="row-11-column-4" class="text-left"> $10/month </td><td key="row-11-column-5" class="text-left"> Northern OR </td> </tr><tr key="row-12"> <td key="row-12-column-0" class="text-left"> AWS Lightsail 4GB </td><td key="row-12-column-1" class="text-left"> 2×E5-2676 </td><td key="row-12-column-2" class="text-left"> 4GB </td><td key="row-12-column-3" class="text-left"> 80GB </td><td key="row-12-column-4" class="text-left"> $20/month </td><td key="row-12-column-5" class="text-left"> Northern OR </td> </tr> </tbody> </table> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><aside>*: Pricing reflects publicly-available promotions or reduced pricing for prepayment. <a href="https://clientarea.ramnode.com/aff.php?aff=2496">RamNode</a>: 6-month term, <a href="https://www.ssdnodes.com/manage/aff.php?aff=686">SSD Nodes</a>: 1-year term, <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a>: 10% off.</aside><p>I profiled 23 different servers over the course of four weeks. I’ve omitted all kinds of gleeful and superfluous detail so it looks like it wasn’t just some obsessive spree. (It was.)</p></div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Contender Specs</h4> <figure class="chart-container"> <svg viewBox="0 0 810 856" width="810" height="856" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(203,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> $/Month </text>  <rect width="12" height="10" fill="#81c9bf" x="89" y="1"></rect> <text x="107" y="10" class="legend-label"> vCPU Cores </text>  <rect width="12" height="10" fill="#e16db3" x="202" y="1"></rect> <text x="220" y="10" class="legend-label"> RAM (GB) </text>  <rect width="12" height="10" fill="#e8cf3c" x="299" y="1"></rect> <text x="317" y="10" class="legend-label"> Storage (GB) </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="807" opacity="0.2"></rect><rect width="1" fill="#666" x="38%" y="30" height="807" opacity="0.2"></rect><rect width="1" fill="#666" x="50%" y="30" height="807" opacity="0.2"></rect><rect width="1" fill="#666" x="62%" y="30" height="807" opacity="0.2"></rect><rect width="1" fill="#666" x="73%" y="30" height="807" opacity="0.2"></rect><rect width="1" fill="#666" x="85%" y="30" height="807" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="807" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="67" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="129" class="label"> RamNode 2GB NVMe </text><text x="25%" y="191" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="253" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="315" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="377" class="label"> Digital Ocean 2GB </text><text x="25%" y="439" class="label"> Digital Ocean 4GB </text><text x="25%" y="501" class="label"> Linode 2GB </text><text x="25%" y="563" class="label"> Linode 4GB </text><text x="25%" y="625" class="label"> Vultr 2GB </text><text x="25%" y="687" class="label"> Vultr 4GB </text><text x="25%" y="749" class="label"> AWS Lightsail 2GB </text><text x="25%" y="811" class="label"> AWS Lightsail 4GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="851"> 0 </text><text class="interval-label" x="38%" y="851"> 25 </text><text class="interval-label" x="50%" y="851"> 50 </text><text class="interval-label" x="62%" y="851"> 75 </text><text class="interval-label" x="73%" y="851"> 100 </text><text class="interval-label" x="85%" y="851"> 125 </text><text class="interval-label" x="97%" y="851"> 150 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="3.1033333333333335%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="62" key="3"></rect><rect width="28%" height="12" fill="#e8cf3c" x="27%" y="75" key="4"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="92" opacity="0.2"></rect>  <rect width="5.6000000000000005%" height="12" fill="#00a1d5" x="27%" y="98" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="111" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="124" key="3"></rect><rect width="11.666666666666666%" height="12" fill="#e8cf3c" x="27%" y="137" key="4"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="154" opacity="0.2"></rect>  <rect width="4.662000000000001%" height="12" fill="#00a1d5" x="27%" y="160" key="1"></rect><rect width="1.8666666666666667%" height="12" fill="#81c9bf" x="27%" y="173" key="2"></rect><rect width="7.466666666666667%" height="12" fill="#e16db3" x="27%" y="186" key="3"></rect><rect width="37.333333333333336%" height="12" fill="#e8cf3c" x="27%" y="199" key="4"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="216" opacity="0.2"></rect>  <rect width="4.461333333333333%" height="12" fill="#00a1d5" x="27%" y="222" key="1"></rect><rect width="1.8666666666666667%" height="12" fill="#81c9bf" x="27%" y="235" key="2"></rect><rect width="1.8666666666666667%" height="12" fill="#e16db3" x="27%" y="248" key="3"></rect><rect width="70%" height="12" fill="#e8cf3c" x="27%" y="261" key="4"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="278" opacity="0.2"></rect>  <rect width="8.815333333333333%" height="12" fill="#00a1d5" x="27%" y="284" key="1"></rect><rect width="3.7333333333333334%" height="12" fill="#81c9bf" x="27%" y="297" key="2"></rect><rect width="5.6000000000000005%" height="12" fill="#e16db3" x="27%" y="310" key="3"></rect><rect width="37.333333333333336%" height="12" fill="#e8cf3c" x="27%" y="323" key="4"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="340" opacity="0.2"></rect>  <rect width="4.666666666666667%" height="12" fill="#00a1d5" x="27%" y="346" key="1"></rect><rect width="0.4666666666666667%" height="12" fill="#81c9bf" x="27%" y="359" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="372" key="3"></rect><rect width="23.333333333333332%" height="12" fill="#e8cf3c" x="27%" y="385" key="4"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="402" opacity="0.2"></rect>  <rect width="9.333333333333334%" height="12" fill="#00a1d5" x="27%" y="408" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="421" key="2"></rect><rect width="1.8666666666666667%" height="12" fill="#e16db3" x="27%" y="434" key="3"></rect><rect width="37.333333333333336%" height="12" fill="#e8cf3c" x="27%" y="447" key="4"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="464" opacity="0.2"></rect>  <rect width="4.666666666666667%" height="12" fill="#00a1d5" x="27%" y="470" key="1"></rect><rect width="0.4666666666666667%" height="12" fill="#81c9bf" x="27%" y="483" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="496" key="3"></rect><rect width="23.333333333333332%" height="12" fill="#e8cf3c" x="27%" y="509" key="4"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="526" opacity="0.2"></rect>  <rect width="9.333333333333334%" height="12" fill="#00a1d5" x="27%" y="532" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="545" key="2"></rect><rect width="1.8666666666666667%" height="12" fill="#e16db3" x="27%" y="558" key="3"></rect><rect width="37.333333333333336%" height="12" fill="#e8cf3c" x="27%" y="571" key="4"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="588" opacity="0.2"></rect>  <rect width="4.666666666666667%" height="12" fill="#00a1d5" x="27%" y="594" key="1"></rect><rect width="0.4666666666666667%" height="12" fill="#81c9bf" x="27%" y="607" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="620" key="3"></rect><rect width="18.666666666666668%" height="12" fill="#e8cf3c" x="27%" y="633" key="4"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="650" opacity="0.2"></rect>  <rect width="9.333333333333334%" height="12" fill="#00a1d5" x="27%" y="656" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="669" key="2"></rect><rect width="1.8666666666666667%" height="12" fill="#e16db3" x="27%" y="682" key="3"></rect><rect width="28%" height="12" fill="#e8cf3c" x="27%" y="695" key="4"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="712" opacity="0.2"></rect>  <rect width="4.666666666666667%" height="12" fill="#00a1d5" x="27%" y="718" key="1"></rect><rect width="0.4666666666666667%" height="12" fill="#81c9bf" x="27%" y="731" key="2"></rect><rect width="0.9333333333333333%" height="12" fill="#e16db3" x="27%" y="744" key="3"></rect><rect width="28%" height="12" fill="#e8cf3c" x="27%" y="757" key="4"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="774" opacity="0.2"></rect>  <rect width="9.333333333333334%" height="12" fill="#00a1d5" x="27%" y="780" key="1"></rect><rect width="0.9333333333333333%" height="12" fill="#81c9bf" x="27%" y="793" key="2"></rect><rect width="1.8666666666666667%" height="12" fill="#e16db3" x="27%" y="806" key="3"></rect><rect width="37.333333333333336%" height="12" fill="#e8cf3c" x="27%" y="819" key="4"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="836" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="30.60333333333333%" y="45.5" data-item-percent="0.044333333333333336"> $6.65 </text><text class="bar-label" x="28.433333333333334%" y="58.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.433333333333334%" y="71.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="55.5%" y="84.5" data-item-percent="0.4"> 60 GB </text><text class="bar-label" x="33.1%" y="107.5" data-item-percent="0.08"> $12 </text><text class="bar-label" x="28.433333333333334%" y="120.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="28.433333333333334%" y="133.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="39.166666666666664%" y="146.5" data-item-percent="0.16666666666666666"> 25 GB </text><text class="bar-label" x="32.162%" y="169.5" data-item-percent="0.0666"> $9.99 </text><text class="bar-label" x="29.366666666666667%" y="182.5" data-item-percent="0.02666666666666667"> 4 </text><text class="bar-label" x="34.96666666666667%" y="195.5" data-item-percent="0.10666666666666667"> 16 GB </text><text class="bar-label" x="64.83333333333334%" y="208.5" data-item-percent="0.5333333333333333"> 80 GB </text><text class="bar-label" x="31.961333333333332%" y="231.5" data-item-percent="0.06373333333333334"> $9.56 </text><text class="bar-label" x="29.366666666666667%" y="244.5" data-item-percent="0.02666666666666667"> 4 </text><text class="bar-label" x="29.366666666666667%" y="257.5" data-item-percent="0.02666666666666667"> 4 GB </text><text class="inverted-bar-label" x="96.5%" y="270.5" data-item-percent="1"> 150 GB </text><text class="bar-label" x="36.315333333333335%" y="293.5" data-item-percent="0.12593333333333334"> $18.89 </text><text class="bar-label" x="31.233333333333334%" y="306.5" data-item-percent="0.05333333333333334"> 8 </text><text class="bar-label" x="33.1%" y="319.5" data-item-percent="0.08"> 12 GB </text><text class="bar-label" x="64.83333333333334%" y="332.5" data-item-percent="0.5333333333333333"> 80 GB </text><text class="bar-label" x="32.16666666666667%" y="355.5" data-item-percent="0.06666666666666667"> $10 </text><text class="bar-label" x="27.966666666666665%" y="368.5" data-item-percent="0.006666666666666667"> 1 </text><text class="bar-label" x="28.433333333333334%" y="381.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="50.83333333333333%" y="394.5" data-item-percent="0.3333333333333333"> 50 GB </text><text class="bar-label" x="36.833333333333336%" y="417.5" data-item-percent="0.13333333333333333"> $20 </text><text class="bar-label" x="28.433333333333334%" y="430.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="29.366666666666667%" y="443.5" data-item-percent="0.02666666666666667"> 4 GB </text><text class="bar-label" x="64.83333333333334%" y="456.5" data-item-percent="0.5333333333333333"> 80 GB </text><text class="bar-label" x="32.16666666666667%" y="479.5" data-item-percent="0.06666666666666667"> $10 </text><text class="bar-label" x="27.966666666666665%" y="492.5" data-item-percent="0.006666666666666667"> 1 </text><text class="bar-label" x="28.433333333333334%" y="505.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="50.83333333333333%" y="518.5" data-item-percent="0.3333333333333333"> 50 GB </text><text class="bar-label" x="36.833333333333336%" y="541.5" data-item-percent="0.13333333333333333"> $20 </text><text class="bar-label" x="28.433333333333334%" y="554.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="29.366666666666667%" y="567.5" data-item-percent="0.02666666666666667"> 4 GB </text><text class="bar-label" x="64.83333333333334%" y="580.5" data-item-percent="0.5333333333333333"> 80 GB </text><text class="bar-label" x="32.16666666666667%" y="603.5" data-item-percent="0.06666666666666667"> $10 </text><text class="bar-label" x="27.966666666666665%" y="616.5" data-item-percent="0.006666666666666667"> 1 </text><text class="bar-label" x="28.433333333333334%" y="629.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="46.16666666666667%" y="642.5" data-item-percent="0.26666666666666666"> 40 GB </text><text class="bar-label" x="36.833333333333336%" y="665.5" data-item-percent="0.13333333333333333"> $20 </text><text class="bar-label" x="28.433333333333334%" y="678.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="29.366666666666667%" y="691.5" data-item-percent="0.02666666666666667"> 4 GB </text><text class="bar-label" x="55.5%" y="704.5" data-item-percent="0.4"> 60 GB </text><text class="bar-label" x="32.16666666666667%" y="727.5" data-item-percent="0.06666666666666667"> $10 </text><text class="bar-label" x="27.966666666666665%" y="740.5" data-item-percent="0.006666666666666667"> 1 </text><text class="bar-label" x="28.433333333333334%" y="753.5" data-item-percent="0.013333333333333334"> 2 GB </text><text class="bar-label" x="55.5%" y="766.5" data-item-percent="0.4"> 60 GB </text><text class="bar-label" x="36.833333333333336%" y="789.5" data-item-percent="0.13333333333333333"> $20 </text><text class="bar-label" x="28.433333333333334%" y="802.5" data-item-percent="0.013333333333333334"> 2 </text><text class="bar-label" x="29.366666666666667%" y="815.5" data-item-percent="0.02666666666666667"> 4 GB </text><text class="bar-label" x="64.83333333333334%" y="828.5" data-item-percent="0.5333333333333333"> 80 GB </text> </g> </svg> <figcaption> <p>The HostUS plan uses RAID-10 HDD, not SSD.</p>
 </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>OpenVZ vs. KVM</h3><p>The type of virtualization may not be hugely important for hosting a few PHP projects, but I think it could be important particularly for low-resource plans.</p><p>In each case, a physical server somewhere is being divided up for multiple customers. The more accounts a hosting company can get onto that hardware, the more money they make. We want them making money because we want them to keep existing, offering support, and having an interest in keeping things fast and stable. At the same time, fewer neighbors means you get more of that hardware to yourself as a customer. Better performance, increased stability, more power. The divvying up happens via virtualization, which often comes in two flavors: OpenVZ and KVM.</p><p>I’m not a virtualization expert, but my takeaway is that OpenVZ is a bit leaner and more limiting. A KVM VPS has you running your own kernel, meaning you can do more with it but you also pay the performance overhead of running the entire kernel. OpenVZ containers all share a kernel and will thus be more limited by a host. Your resources, however, are more devoted to applications and the business end of whatever you’re doing there.</p><p>Performance-wise, I don’t know that OpenVZ or KVM virtualization made a whole lot of difference. My only practical takeaway is that KVM is better for monitoring and can be tuned more flexibly; my OpenVZ servers didn’t report various I/O metrics to <a href="https://www.nginx.com/products/nginx-amplify/">nginx Amplify</a> or <a href="https://www.statuscake.com/?a_aid=5da50d966178f&a_bid=af013c39">StatusCake</a>, and Centmin Mod has more power to tune when it has more direct control over system settings.</p><h3>RamNode</h3><p><a href="https://clientarea.ramnode.com/aff.php?aff=2496">RamNode</a> is a small company with a seemingly good reputation, and in my experience extremely low latency and unmatched uptime. This is where I first compared OpenVZ and KVM servers with similar specs and got my first hands-on experience with the two virtualization types. I tried the (blazing fast) NVMe service from Los Angeles since all NVMe were out of stock in Seattle, but I’d put that in my top three if the stock replenished. I’m also curious about their VDS, but it’d break the $20 budget.</p><h3>SSD Nodes</h3><p>I found <a href="https://www.ssdnodes.com/manage/aff.php?aff=686">SSD Nodes</a> on <a href="https://lowendbox.com/">LowEndBox</a> and assumed it was a scam. After digging around skeptically, it seemed like the company actually delivered on fast, reliable servers at surprisingly low prices. Most reviewers claimed to see strong uptime and performance, and the company responded to overcrowding accusations by publishing a live display of load across all its servers. This and a generous trial period convinced me to give their XL plan a shot. </p><p>They apparently used to offer Seattle servers, and if those ever re-appear with similar pricing I’ll jump on one in a heartbeat.</p><p>So far, this $9.99/month server (prepaid one year) has vastly outperformed anything in the same price range. Support responses have been helpful and decent, taking maybe a few hours with medium priority. (I try not to abuse "urgent" flags, and I’ve only been testing the VPS I got.) I decided to keep it and am curious whether uptime and benchmarks will be as good a year later.</p><h3>HostUS</h3><p><a href="https://my.hostus.us/aff.php?aff=2294">HostUS</a> is another company I encountered on LowEndBox. My initial benchmarks were strong, and after some weird IO issues I ran another set and found read and write performance wheezing at 1-2 MB/s. I filed a ticket that spurred a pleasant support discussion and investigation, only to find that the ticket had been closed after this comment from support staff:</p><blockquote><p>We sometimes end up putting a blanket limit on IO in place per VM due to the level of abuse we see on our OpenVZ plans, this is the quickest and easiest method to mitigate larger issues at scale.</p></blockquote><p>While I did run two benchmarks that used a bunch of CPU, and while I understand resources aren’t dedicated, the limit of 1-2 MB/s observed over several tests is an issue. This is the only server I tested that had an outage and dropped occasional http requests, and I’m not feeling compelled to keep the account. For what it’s worth, I do like the customization HostUS made to the otherwise-standard control panel, which offers convenient bonuses like adding SSH keys and disabling root login from outside the VPS.</p><h3>Hyper Expert</h3><p>I found <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a>, once again on LowEndBox, toward the end of my search specifically for Seattle-based providers. I wasn’t sure exactly what I was getting when I signed up, but took advantage of a 10% off code and used it to customize a seemingly impressive VPS. I didn’t expect much, but it’s been my all-around favorite. Excellent performance, lowest load among its competition and friendly, responsive support. Its proximity and low latency make everything about the server feel fast. The company has a Discord server and strong reviews, and my one (low priority) support ticket received a fast and friendly answer in just under three hours on a Saturday.</p><p><em>And then there are the more established providers…</em></p><h3>Linode</h3><p>SliceHost and <a href="https://www.linode.com/?r=5be5cd6f81a5d7c5b6d8afe1546f4755470dc43f">Linode</a> were the first VPS providers I ever used. SliceHost was acquired by Rackspace, and Linode’s still going strong. The company is apparently one of Digital Ocean’s major competitors, and is the first of four "name brand" providers I decided to include for comparison.</p><h3>Digital Ocean</h3><p><a href="https://m.do.co/c/f1391f41e5e8">Digital Ocean</a> is lovely to work with and has been my choice for several client projects, but I don’t need instant provisioning or hourly billing and for now I’ll manage my own monitoring and backups. But Digital Ocean is a popular provider, so I figure it can’t hurt to compare my budget finds against an established host and much larger company.</p><h3>Vultr</h3><p>I’ve only ever poked around at <a href="https://www.vultr.com/?ref=7572777">Vultr</a>, which has a reputation for being fast that didn’t disappoint. Seattle is one of their datacenter options, which is a plus for me.</p><h3>AWS (+Lightsail)</h3><p>High on the rush of spinning up powerful servers just to benchmark them, I decided to throw in a beefy EC2 instance along with another from Lightsail, which is Amazon’s clever way of simplifying your experience with AWS while drawing you into its vast universe.</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Testing Method</h2><p>For each server, I installed CentOS 7 and Centmin Mod before running tests with <a href="https://serverscope.io/">ServerScope.io</a>, <a href="https://www.geekbench.com/">GeekBench</a>, and <a href="https://github.com/centminmod/centminmodbench/">Centminbench</a>. The metrics I’m sharing here are the one’s I’ve found most interesting or useful.</p><p>Geekbench and UnixBench scores are straightforward, the latter all coming from ServerScope’s tests.</p><p>MySQL performance measurements came from mysqlslap via Centminbench:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>-------------------------------------------</span></span>
<span class="line"><span>Running mysqlslap</span></span>
<span class="line"><span>-------------------------------------------</span></span>
<span class="line"><span></span></span>
<span class="line"><span>mysqlslap --auto-generate-sql --auto-generate-sql-add-autoincrement --auto-generate-sql-secondary-indexes=5 --number-int-cols=5 --number-char-cols=5 --number-of-queries=25000 --auto-generate-sql-unique-query-number=40 --auto-generate-sql-unique-write-number=40 --auto-generate-sql-write-number=1000 --concurrency=64 --iterations=10 --engine=myisam </span></span>
<span class="line"><span>Benchmark</span></span>
<span class="line"><span>	Running for engine myisam</span></span>
<span class="line"><span>	Average number of seconds to run all queries: 1.644 seconds</span></span>
<span class="line"><span>	Minimum number of seconds to run all queries: 1.379 seconds</span></span>
<span class="line"><span>	Maximum number of seconds to run all queries: 1.805 seconds</span></span>
<span class="line"><span>	Number of clients running queries: 64</span></span>
<span class="line"><span>	Average number of queries per client: 390</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>PHP performance measurements, again from Centminbench, are the average of three Zend/micro_bench.php runs:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>-------------------------------------------</span></span>
<span class="line"><span>Run PHP test Zend/micro_bench.php</span></span>
<span class="line"><span>-------------------------------------------</span></span>
<span class="line"><span></span></span>
<span class="line"><span>empty_loop         0.034</span></span>
<span class="line"><span>func()             0.089    0.055</span></span>
<span class="line"><span>undef_func()       0.096    0.062</span></span>
<span class="line"><span>int_func()         0.047    0.012</span></span>
<span class="line"><span>$x = self::$x      0.081    0.047</span></span>
<span class="line"><span>self::$x = 0       0.082    0.048</span></span>
<span class="line"><span>isset(self::$x)    0.079    0.044</span></span>
<span class="line"><span>empty(self::$x)    0.086    0.052</span></span>
<span class="line"><span>$x = Foo::$x       0.061    0.027</span></span>
<span class="line"><span>Foo::$x = 0        0.059    0.025</span></span>
<span class="line"><span>isset(Foo::$x)     0.060    0.026</span></span>
<span class="line"><span>empty(Foo::$x)     0.065    0.031</span></span>
<span class="line"><span>self::f()          0.109    0.075</span></span>
<span class="line"><span>Foo::f()           0.088    0.054</span></span>
<span class="line"><span>$x = $this->x      0.060    0.026</span></span>
<span class="line"><span>$this->x = 0       0.051    0.017</span></span>
<span class="line"><span>$this->x += 2      0.091    0.057</span></span>
<span class="line"><span>++$this->x         0.064    0.030</span></span>
<span class="line"><span>--$this->x         0.063    0.029</span></span>
<span class="line"><span>$this->x++         0.072    0.038</span></span>
<span class="line"><span>$this->x--         0.072    0.038</span></span>
<span class="line"><span>isset($this->x)    0.078    0.044</span></span>
<span class="line"><span>empty($this->x)    0.082    0.048</span></span>
<span class="line"><span>$this->f()         0.096    0.062</span></span>
<span class="line"><span>$x = Foo::TEST     0.083    0.049</span></span>
<span class="line"><span>new Foo()          0.204    0.170</span></span>
<span class="line"><span>$x = TEST          0.058    0.024</span></span>
<span class="line"><span>$x = $_GET         0.088    0.054</span></span>
<span class="line"><span>$x = $GLOBALS['v'] 0.131    0.097</span></span>
<span class="line"><span>$x = $hash['v']    0.089    0.055</span></span>
<span class="line"><span>$x = $str[0]       0.106    0.072</span></span>
<span class="line"><span>$x = $a ?: null    0.063    0.029</span></span>
<span class="line"><span>$x = $f ?: tmp     0.070    0.035</span></span>
<span class="line"><span>$x = $f ? $f : $a  0.057    0.023</span></span>
<span class="line"><span>$x = $f ? $f : tmp 0.062    0.028</span></span>
<span class="line"><span>------------------------</span></span>
<span class="line"><span>Total              2.776</span></span>
<span class="line"><span>real: 2.84s user: 2.80s sys: 0.02s cpu: 99% maxmem: 19740 KB cswaits: 2</span></span>
<span class="line"><span></span></span>
<span class="line"><span>empty_loop         0.032</span></span>
<span class="line"><span>func()             0.087    0.056</span></span>
<span class="line"><span>undef_func()       0.094    0.063</span></span>
<span class="line"><span>int_func()         0.045    0.014</span></span>
<span class="line"><span>$x = self::$x      0.078    0.047</span></span>
<span class="line"><span>self::$x = 0       0.079    0.047</span></span>
<span class="line"><span>isset(self::$x)    0.078    0.046</span></span>
<span class="line"><span>empty(self::$x)    0.084    0.052</span></span>
<span class="line"><span>$x = Foo::$x       0.058    0.026</span></span>
<span class="line"><span>Foo::$x = 0        0.058    0.027</span></span>
<span class="line"><span>isset(Foo::$x)     0.058    0.027</span></span>
<span class="line"><span>empty(Foo::$x)     0.065    0.033</span></span>
<span class="line"><span>self::f()          0.116    0.084</span></span>
<span class="line"><span>Foo::f()           0.089    0.057</span></span>
<span class="line"><span>$x = $this->x      0.056    0.024</span></span>
<span class="line"><span>$this->x = 0       0.048    0.016</span></span>
<span class="line"><span>$this->x += 2      0.084    0.052</span></span>
<span class="line"><span>++$this->x         0.060    0.028</span></span>
<span class="line"><span>--$this->x         0.059    0.027</span></span>
<span class="line"><span>$this->x++         0.064    0.033</span></span>
<span class="line"><span>$this->x--         0.065    0.033</span></span>
<span class="line"><span>isset($this->x)    0.074    0.043</span></span>
<span class="line"><span>empty($this->x)    0.079    0.047</span></span>
<span class="line"><span>$this->f()         0.093    0.061</span></span>
<span class="line"><span>$x = Foo::TEST     0.078    0.047</span></span>
<span class="line"><span>new Foo()          0.209    0.177</span></span>
<span class="line"><span>$x = TEST          0.053    0.022</span></span>
<span class="line"><span>$x = $_GET         0.082    0.050</span></span>
<span class="line"><span>$x = $GLOBALS['v'] 0.117    0.085</span></span>
<span class="line"><span>$x = $hash['v']    0.087    0.055</span></span>
<span class="line"><span>$x = $str[0]       0.077    0.045</span></span>
<span class="line"><span>$x = $a ?: null    0.062    0.031</span></span>
<span class="line"><span>$x = $f ?: tmp     0.067    0.036</span></span>
<span class="line"><span>$x = $f ? $f : $a  0.057    0.025</span></span>
<span class="line"><span>$x = $f ? $f : tmp 0.061    0.030</span></span>
<span class="line"><span>------------------------</span></span>
<span class="line"><span>Total              2.655</span></span>
<span class="line"><span>real: 2.74s user: 2.70s sys: 0.03s cpu: 99% maxmem: 19740 KB cswaits: 1</span></span>
<span class="line"><span></span></span>
<span class="line"><span>empty_loop         0.032</span></span>
<span class="line"><span>func()             0.089    0.057</span></span>
<span class="line"><span>undef_func()       0.096    0.064</span></span>
<span class="line"><span>int_func()         0.046    0.013</span></span>
<span class="line"><span>$x = self::$x      0.079    0.046</span></span>
<span class="line"><span>self::$x = 0       0.078    0.045</span></span>
<span class="line"><span>isset(self::$x)    0.077    0.045</span></span>
<span class="line"><span>empty(self::$x)    0.083    0.051</span></span>
<span class="line"><span>$x = Foo::$x       0.060    0.027</span></span>
<span class="line"><span>Foo::$x = 0        0.058    0.026</span></span>
<span class="line"><span>isset(Foo::$x)     0.058    0.025</span></span>
<span class="line"><span>empty(Foo::$x)     0.064    0.032</span></span>
<span class="line"><span>self::f()          0.108    0.075</span></span>
<span class="line"><span>Foo::f()           0.087    0.054</span></span>
<span class="line"><span>$x = $this->x      0.057    0.025</span></span>
<span class="line"><span>$this->x = 0       0.049    0.016</span></span>
<span class="line"><span>$this->x += 2      0.085    0.053</span></span>
<span class="line"><span>++$this->x         0.062    0.029</span></span>
<span class="line"><span>--$this->x         0.060    0.027</span></span>
<span class="line"><span>$this->x++         0.067    0.034</span></span>
<span class="line"><span>$this->x--         0.067    0.034</span></span>
<span class="line"><span>isset($this->x)    0.077    0.045</span></span>
<span class="line"><span>empty($this->x)    0.080    0.048</span></span>
<span class="line"><span>$this->f()         0.094    0.061</span></span>
<span class="line"><span>$x = Foo::TEST     0.079    0.046</span></span>
<span class="line"><span>new Foo()          0.197    0.165</span></span>
<span class="line"><span>$x = TEST          0.054    0.021</span></span>
<span class="line"><span>$x = $_GET         0.081    0.049</span></span>
<span class="line"><span>$x = $GLOBALS['v'] 0.134    0.101</span></span>
<span class="line"><span>$x = $hash['v']    0.089    0.056</span></span>
<span class="line"><span>$x = $str[0]       0.077    0.044</span></span>
<span class="line"><span>$x = $a ?: null    0.063    0.031</span></span>
<span class="line"><span>$x = $f ?: tmp     0.067    0.035</span></span>
<span class="line"><span>$x = $f ? $f : $a  0.057    0.024</span></span>
<span class="line"><span>$x = $f ? $f : tmp 0.062    0.029</span></span>
<span class="line"><span>------------------------</span></span>
<span class="line"><span>Total              2.672</span></span>
<span class="line"><span>real: 2.71s user: 2.69s sys: 0.01s cpu: 99% maxmem: 19740 KB cswaits: 1</span></span>
<span class="line"><span></span></span>
<span class="line"><span>micro_bench.php results from 3 runs</span></span>
<span class="line"><span>2.776</span></span>
<span class="line"><span>2.655</span></span>
<span class="line"><span>2.672</span></span>
<span class="line"><span></span></span>
<span class="line"><span>micro_bench.php avg: 2.7010</span></span>
<span class="line"><span>Avg: real: 2.76s user: 2.73s sys: 0.02s cpu: 99.00% maxmem: 19740.00KB cswaits: 1.33</span></span>
<span class="line"><span>created results log at /home/phpbench_logs/bench_micro_291018-190520.log</span></span>
<span class="line"><span>server PHP info log at /home/phpbench_logs/bench_phpinfo_291018-190520.log</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I opted to use a very simple bandwidth benchmark, which is Centminbench’s Cachefly download. The file itself should be close because of the CDN:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>Download from Cachefly (http://cachefly.cachefly.net/100mb.test)</span></span>
<span class="line"><span>Download Cachefly: 120MB/s</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Disk read/write came from Centminbench’s fio benchmarks:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>Running FIO benchmark...</span></span>
<span class="line"><span></span></span>
<span class="line"><span>FIO_VERSION = fio-2.0.9</span></span>
<span class="line"><span></span></span>
<span class="line"><span>FIO random reads: </span></span>
<span class="line"><span>randomreads: (g=0): rw=randread, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=64</span></span>
<span class="line"><span>fio-2.0.9</span></span>
<span class="line"><span>Starting 1 process</span></span>
<span class="line"><span>randomreads: Laying out IO file(s) (1 file(s) / 1024MB)</span></span>
<span class="line"><span></span></span>
<span class="line"><span>randomreads: (groupid=0, jobs=1): err= 0: pid=32279: Mon Oct 29 19:06:42 2018</span></span>
<span class="line"><span>  read : io=1024.3MB, bw=164008KB/s, iops=41001 , runt=  6395msec</span></span>
<span class="line"><span>  cpu          : usr=4.69%, sys=20.24%, ctx=46236, majf=0, minf=85</span></span>
<span class="line"><span>  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%</span></span>
<span class="line"><span>     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%</span></span>
<span class="line"><span>     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%</span></span>
<span class="line"><span>     issued    : total=r=262207/w=0/d=0, short=r=0/w=0/d=0</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Run status group 0 (all jobs):</span></span>
<span class="line"><span>   READ: io=1024.3MB, aggrb=164007KB/s, minb=164007KB/s, maxb=164007KB/s, mint=6395msec, maxt=6395msec</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Disk stats (read/write):</span></span>
<span class="line"><span>  vda: ios=253783/2, merge=0/1, ticks=380446/9, in_queue=380417, util=98.45%</span></span>
<span class="line"><span></span></span>
<span class="line"><span>FIO random writes: </span></span>
<span class="line"><span>randomwrites: (g=0): rw=randwrite, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=64</span></span>
<span class="line"><span>fio-2.0.9</span></span>
<span class="line"><span>Starting 1 process</span></span>
<span class="line"><span></span></span>
<span class="line"><span>randomwrites: (groupid=0, jobs=1): err= 0: pid=32283: Mon Oct 29 19:06:50 2018</span></span>
<span class="line"><span>  write: io=1024.3MB, bw=148854KB/s, iops=37213 , runt=  7046msec</span></span>
<span class="line"><span>  cpu          : usr=4.95%, sys=20.55%, ctx=24538, majf=0, minf=20</span></span>
<span class="line"><span>  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%</span></span>
<span class="line"><span>     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%</span></span>
<span class="line"><span>     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%</span></span>
<span class="line"><span>     issued    : total=r=0/w=262207/d=0, short=r=0/w=0/d=0</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Run status group 0 (all jobs):</span></span>
<span class="line"><span>  WRITE: io=1024.3MB, aggrb=148854KB/s, minb=148854KB/s, maxb=148854KB/s, mint=7046msec, maxt=7046msec</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Disk stats (read/write):</span></span>
<span class="line"><span>  vda: ios=0/257522, merge=0/3, ticks=0/422346, in_queue=422340, util=98.67%</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ll be monitoring my finalists with <a href="https://www.statuscake.com/?a_aid=5da50d966178f&a_bid=af013c39">StatusCake</a> and <a href="https://www.nginx.com/products/nginx-amplify/">nginx Amplify</a> to see how they behave longer-term.</p><h2>Cheap VPS Results</h2></div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Geekbench Multi-Core + UnixBench Scores</h4> <figure class="chart-container"> <svg viewBox="0 0 810 518" width="810" height="518" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(262,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> Geekbench Multi-Core </text>  <rect width="12" height="10" fill="#81c9bf" x="193" y="1"></rect> <text x="211" y="10" class="legend-label"> UnixBench </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="469" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="90" class="label"> RamNode 2GB NVMe </text><text x="25%" y="126" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="162" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="198" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="234" class="label"> Digital Ocean 2GB </text><text x="25%" y="270" class="label"> Digital Ocean 4GB </text><text x="25%" y="306" class="label"> Linode 2GB </text><text x="25%" y="342" class="label"> Linode 4GB </text><text x="25%" y="378" class="label"> Vultr 2GB </text><text x="25%" y="414" class="label"> Vultr 4GB </text><text x="25%" y="450" class="label"> AWS Lightsail 2GB </text><text x="25%" y="486" class="label"> AWS Lightsail 4GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="513"> 0 </text><text class="interval-label" x="41%" y="513"> 2800 </text><text class="interval-label" x="55%" y="513"> 5600 </text><text class="interval-label" x="69%" y="513"> 8400 </text><text class="interval-label" x="83%" y="513"> 11200 </text><text class="interval-label" x="97%" y="513"> 14000 </text><text class="interval-label" x="111%" y="513"> 16800 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="14.370000000000001%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="3.1125%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="39.63%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="8.593%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="40.025%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="8.4215%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="31.32%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="3.5610000000000004%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="69.47%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="17.576999999999998%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="13.45%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="3.5135%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="27.189999999999998%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="7.4079999999999995%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="16.605%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="4.298500000000001%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect>  <rect width="22.975%" height="12" fill="#00a1d5" x="27%" y="324" key="1"></rect><rect width="7.7175%" height="12" fill="#81c9bf" x="27%" y="337" key="2"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="354" opacity="0.2"></rect>  <rect width="18.82%" height="12" fill="#00a1d5" x="27%" y="360" key="1"></rect><rect width="5.6770000000000005%" height="12" fill="#81c9bf" x="27%" y="373" key="2"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="390" opacity="0.2"></rect>  <rect width="37.175%" height="12" fill="#00a1d5" x="27%" y="396" key="1"></rect><rect width="10.032%" height="12" fill="#81c9bf" x="27%" y="409" key="2"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="426" opacity="0.2"></rect>  <rect width="15.49%" height="12" fill="#00a1d5" x="27%" y="432" key="1"></rect><rect width="3.74%" height="12" fill="#81c9bf" x="27%" y="445" key="2"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="462" opacity="0.2"></rect>  <rect width="29.39%" height="12" fill="#00a1d5" x="27%" y="468" key="1"></rect><rect width="7.4510000000000005%" height="12" fill="#81c9bf" x="27%" y="481" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="498" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="41.870000000000005%" y="45.5" data-item-percent="0.2052857142857143"> 2874 </text><text class="bar-label" x="30.6125%" y="58.5" data-item-percent="0.04446428571428571"> 622.5 </text><text class="bar-label" x="67.13%" y="81.5" data-item-percent="0.5661428571428572"> 7926 </text><text class="bar-label" x="36.093%" y="94.5" data-item-percent="0.12275714285714286"> 1718.6 </text><text class="bar-label" x="67.525%" y="117.5" data-item-percent="0.5717857142857142"> 8005 </text><text class="bar-label" x="35.9215%" y="130.5" data-item-percent="0.12030714285714285"> 1684.3 </text><text class="bar-label" x="58.82%" y="153.5" data-item-percent="0.44742857142857145"> 6264 </text><text class="bar-label" x="31.061%" y="166.5" data-item-percent="0.050871428571428574"> 712.2 </text><text class="inverted-bar-label" x="95.97%" y="189.5" data-item-percent="0.9924285714285714"> 13894 </text><text class="bar-label" x="45.077%" y="202.5" data-item-percent="0.2511"> 3515.4 </text><text class="bar-label" x="40.95%" y="225.5" data-item-percent="0.19214285714285714"> 2690 </text><text class="bar-label" x="31.0135%" y="238.5" data-item-percent="0.05019285714285714"> 702.7 </text><text class="bar-label" x="54.69%" y="261.5" data-item-percent="0.3884285714285714"> 5438 </text><text class="bar-label" x="34.908%" y="274.5" data-item-percent="0.10582857142857142"> 1481.6 </text><text class="bar-label" x="44.105000000000004%" y="297.5" data-item-percent="0.2372142857142857"> 3321 </text><text class="bar-label" x="31.7985%" y="310.5" data-item-percent="0.06140714285714286"> 859.7 </text><text class="bar-label" x="50.475%" y="333.5" data-item-percent="0.32821428571428574"> 4595 </text><text class="bar-label" x="35.2175%" y="346.5" data-item-percent="0.11025"> 1543.5 </text><text class="bar-label" x="46.32%" y="369.5" data-item-percent="0.26885714285714285"> 3764 </text><text class="bar-label" x="33.177%" y="382.5" data-item-percent="0.0811"> 1135.4 </text><text class="bar-label" x="64.675%" y="405.5" data-item-percent="0.5310714285714285"> 7435 </text><text class="bar-label" x="37.532%" y="418.5" data-item-percent="0.14331428571428573"> 2006.4 </text><text class="bar-label" x="42.99%" y="441.5" data-item-percent="0.22128571428571428"> 3098 </text><text class="bar-label" x="31.240000000000002%" y="454.5" data-item-percent="0.05342857142857143"> 748 </text><text class="bar-label" x="56.89%" y="477.5" data-item-percent="0.4198571428571429"> 5878 </text><text class="bar-label" x="34.951%" y="490.5" data-item-percent="0.10644285714285714"> 1490.2 </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">PHP + MySQL Performance</h4> <figure class="chart-container"> <svg viewBox="0 0 810 518" width="810" height="518" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(346,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> PHP </text>  <rect width="12" height="10" fill="#81c9bf" x="57" y="1"></rect> <text x="75" y="10" class="legend-label"> MySQL </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="38%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="50%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="62%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="73%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="85%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="469" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="90" class="label"> RamNode 2GB NVMe </text><text x="25%" y="126" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="162" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="198" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="234" class="label"> Digital Ocean 2GB </text><text x="25%" y="270" class="label"> Digital Ocean 4GB </text><text x="25%" y="306" class="label"> Linode 2GB </text><text x="25%" y="342" class="label"> Linode 4GB </text><text x="25%" y="378" class="label"> Vultr 2GB </text><text x="25%" y="414" class="label"> Vultr 4GB </text><text x="25%" y="450" class="label"> AWS Lightsail 2GB </text><text x="25%" y="486" class="label"> AWS Lightsail 4GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="513"> 0 </text><text class="interval-label" x="38%" y="513"> 5 </text><text class="interval-label" x="50%" y="513"> 10 </text><text class="interval-label" x="62%" y="513"> 15 </text><text class="interval-label" x="73%" y="513"> 20 </text><text class="interval-label" x="85%" y="513"> 25 </text><text class="interval-label" x="97%" y="513"> 30 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="55.239333333333335%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="11.085666666666668%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="5.683999999999999%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="4.764666666666667%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="8.786633333333333%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="7.9239999999999995%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="10.811033333333334%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="6.549666666666666%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="9.183300000000001%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="4.496333333333333%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="10.627633333333334%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="9.909666666666666%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="9.3303%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="5.658333333333333%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="8.239%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="6.246333333333333%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect>  <rect width="10.8577%" height="12" fill="#00a1d5" x="27%" y="324" key="1"></rect><rect width="5.254666666666666%" height="12" fill="#81c9bf" x="27%" y="337" key="2"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="354" opacity="0.2"></rect>  <rect width="6.061999999999999%" height="12" fill="#00a1d5" x="27%" y="360" key="1"></rect><rect width="6.241666666666666%" height="12" fill="#81c9bf" x="27%" y="373" key="2"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="390" opacity="0.2"></rect>  <rect width="6.4360333333333335%" height="12" fill="#00a1d5" x="27%" y="396" key="1"></rect><rect width="3.0286666666666666%" height="12" fill="#81c9bf" x="27%" y="409" key="2"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="426" opacity="0.2"></rect>  <rect width="8.354966666666668%" height="12" fill="#00a1d5" x="27%" y="432" key="1"></rect><rect width="6.743333333333334%" height="12" fill="#81c9bf" x="27%" y="445" key="2"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="462" opacity="0.2"></rect>  <rect width="8.6583%" height="12" fill="#00a1d5" x="27%" y="468" key="1"></rect><rect width="3.8850000000000002%" height="12" fill="#81c9bf" x="27%" y="481" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="498" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="82.73933333333333%" y="45.5" data-item-percent="0.7891333333333334"> 23.674s </text><text class="bar-label" x="38.58566666666667%" y="58.5" data-item-percent="0.15836666666666668"> 4.751s </text><text class="bar-label" x="33.184%" y="81.5" data-item-percent="0.0812"> 2.436s </text><text class="bar-label" x="32.26466666666667%" y="94.5" data-item-percent="0.06806666666666666"> 2.042s </text><text class="bar-label" x="36.286633333333334%" y="117.5" data-item-percent="0.12552333333333332"> 3.7657s </text><text class="bar-label" x="35.424%" y="130.5" data-item-percent="0.1132"> 3.396s </text><text class="bar-label" x="38.311033333333334%" y="153.5" data-item-percent="0.15444333333333335"> 4.6333s </text><text class="bar-label" x="34.04966666666667%" y="166.5" data-item-percent="0.09356666666666666"> 2.807s </text><text class="bar-label" x="36.6833%" y="189.5" data-item-percent="0.13119"> 3.9357s </text><text class="bar-label" x="31.996333333333332%" y="202.5" data-item-percent="0.06423333333333334"> 1.927s </text><text class="bar-label" x="38.127633333333335%" y="225.5" data-item-percent="0.15182333333333334"> 4.5547s </text><text class="bar-label" x="37.409666666666666%" y="238.5" data-item-percent="0.14156666666666667"> 4.247s </text><text class="bar-label" x="36.8303%" y="261.5" data-item-percent="0.13329"> 3.9987s </text><text class="bar-label" x="33.15833333333333%" y="274.5" data-item-percent="0.08083333333333333"> 2.425s </text><text class="bar-label" x="35.739000000000004%" y="297.5" data-item-percent="0.1177"> 3.531s </text><text class="bar-label" x="33.74633333333333%" y="310.5" data-item-percent="0.08923333333333333"> 2.677s </text><text class="bar-label" x="38.3577%" y="333.5" data-item-percent="0.15511"> 4.6533s </text><text class="bar-label" x="32.754666666666665%" y="346.5" data-item-percent="0.07506666666666666"> 2.252s </text><text class="bar-label" x="33.562%" y="369.5" data-item-percent="0.0866"> 2.598s </text><text class="bar-label" x="33.74166666666667%" y="382.5" data-item-percent="0.08916666666666666"> 2.675s </text><text class="bar-label" x="33.936033333333334%" y="405.5" data-item-percent="0.09194333333333334"> 2.7583s </text><text class="bar-label" x="30.528666666666666%" y="418.5" data-item-percent="0.04326666666666667"> 1.298s </text><text class="bar-label" x="35.85496666666667%" y="441.5" data-item-percent="0.11935666666666668"> 3.5807s </text><text class="bar-label" x="34.24333333333333%" y="454.5" data-item-percent="0.09633333333333334"> 2.89s </text><text class="bar-label" x="36.1583%" y="477.5" data-item-percent="0.12369000000000001"> 3.7107s </text><text class="bar-label" x="31.385%" y="490.5" data-item-percent="0.0555"> 1.665s </text> </g> </svg> <figcaption> <p>Lower is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Storage I/O</h4> <figure class="chart-container"> <svg viewBox="0 0 810 518" width="810" height="518" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(246,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> Random Read MB/s </text>  <rect width="12" height="10" fill="#81c9bf" x="161" y="1"></rect> <text x="179" y="10" class="legend-label"> Random Write MB/s </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="469" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="469" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="90" class="label"> RamNode 2GB NVMe </text><text x="25%" y="126" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="162" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="198" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="234" class="label"> Digital Ocean 2GB </text><text x="25%" y="270" class="label"> Digital Ocean 4GB </text><text x="25%" y="306" class="label"> Linode 2GB </text><text x="25%" y="342" class="label"> Linode 4GB </text><text x="25%" y="378" class="label"> Vultr 2GB </text><text x="25%" y="414" class="label"> Vultr 4GB </text><text x="25%" y="450" class="label"> AWS Lightsail 2GB </text><text x="25%" y="486" class="label"> AWS Lightsail 4GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="513"> 0 </text><text class="interval-label" x="41%" y="513"> 200 </text><text class="interval-label" x="55%" y="513"> 400 </text><text class="interval-label" x="69%" y="513"> 600 </text><text class="interval-label" x="83%" y="513"> 800 </text><text class="interval-label" x="97%" y="513"> 1000 </text><text class="interval-label" x="111%" y="513"> 1200 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="4.26797%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="23.906889999999997%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="34.02133%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="30.451259999999998%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="12.02586%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="6.854470000000001%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="20.439300000000003%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="40.229000000000006%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="22.014369999999996%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="26.45692%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="22.49324%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="7.86821%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="27.67359%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="13.68465%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="34.61479%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="21.52388%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect>  <rect width="23.336949999999998%" height="12" fill="#00a1d5" x="27%" y="324" key="1"></rect><rect width="9.0216%" height="12" fill="#81c9bf" x="27%" y="337" key="2"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="354" opacity="0.2"></rect>  <rect width="11.18838%" height="12" fill="#00a1d5" x="27%" y="360" key="1"></rect><rect width="11.179839999999999%" height="12" fill="#81c9bf" x="27%" y="373" key="2"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="390" opacity="0.2"></rect>  <rect width="14.129710000000001%" height="12" fill="#00a1d5" x="27%" y="396" key="1"></rect><rect width="13.412139999999999%" height="12" fill="#81c9bf" x="27%" y="409" key="2"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="426" opacity="0.2"></rect>  <rect width="0.8668800000000001%" height="12" fill="#00a1d5" x="27%" y="432" key="1"></rect><rect width="0.86037%" height="12" fill="#81c9bf" x="27%" y="445" key="2"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="462" opacity="0.2"></rect>  <rect width="0.8666699999999999%" height="12" fill="#00a1d5" x="27%" y="468" key="1"></rect><rect width="0.85974%" height="12" fill="#81c9bf" x="27%" y="481" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="498" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="31.76797%" y="45.5" data-item-percent="0.060971"> 60.971 MB/s </text><text class="bar-label" x="51.40689%" y="58.5" data-item-percent="0.34152699999999997"> 341.527 MB/s </text><text class="bar-label" x="61.52133%" y="81.5" data-item-percent="0.486019"> 486.019 MB/s </text><text class="bar-label" x="57.95126%" y="94.5" data-item-percent="0.43501799999999996"> 435.018 MB/s </text><text class="bar-label" x="39.52586%" y="117.5" data-item-percent="0.171798"> 171.798 MB/s </text><text class="bar-label" x="34.35447%" y="130.5" data-item-percent="0.09792100000000001"> 97.921 MB/s </text><text class="bar-label" x="47.9393%" y="153.5" data-item-percent="0.29199"> 291.99 MB/s </text><text class="bar-label" x="67.72900000000001%" y="166.5" data-item-percent="0.5747000000000001"> 574.7 MB/s </text><text class="bar-label" x="49.51437%" y="189.5" data-item-percent="0.31449099999999997"> 314.491 MB/s </text><text class="bar-label" x="53.95692%" y="202.5" data-item-percent="0.377956"> 377.956 MB/s </text><text class="bar-label" x="49.99324%" y="225.5" data-item-percent="0.321332"> 321.332 MB/s </text><text class="bar-label" x="35.36821%" y="238.5" data-item-percent="0.112403"> 112.403 MB/s </text><text class="bar-label" x="55.173590000000004%" y="261.5" data-item-percent="0.395337"> 395.337 MB/s </text><text class="bar-label" x="41.18465%" y="274.5" data-item-percent="0.195495"> 195.495 MB/s </text><text class="bar-label" x="62.11479%" y="297.5" data-item-percent="0.494497"> 494.497 MB/s </text><text class="bar-label" x="49.02388%" y="310.5" data-item-percent="0.307484"> 307.484 MB/s </text><text class="bar-label" x="50.83695%" y="333.5" data-item-percent="0.333385"> 333.385 MB/s </text><text class="bar-label" x="36.5216%" y="346.5" data-item-percent="0.12888"> 128.880 MB/s </text><text class="bar-label" x="38.68838%" y="369.5" data-item-percent="0.159834"> 159.834 MB/s </text><text class="bar-label" x="38.67984%" y="382.5" data-item-percent="0.159712"> 159.712 MB/s </text><text class="bar-label" x="41.62971%" y="405.5" data-item-percent="0.201853"> 201.853 MB/s </text><text class="bar-label" x="40.91214%" y="418.5" data-item-percent="0.191602"> 191.602 MB/s </text><text class="bar-label" x="28.366880000000002%" y="441.5" data-item-percent="0.012384000000000001"> 12.384 MB/s </text><text class="bar-label" x="28.36037%" y="454.5" data-item-percent="0.012291"> 12.291 MB/s </text><text class="bar-label" x="28.36667%" y="477.5" data-item-percent="0.012381"> 12.381 MB/s </text><text class="bar-label" x="28.35974%" y="490.5" data-item-percent="0.012282"> 12.282 MB/s </text> </g> </svg>  </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Bandwidth</h4> <figure class="chart-container"> <svg viewBox="0 0 810 397" width="810" height="397" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="378" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="0" height="378" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="0" height="378" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="0" height="378" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="0" height="378" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="378" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="0" height="378" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="49.5" class="label"> RamNode 2GB NVMe </text><text x="25%" y="78.5" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="107.5" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="136.5" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="165.5" class="label"> Digital Ocean 2GB </text><text x="25%" y="194.5" class="label"> Digital Ocean 4GB </text><text x="25%" y="223.5" class="label"> Linode 2GB </text><text x="25%" y="252.5" class="label"> Linode 4GB </text><text x="25%" y="281.5" class="label"> Vultr 2GB </text><text x="25%" y="310.5" class="label"> Vultr 4GB </text><text x="25%" y="339.5" class="label"> AWS Lightsail 2GB </text><text x="25%" y="368.5" class="label"> AWS Lightsail 4GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="392"> 0 </text><text class="interval-label" x="41%" y="392"> 50 </text><text class="interval-label" x="55%" y="392"> 100 </text><text class="interval-label" x="69%" y="392"> 150 </text><text class="interval-label" x="83%" y="392"> 200 </text><text class="interval-label" x="97%" y="392"> 250 </text><text class="interval-label" x="111%" y="392"> 300 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="5.012%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="24.192%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="28.84%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="10.024%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="29.4%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="67.75999999999999%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="38.36%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="49.839999999999996%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect>  <rect width="36.4%" height="18" fill="#00a1d5" x="27%" y="238" key="1"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="261" opacity="0.2"></rect>  <rect width="38.64%" height="18" fill="#00a1d5" x="27%" y="267" key="1"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="290" opacity="0.2"></rect>  <rect width="39.2%" height="18" fill="#00a1d5" x="27%" y="296" key="1"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="319" opacity="0.2"></rect>  <rect width="19.152%" height="18" fill="#00a1d5" x="27%" y="325" key="1"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="348" opacity="0.2"></rect>  <rect width="21.028%" height="18" fill="#00a1d5" x="27%" y="354" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="377" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="32.512%" y="18.56" data-item-percent="0.0716"> 17.9 MB/s </text><text class="bar-label" x="51.692%" y="47.56" data-item-percent="0.3456"> 86.4 MB/s </text><text class="bar-label" x="56.34%" y="76.56" data-item-percent="0.412"> 103 MB/s </text><text class="bar-label" x="37.524%" y="105.56" data-item-percent="0.1432"> 35.8 MB/s </text><text class="bar-label" x="56.9%" y="134.56" data-item-percent="0.42"> 105 MB/s </text><text class="inverted-bar-label" x="94.25999999999999%" y="163.56" data-item-percent="0.968"> 242 MB/s </text><text class="bar-label" x="65.86%" y="192.56" data-item-percent="0.548"> 137 MB/s </text><text class="bar-label" x="77.34%" y="221.56" data-item-percent="0.712"> 178 MB/s </text><text class="bar-label" x="63.9%" y="250.56" data-item-percent="0.52"> 130 MB/s </text><text class="bar-label" x="66.14%" y="279.56" data-item-percent="0.552"> 138 MB/s </text><text class="bar-label" x="66.7%" y="308.56" data-item-percent="0.56"> 140 MB/s </text><text class="bar-label" x="46.652%" y="337.56" data-item-percent="0.2736"> 68.4 MB/s </text><text class="bar-label" x="48.528%" y="366.56" data-item-percent="0.3004"> 75.1 MB/s </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Performance Competitors</h2><p>Thrilled with the <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a> VPS performance, I decided I’d try a few more plans to compete on directly performance instead of price.</p><p>Here are the additional higher-performance plans:</p></div><div class="table-block pl-6 copy overflow-x-auto max-w-full lg:mx-auto lg:pl-0 lg:max-w-lg"> <table> <thead> <tr> <th class="text-left" width key="0"> Provider+Plan </th><th class="text-left" width key="1"> Xeon CPU </th><th class="text-left" width key="2"> RAM </th><th class="text-left" width key="3"> Storage </th><th class="text-left" width key="4"> Cost </th><th class="text-left" width key="5"> Location </th> </tr> </thead> <tbody> <tr key="row-0"> <td key="row-0-column-0" class="text-left"> Vultr 32GB </td><td key="row-0-column-1" class="text-left"> 8×Skylake </td><td key="row-0-column-2" class="text-left"> 32GB </td><td key="row-0-column-3" class="text-left"> 300GB </td><td key="row-0-column-4" class="text-left"> $160/month </td><td key="row-0-column-5" class="text-left"> Seattle, WA </td> </tr><tr key="row-1"> <td key="row-1-column-0" class="text-left"> Digital Ocean 16GB CPU </td><td key="row-1-column-1" class="text-left"> 8×Platinum 8168 </td><td key="row-1-column-2" class="text-left"> 16GB </td><td key="row-1-column-3" class="text-left"> 100GB SSD </td><td key="row-1-column-4" class="text-left"> $160/month </td><td key="row-1-column-5" class="text-left"> San Francisco, CA </td> </tr><tr key="row-2"> <td key="row-2-column-0" class="text-left"> AWS m5.2xlarge </td><td key="row-2-column-1" class="text-left"> 8×Skylake </td><td key="row-2-column-2" class="text-left"> 32GB </td><td key="row-2-column-3" class="text-left"> - </td><td key="row-2-column-4" class="text-left"> $262.08/month </td><td key="row-2-column-5" class="text-left"> Northern OR </td> </tr><tr key="row-3"> <td key="row-3-column-0" class="text-left"> Digital Ocean 32GB </td><td key="row-3-column-1" class="text-left"> 8×Gold 6140 </td><td key="row-3-column-2" class="text-left"> 32GB </td><td key="row-3-column-3" class="text-left"> 640GB SSD </td><td key="row-3-column-4" class="text-left"> $160/month </td><td key="row-3-column-5" class="text-left"> San Francisco, CA </td> </tr><tr key="row-4"> <td key="row-4-column-0" class="text-left"> Linode 32GB </td><td key="row-4-column-1" class="text-left"> 8×E5-2680 </td><td key="row-4-column-2" class="text-left"> 32GB </td><td key="row-4-column-3" class="text-left"> 640GB SSD </td><td key="row-4-column-4" class="text-left"> $160/month </td><td key="row-4-column-5" class="text-left"> Fremont, CA </td> </tr><tr key="row-5"> <td key="row-5-column-0" class="text-left"> Lightsail 16GB </td><td key="row-5-column-1" class="text-left"> 4×E5-2686 </td><td key="row-5-column-2" class="text-left"> 16GB </td><td key="row-5-column-3" class="text-left"> 320GB SSD </td><td key="row-5-column-4" class="text-left"> $80/month </td><td key="row-5-column-5" class="text-left"> Northern OR </td> </tr> </tbody> </table> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><aside>AWS doesn’t work like the others; bandwidth and storage are completely flexible and billed by usage. I just added a 40GB volume to the <a href="https://aws.amazon.com/ec2/instance-types/m5/">m5.2xlarge</a> to run tests.</aside><aside>The Digital Ocean “CPU” Droplet is the CPU-optimized variant, more expensive and way more powerful.</aside></div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Geekbench Multi-Core + UnixBench Scores</h4> <figure class="chart-container"> <svg viewBox="0 0 810 338" width="810" height="338" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(262,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> Geekbench Multi-Core </text>  <rect width="12" height="10" fill="#81c9bf" x="193" y="1"></rect> <text x="211" y="10" class="legend-label"> UnixBench </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="289" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="90" class="label"> SSD Nodes 16GB KVM XL </text><text x="25%" y="126" class="label"> Vultr 32GB </text><text x="25%" y="162" class="label"> Digital Ocean 16GB CPU </text><text x="25%" y="198" class="label"> AWS m5.2xlarge </text><text x="25%" y="234" class="label"> Digital Ocean 32GB </text><text x="25%" y="270" class="label"> Linode 32GB </text><text x="25%" y="306" class="label"> Lightsail 16GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="333"> 0 </text><text class="interval-label" x="41%" y="333"> 4300 </text><text class="interval-label" x="55%" y="333"> 8600 </text><text class="interval-label" x="69%" y="333"> 12900 </text><text class="interval-label" x="83%" y="333"> 17200 </text><text class="interval-label" x="97%" y="333"> 21500 </text><text class="interval-label" x="111%" y="333"> 25800 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="45.23627906976744%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="11.445488372093022%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="26.06279069767442%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="5.483767441860465%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="69.77534883720931%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="13.618744186046511%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="56.97674418604651%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="14.351953488372093%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="52.666046511627904%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="15.24079069767442%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="43.12651162790698%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="13.38139534883721%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="40.22558139534884%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="11.780186046511627%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="36.48790697674419%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="9.699069767441861%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="72.73627906976745%" y="45.5" data-item-percent="0.6462325581395348"> 13894 </text><text class="bar-label" x="38.945488372093024%" y="58.5" data-item-percent="0.16350697674418604"> 3515.4 </text><text class="bar-label" x="53.562790697674416%" y="81.5" data-item-percent="0.3723255813953488"> 8005 </text><text class="bar-label" x="32.983767441860465%" y="94.5" data-item-percent="0.07833953488372093"> 1684.3 </text><text class="inverted-bar-label" x="96.27534883720931%" y="117.5" data-item-percent="0.9967906976744186"> 21431 </text><text class="bar-label" x="41.11874418604651%" y="130.5" data-item-percent="0.19455348837209302"> 4182.9 </text><text class="inverted-bar-label" x="83.47674418604652%" y="153.5" data-item-percent="0.813953488372093"> 17500 </text><text class="bar-label" x="41.85195348837209%" y="166.5" data-item-percent="0.2050279069767442"> 4408.1 </text><text class="bar-label" x="80.16604651162791%" y="189.5" data-item-percent="0.7523720930232558"> 16176 </text><text class="bar-label" x="42.74079069767442%" y="202.5" data-item-percent="0.21772558139534887"> 4681.1 </text><text class="bar-label" x="70.62651162790698%" y="225.5" data-item-percent="0.6160930232558139"> 13246 </text><text class="bar-label" x="40.88139534883721%" y="238.5" data-item-percent="0.19116279069767442"> 4110 </text><text class="bar-label" x="67.72558139534884%" y="261.5" data-item-percent="0.5746511627906977"> 12355 </text><text class="bar-label" x="39.28018604651163%" y="274.5" data-item-percent="0.16828837209302325"> 3618.2 </text><text class="bar-label" x="63.98790697674419%" y="297.5" data-item-percent="0.5212558139534884"> 11207 </text><text class="bar-label" x="37.19906976744186%" y="310.5" data-item-percent="0.13855813953488372"> 2979 </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">PHP + MySQL Performance</h4> <figure class="chart-container"> <svg viewBox="0 0 810 338" width="810" height="338" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(346,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> PHP </text>  <rect width="12" height="10" fill="#81c9bf" x="57" y="1"></rect> <text x="75" y="10" class="legend-label"> MySQL </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="289" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="90" class="label"> SSD Nodes 16GB KVM XL </text><text x="25%" y="126" class="label"> Vultr 32GB </text><text x="25%" y="162" class="label"> Digital Ocean 16GB CPU </text><text x="25%" y="198" class="label"> AWS m5.2xlarge </text><text x="25%" y="234" class="label"> Digital Ocean 32GB </text><text x="25%" y="270" class="label"> Linode 32GB </text><text x="25%" y="306" class="label"> Lightsail 16GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="333"> 0 </text><text class="interval-label" x="41%" y="333"> 2 </text><text class="interval-label" x="55%" y="333"> 4 </text><text class="interval-label" x="69%" y="333"> 6 </text><text class="interval-label" x="83%" y="333"> 8 </text><text class="interval-label" x="97%" y="333"> 10 </text><text class="interval-label" x="111%" y="333"> 12 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="27.5499%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="13.489%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="26.359899999999996%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="23.772000000000002%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="18.907%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="11.508%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="17.7121%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="6.951%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="19.2619%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="4.627000000000001%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="41.5009%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="12.208%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="29.444099999999995%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="13.741%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="24.045%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="7.412999999999999%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="55.0499%" y="45.5" data-item-percent="0.39357000000000003"> 3.9357s </text><text class="bar-label" x="40.989000000000004%" y="58.5" data-item-percent="0.1927"> 1.927s </text><text class="bar-label" x="53.859899999999996%" y="81.5" data-item-percent="0.37656999999999996"> 3.7657s </text><text class="bar-label" x="51.272000000000006%" y="94.5" data-item-percent="0.3396"> 3.396s </text><text class="bar-label" x="46.407%" y="117.5" data-item-percent="0.2701"> 2.701s </text><text class="bar-label" x="39.007999999999996%" y="130.5" data-item-percent="0.1644"> 1.644s </text><text class="bar-label" x="45.2121%" y="153.5" data-item-percent="0.25303"> 2.5303s </text><text class="bar-label" x="34.451%" y="166.5" data-item-percent="0.0993"> 0.993s </text><text class="bar-label" x="46.7619%" y="189.5" data-item-percent="0.27517"> 2.7517s </text><text class="bar-label" x="32.127%" y="202.5" data-item-percent="0.0661"> 0.661s </text><text class="bar-label" x="69.0009%" y="225.5" data-item-percent="0.59287"> 5.9287s </text><text class="bar-label" x="39.708%" y="238.5" data-item-percent="0.1744"> 1.744s </text><text class="bar-label" x="56.94409999999999%" y="261.5" data-item-percent="0.42062999999999995"> 4.2063s </text><text class="bar-label" x="41.241%" y="274.5" data-item-percent="0.1963"> 1.963s </text><text class="bar-label" x="51.545%" y="297.5" data-item-percent="0.3435"> 3.435s </text><text class="bar-label" x="34.913%" y="310.5" data-item-percent="0.1059"> 1.059s </text> </g> </svg> <figcaption> <p>Lower is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Storage I/O</h4> <figure class="chart-container"> <svg viewBox="0 0 810 338" width="810" height="338" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style> <g class="legend" transform="translate(246,0)">  <rect width="12" height="10" fill="#00a1d5" x="0" y="1"></rect> <text x="18" y="10" class="legend-label"> Random Read MB/s </text>  <rect width="12" height="10" fill="#81c9bf" x="161" y="1"></rect> <text x="179" y="10" class="legend-label"> Random Write MB/s </text>  </g> <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="30" height="289" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="30" height="289" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="54" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="90" class="label"> SSD Nodes 16GB KVM XL </text><text x="25%" y="126" class="label"> Vultr 32GB </text><text x="25%" y="162" class="label"> Digital Ocean 16GB CPU </text><text x="25%" y="198" class="label"> AWS m5.2xlarge </text><text x="25%" y="234" class="label"> Digital Ocean 32GB </text><text x="25%" y="270" class="label"> Linode 32GB </text><text x="25%" y="306" class="label"> Lightsail 16GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="333"> 0 </text><text class="interval-label" x="41%" y="333"> 200 </text><text class="interval-label" x="55%" y="333"> 400 </text><text class="interval-label" x="69%" y="333"> 600 </text><text class="interval-label" x="83%" y="333"> 800 </text><text class="interval-label" x="97%" y="333"> 1000 </text><text class="interval-label" x="111%" y="333"> 1200 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="30" opacity="0.2"></rect>  <rect width="22.014369999999996%" height="12" fill="#00a1d5" x="27%" y="36" key="1"></rect><rect width="26.45692%" height="12" fill="#81c9bf" x="27%" y="49" key="2"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="66" opacity="0.2"></rect>  <rect width="12.02586%" height="12" fill="#00a1d5" x="27%" y="72" key="1"></rect><rect width="6.854470000000001%" height="12" fill="#81c9bf" x="27%" y="85" key="2"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="102" opacity="0.2"></rect>  <rect width="11.48056%" height="12" fill="#00a1d5" x="27%" y="108" key="1"></rect><rect width="10.419780000000001%" height="12" fill="#81c9bf" x="27%" y="121" key="2"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="138" opacity="0.2"></rect>  <rect width="48.07985%" height="12" fill="#00a1d5" x="27%" y="144" key="1"></rect><rect width="11.49309%" height="12" fill="#81c9bf" x="27%" y="157" key="2"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="0.8495900000000001%" height="12" fill="#00a1d5" x="27%" y="180" key="1"></rect><rect width="0.8437100000000001%" height="12" fill="#81c9bf" x="27%" y="193" key="2"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="210" opacity="0.2"></rect>  <rect width="24.5217%" height="12" fill="#00a1d5" x="27%" y="216" key="1"></rect><rect width="2.26303%" height="12" fill="#81c9bf" x="27%" y="229" key="2"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="246" opacity="0.2"></rect>  <rect width="21.030659999999997%" height="12" fill="#00a1d5" x="27%" y="252" key="1"></rect><rect width="9.106649999999998%" height="12" fill="#81c9bf" x="27%" y="265" key="2"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="282" opacity="0.2"></rect>  <rect width="0.8638%" height="12" fill="#00a1d5" x="27%" y="288" key="1"></rect><rect width="0.85967%" height="12" fill="#81c9bf" x="27%" y="301" key="2"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="318" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="49.51437%" y="45.5" data-item-percent="0.31449099999999997"> 314.491 MB/s </text><text class="bar-label" x="53.95692%" y="58.5" data-item-percent="0.377956"> 377.956 MB/s </text><text class="bar-label" x="39.52586%" y="81.5" data-item-percent="0.171798"> 171.798 MB/s </text><text class="bar-label" x="34.35447%" y="94.5" data-item-percent="0.09792100000000001"> 97.921 MB/s </text><text class="bar-label" x="38.98056%" y="117.5" data-item-percent="0.16400800000000001"> 164.008 MB/s </text><text class="bar-label" x="37.91978%" y="130.5" data-item-percent="0.14885400000000001"> 148.854 MB/s </text><text class="bar-label" x="75.57985%" y="153.5" data-item-percent="0.686855"> 686.855 MB/s </text><text class="bar-label" x="38.99309%" y="166.5" data-item-percent="0.164187"> 164.187 MB/s </text><text class="bar-label" x="28.34959%" y="189.5" data-item-percent="0.012137"> 12.137 MB/s </text><text class="bar-label" x="28.34371%" y="202.5" data-item-percent="0.012053000000000001"> 12.053 MB/s </text><text class="bar-label" x="52.021699999999996%" y="225.5" data-item-percent="0.35031"> 350.31 MB/s </text><text class="bar-label" x="29.76303%" y="238.5" data-item-percent="0.032329000000000004"> 32.329 MB/s </text><text class="bar-label" x="48.53066%" y="261.5" data-item-percent="0.300438"> 300.438 MB/s </text><text class="bar-label" x="36.60665%" y="274.5" data-item-percent="0.130095"> 130.095 MB/s </text><text class="bar-label" x="28.3638%" y="297.5" data-item-percent="0.01234"> 12.34 MB/s </text><text class="bar-label" x="28.35967%" y="310.5" data-item-percent="0.012281"> 12.281 MB/s </text> </g> </svg>  </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Bandwidth</h4> <figure class="chart-container"> <svg viewBox="0 0 810 252" width="810" height="252" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="233" opacity="0.2"></rect><rect width="1" fill="#666" x="38%" y="0" height="233" opacity="0.2"></rect><rect width="1" fill="#666" x="50%" y="0" height="233" opacity="0.2"></rect><rect width="1" fill="#666" x="62%" y="0" height="233" opacity="0.2"></rect><rect width="1" fill="#666" x="73%" y="0" height="233" opacity="0.2"></rect><rect width="1" fill="#666" x="85%" y="0" height="233" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="233" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="49.5" class="label"> SSD Nodes 16GB KVM XL </text><text x="25%" y="78.5" class="label"> Vultr 32GB </text><text x="25%" y="107.5" class="label"> Digital Ocean 16GB CPU </text><text x="25%" y="136.5" class="label"> AWS m5.2xlarge </text><text x="25%" y="165.5" class="label"> Digital Ocean 32GB </text><text x="25%" y="194.5" class="label"> Linode 32GB </text><text x="25%" y="223.5" class="label"> Lightsail 16GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="247"> 0 </text><text class="interval-label" x="38%" y="247"> 50 </text><text class="interval-label" x="50%" y="247"> 100 </text><text class="interval-label" x="62%" y="247"> 150 </text><text class="interval-label" x="73%" y="247"> 200 </text><text class="interval-label" x="85%" y="247"> 250 </text><text class="interval-label" x="97%" y="247"> 300 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="24.5%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="24.03333333333333%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="28%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="65.10000000000001%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="52.266666666666666%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="55.300000000000004%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="39.9%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="10.173333333333334%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="52%" y="18.56" data-item-percent="0.35"> 105 MB/s </text><text class="bar-label" x="51.53333333333333%" y="47.56" data-item-percent="0.3433333333333333"> 103 MB/s </text><text class="bar-label" x="55.5%" y="76.56" data-item-percent="0.4"> 120 MB/s </text><text class="inverted-bar-label" x="91.60000000000001%" y="105.56" data-item-percent="0.93"> 279 MB/s </text><text class="bar-label" x="79.76666666666667%" y="134.56" data-item-percent="0.7466666666666667"> 224 MB/s </text><text class="bar-label" x="82.80000000000001%" y="163.56" data-item-percent="0.79"> 237 MB/s </text><text class="bar-label" x="67.4%" y="192.56" data-item-percent="0.57"> 171 MB/s </text><text class="bar-label" x="37.67333333333333%" y="221.56" data-item-percent="0.14533333333333334"> 43.6 MB/s </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Working Conclusion</h2><p>My current concept of value centers around performance and stability, but despite all the fun here I could see eventually wanting to pay more for service that comes with monitoring, backups, and features that cost very little compared to the time it’d take me to establish and maintain my own solutions. At the moment, I’ve let easily-chartable stats guide me a bit.</p></div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Megabytes of RAM per Dollar</h4> <figure class="chart-container"> <svg viewBox="0 0 810 571" width="810" height="571" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="0" height="552" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="49.5" class="label"> RamNode 2GB NVMe </text><text x="25%" y="78.5" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="107.5" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="136.5" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="165.5" class="label"> Digital Ocean 2GB </text><text x="25%" y="194.5" class="label"> Digital Ocean 4GB </text><text x="25%" y="223.5" class="label"> Digital Ocean 16GB CPU </text><text x="25%" y="252.5" class="label"> Digital Ocean 32GB </text><text x="25%" y="281.5" class="label"> Linode 2GB </text><text x="25%" y="310.5" class="label"> Linode 4GB </text><text x="25%" y="339.5" class="label"> Linode 32GB </text><text x="25%" y="368.5" class="label"> Vultr 2GB </text><text x="25%" y="397.5" class="label"> Vultr 4GB </text><text x="25%" y="426.5" class="label"> Vultr 32GB </text><text x="25%" y="455.5" class="label"> AWS m5.2xlarge </text><text x="25%" y="484.5" class="label"> AWS Lightsail 2GB </text><text x="25%" y="513.5" class="label"> AWS Lightsail 4GB </text><text x="25%" y="542.5" class="label"> AWS Lightsail 16GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="566"> 0 </text><text class="interval-label" x="41%" y="566"> 400 </text><text class="interval-label" x="55%" y="566"> 800 </text><text class="interval-label" x="69%" y="566"> 1200 </text><text class="interval-label" x="83%" y="566"> 1600 </text><text class="interval-label" x="97%" y="566"> 2000 </text><text class="interval-label" x="111%" y="566"> 2400 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="10.745%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="5.95%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="57.4%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="14.98%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="22.75%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="3.57%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="238" key="1"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="261" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="267" key="1"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="290" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="296" key="1"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="319" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="325" key="1"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="348" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="354" key="1"></rect>   </g><g class="set" key="13">  <rect width="70%" height="1" fill="#666" x="27%" y="377" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="383" key="1"></rect>   </g><g class="set" key="14">  <rect width="70%" height="1" fill="#666" x="27%" y="406" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="412" key="1"></rect>   </g><g class="set" key="15">  <rect width="70%" height="1" fill="#666" x="27%" y="435" opacity="0.2"></rect>  <rect width="4.375%" height="18" fill="#00a1d5" x="27%" y="441" key="1"></rect>   </g><g class="set" key="16">  <rect width="70%" height="1" fill="#666" x="27%" y="464" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="470" key="1"></rect>   </g><g class="set" key="17">  <rect width="70%" height="1" fill="#666" x="27%" y="493" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="499" key="1"></rect>   </g><g class="set" key="18">  <rect width="70%" height="1" fill="#666" x="27%" y="522" opacity="0.2"></rect>  <rect width="7.14%" height="18" fill="#00a1d5" x="27%" y="528" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="551" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="38.245%" y="18.56" data-item-percent="0.1535"> 307 MB </text><text class="bar-label" x="33.45%" y="47.56" data-item-percent="0.085"> 170 MB </text><text class="inverted-bar-label" x="83.9%" y="76.56" data-item-percent="0.82"> 1640 MB </text><text class="bar-label" x="42.480000000000004%" y="105.56" data-item-percent="0.214"> 428 MB </text><text class="bar-label" x="50.25%" y="134.56" data-item-percent="0.325"> 650 MB </text><text class="bar-label" x="34.64%" y="163.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="192.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="31.07%" y="221.56" data-item-percent="0.051"> 102 MB </text><text class="bar-label" x="34.64%" y="250.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="279.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="308.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="337.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="366.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="395.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="424.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="31.875%" y="453.56" data-item-percent="0.0625"> 125 MB </text><text class="bar-label" x="34.64%" y="482.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="511.56" data-item-percent="0.102"> 204 MB </text><text class="bar-label" x="34.64%" y="540.56" data-item-percent="0.102"> 204 MB </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="chart-block max-w-lg mx-auto mb-8"> <h4 class="text-center font-sans font-bold">Geekbench Points per Dollar</h4> <figure class="chart-container"> <svg viewBox="0 0 810 571" width="810" height="571" class="chart" style="max-width:100%;height:auto;display:block;overflow:hidden;margin:0"> <style>
                .label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #61899b; text-anchor: end; }
                .interval-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #61899b; text-anchor: middle; }
                .bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #6F9DB2; text-anchor: start; }
                .inverted-bar-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.6rem; fill: #fff; text-anchor: end; }
                .legend-label { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 0.8rem; fill: #6F9DB2; text-anchor: start; }
              </style>  <g class="interval-marks"><rect width="1" fill="#666" x="27%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="41%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="55%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="69%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="83%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="97%" y="0" height="552" opacity="0.2"></rect><rect width="1" fill="#666" x="111%" y="0" height="552" opacity="0.2"></rect></g> <g class="labels"> <text x="25%" y="20.5" class="label"> RamNode 2GB OpenVZ </text><text x="25%" y="49.5" class="label"> RamNode 2GB NVMe </text><text x="25%" y="78.5" class="label"> SSD Nodes KVM / X-Large </text><text x="25%" y="107.5" class="label"> HostUS 4GB OpenVZ </text><text x="25%" y="136.5" class="label"> Hyper Expert 12GB KVM </text><text x="25%" y="165.5" class="label"> Digital Ocean 2GB </text><text x="25%" y="194.5" class="label"> Digital Ocean 4GB </text><text x="25%" y="223.5" class="label"> Digital Ocean 16GB CPU </text><text x="25%" y="252.5" class="label"> Digital Ocean 32GB </text><text x="25%" y="281.5" class="label"> Linode 2GB </text><text x="25%" y="310.5" class="label"> Linode 4GB </text><text x="25%" y="339.5" class="label"> Linode 32GB </text><text x="25%" y="368.5" class="label"> Vultr 2GB </text><text x="25%" y="397.5" class="label"> Vultr 4GB </text><text x="25%" y="426.5" class="label"> Vultr 32GB </text><text x="25%" y="455.5" class="label"> AWS m5.2xlarge </text><text x="25%" y="484.5" class="label"> AWS Lightsail 2GB </text><text x="25%" y="513.5" class="label"> AWS Lightsail 4GB </text><text x="25%" y="542.5" class="label"> AWS Lightsail 16GB </text> </g> <g class="interval-labels"><text class="interval-label" x="27%" y="566"> 0 </text><text class="interval-label" x="41%" y="566"> 200 </text><text class="interval-label" x="55%" y="566"> 400 </text><text class="interval-label" x="69%" y="566"> 600 </text><text class="interval-label" x="83%" y="566"> 800 </text><text class="interval-label" x="97%" y="566"> 1000 </text><text class="interval-label" x="111%" y="566"> 1200 </text></g> <g class="bars"> <g class="set" key="0">  <rect width="70%" height="1" fill="#666" x="27%" y="0" opacity="0.2"></rect>  <rect width="30.24%" height="18" fill="#00a1d5" x="27%" y="6" key="1"></rect>   </g><g class="set" key="1">  <rect width="70%" height="1" fill="#666" x="27%" y="29" opacity="0.2"></rect>  <rect width="46.2%" height="18" fill="#00a1d5" x="27%" y="35" key="1"></rect>   </g><g class="set" key="2">  <rect width="70%" height="1" fill="#666" x="27%" y="58" opacity="0.2"></rect>  <rect width="56.07%" height="18" fill="#00a1d5" x="27%" y="64" key="1"></rect>   </g><g class="set" key="3">  <rect width="70%" height="1" fill="#666" x="27%" y="87" opacity="0.2"></rect>  <rect width="29.96%" height="18" fill="#00a1d5" x="27%" y="93" key="1"></rect>   </g><g class="set" key="4">  <rect width="70%" height="1" fill="#666" x="27%" y="116" opacity="0.2"></rect>  <rect width="51.449999999999996%" height="18" fill="#00a1d5" x="27%" y="122" key="1"></rect>   </g><g class="set" key="5">  <rect width="70%" height="1" fill="#666" x="27%" y="145" opacity="0.2"></rect>  <rect width="18.830000000000002%" height="18" fill="#00a1d5" x="27%" y="151" key="1"></rect>   </g><g class="set" key="6">  <rect width="70%" height="1" fill="#666" x="27%" y="174" opacity="0.2"></rect>  <rect width="18.970000000000002%" height="18" fill="#00a1d5" x="27%" y="180" key="1"></rect>   </g><g class="set" key="7">  <rect width="70%" height="1" fill="#666" x="27%" y="203" opacity="0.2"></rect>  <rect width="7.63%" height="18" fill="#00a1d5" x="27%" y="209" key="1"></rect>   </g><g class="set" key="8">  <rect width="70%" height="1" fill="#666" x="27%" y="232" opacity="0.2"></rect>  <rect width="5.74%" height="18" fill="#00a1d5" x="27%" y="238" key="1"></rect>   </g><g class="set" key="9">  <rect width="70%" height="1" fill="#666" x="27%" y="261" opacity="0.2"></rect>  <rect width="23.240000000000002%" height="18" fill="#00a1d5" x="27%" y="267" key="1"></rect>   </g><g class="set" key="10">  <rect width="70%" height="1" fill="#666" x="27%" y="290" opacity="0.2"></rect>  <rect width="16.03%" height="18" fill="#00a1d5" x="27%" y="296" key="1"></rect>   </g><g class="set" key="11">  <rect width="70%" height="1" fill="#666" x="27%" y="319" opacity="0.2"></rect>  <rect width="5.39%" height="18" fill="#00a1d5" x="27%" y="325" key="1"></rect>   </g><g class="set" key="12">  <rect width="70%" height="1" fill="#666" x="27%" y="348" opacity="0.2"></rect>  <rect width="26.32%" height="18" fill="#00a1d5" x="27%" y="354" key="1"></rect>   </g><g class="set" key="13">  <rect width="70%" height="1" fill="#666" x="27%" y="377" opacity="0.2"></rect>  <rect width="25.689999999999998%" height="18" fill="#00a1d5" x="27%" y="383" key="1"></rect>   </g><g class="set" key="14">  <rect width="70%" height="1" fill="#666" x="27%" y="406" opacity="0.2"></rect>  <rect width="9.31%" height="18" fill="#00a1d5" x="27%" y="412" key="1"></rect>   </g><g class="set" key="15">  <rect width="70%" height="1" fill="#666" x="27%" y="435" opacity="0.2"></rect>  <rect width="4.27%" height="18" fill="#00a1d5" x="27%" y="441" key="1"></rect>   </g><g class="set" key="16">  <rect width="70%" height="1" fill="#666" x="27%" y="464" opacity="0.2"></rect>  <rect width="21.63%" height="18" fill="#00a1d5" x="27%" y="470" key="1"></rect>   </g><g class="set" key="17">  <rect width="70%" height="1" fill="#666" x="27%" y="493" opacity="0.2"></rect>  <rect width="20.509999999999998%" height="18" fill="#00a1d5" x="27%" y="499" key="1"></rect>   </g><g class="set" key="18">  <rect width="70%" height="1" fill="#666" x="27%" y="522" opacity="0.2"></rect>  <rect width="9.8%" height="18" fill="#00a1d5" x="27%" y="528" key="1"></rect>  <rect width="70%" height="1" fill="#666" x="27%" y="551" opacity="0.2"></rect> </g> </g> <g class="bar-labels"> <text class="bar-label" x="57.739999999999995%" y="18.56" data-item-percent="0.432"> 432 </text><text class="bar-label" x="73.7%" y="47.56" data-item-percent="0.66"> 660 </text><text class="inverted-bar-label" x="82.57%" y="76.56" data-item-percent="0.801"> 801 </text><text class="bar-label" x="57.46%" y="105.56" data-item-percent="0.428"> 428 </text><text class="bar-label" x="78.94999999999999%" y="134.56" data-item-percent="0.735"> 735 </text><text class="bar-label" x="46.33%" y="163.56" data-item-percent="0.269"> 269 </text><text class="bar-label" x="46.47%" y="192.56" data-item-percent="0.271"> 271 </text><text class="bar-label" x="35.13%" y="221.56" data-item-percent="0.109"> 109 </text><text class="bar-label" x="33.24%" y="250.56" data-item-percent="0.082"> 82 </text><text class="bar-label" x="50.74%" y="279.56" data-item-percent="0.332"> 332 </text><text class="bar-label" x="43.53%" y="308.56" data-item-percent="0.229"> 229 </text><text class="bar-label" x="32.89%" y="337.56" data-item-percent="0.077"> 77 </text><text class="bar-label" x="53.82%" y="366.56" data-item-percent="0.376"> 376 </text><text class="bar-label" x="53.19%" y="395.56" data-item-percent="0.367"> 367 </text><text class="bar-label" x="36.81%" y="424.56" data-item-percent="0.133"> 133 </text><text class="bar-label" x="31.77%" y="453.56" data-item-percent="0.061"> 61 </text><text class="bar-label" x="49.129999999999995%" y="482.56" data-item-percent="0.309"> 309 </text><text class="bar-label" x="48.01%" y="511.56" data-item-percent="0.293"> 293 </text><text class="bar-label" x="37.3%" y="540.56" data-item-percent="0.14"> 140 </text> </g> </svg> <figcaption> <p>Higher is better.</p>
 </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><a href="https://www.ssdnodes.com/manage/aff.php?aff=686">SSD Nodes</a> clearly wins for best performance/cost ratio, but I’ve been most thrilled with the <a href="https://p.hyper.expert/aff.php?aff=67">Hyper Expert</a> VPS since it’s in my neighborhood. I’m going to keep both of those around for the next year or so and see what else I learn.</p><p>I don’t understand why all AWS read/write values seem consistently fixed near 12 MB/s, or how <a href="https://m.do.co/c/f1391f41e5e8">Digital Ocean</a>’s Droplets do so much better in bandwidth tests.</p><p>A lot of this testing seems circumstantial and general at best, but it’s clear to me that processors and storage types matter when it comes to these shared resources. <a href="https://clientarea.ramnode.com/aff.php?aff=2496">RamNode</a>’s NVMe plan comes with good processors and exceptionally fast storage, and it shows. Time will tell whether all this glorious performance wins out over major platforms and all their perks.</p><p>I hope you enjoyed this post! I welcome questions and criticisms in the comments.</p><hr><h3>Non-Affiliate Links</h3><ul><li><a href="https://www.hyperexpert.com/">Hyper Expert</a></li><li><a href="https://www.ssdnodes.com/">SSD Nodes</a></li><li><a href="https://ramnode.com/">RamNode</a></li><li><a href="https://hostus.us/">HostUS</a></li><li><a href="https://www.digitalocean.com/">Digital Ocean</a></li><li><a href="https://www.linode.com/">Linode</a></li><li><a href="https://www.vultr.com/">Vultr</a></li></ul></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Easy Forge Backups with Restic and Backblaze B2]]></title>
            <link>https://workingconcept.com/blog/forge-backups-restic-backblaze-b2</link>
            <guid>https://workingconcept.com/blog/forge-backups-restic-backblaze-b2</guid>
            <pubDate>Tue, 26 Feb 2019 15:30:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/forge-b2.C6TM_sxi_ZbeSkI.webp" type="image/webp"><source src="https://workingconcept.com//_astro/forge-b2.C6TM_sxi_Z2i8vYQ.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/forge-b2.C6TM_sxi_ZtEEQw.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/forge-b2.C6TM_sxi_Z2mSUQa.png" decoding="async" loading="lazy" alt width="2720" height="1254"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>One of my goals this year is to stop spending time maintaining my own private code. </p><p>I have a pile of apps and scripts and learn a great deal writing them, but I’d rather focus on sharing and improving code instead of being a hoarder. Another goal is to spend more time on code and techniques I’d offer to clients and fewer rabbit-hole learning quests. So this one’s a two-fer.</p><p>After <a href="{entry:419@1:url||https://workingconcept.com/blog/2018-budget-vps-survey}">a thorough comparison of VPS providers</a> and ways of provisioning I’ve decided to simplify my live and embrace <a href="https://forge.laravel.com">Laravel Forge</a>. I have a few hosting accounts with smaller providers that don’t offer backup service, so I’ve spiffed up my pre-Forge backup scheme for quick setup on a newly-provisioned Forge server.</p><p>It stores data offsite with <a href="https://www.backblaze.com/b2/cloud-storage.html">Backblaze B2</a> using a command line app called <a href="https://restic.net/">restic</a>.</p><p>The combination is stable, inexpensive, efficient and encrypted. A nice perk is that the backups are snapshots, meaning you can choose a specific backup point to browse/restore much like Apple’s Time Machine. This and the built-in encryption make it better than just rsyncing data. (Credit goes to <a href="https://maxrohde.com/2018/02/01/easy-vps-backup/">The Full Stack Blog</a> for pointing me toward restic!) </p><p>Good news, dear reader: if you’re using Forge, you can probably use this too! Grab it from GitHub (<a href="https://github.com/workingconcept/forge-backup">workingconcept/forge-backup</a>), and let’s take a quick look at how it works in the wild.</p><p><strong>Requirements</strong></p><ol><li>Server provisioned with Laravel Forge running Ubuntu 18+.</li><li>Backblaze account.</li></ol><p><strong>Quick Version</strong></p><p>You can clone the repository to start, but everything’s built into one shell script so you can run a one-liner as root:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">curl</span><span style="color:#C3E88D"> -O</span><span style="color:#C3E88D"> https://raw.githubusercontent.com/workingconcept/forge-backup/master/restic-setup.sh</span><span style="color:#89DDFF"> &#x26;&#x26;</span><span style="color:#FFCB6B"> chmod</span><span style="color:#C3E88D"> +x</span><span style="color:#C3E88D"> restic-setup.sh</span><span style="color:#89DDFF"> &#x26;&#x26;</span><span style="color:#FFCB6B"> ./restic-setup.sh</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Note that you <em>must</em> be root here and not just use <code>sudo</code>.</p><p>It’s a good idea to be wary of downloading and running shell scripts as root because some random internet guy said it’s cool on his blog. So let’s examine what happens one step at a time.</p><h2>Step 0: Ready the Things</h2><p>Provision a Forge server and keep track of the sudo and database passwords you get. You can safely run this on an existing server as long as it’s Ubuntu 18+, because <code>restic</code> isn’t included in Ubuntu’s default packages for lower versions.</p><p>Create a Backblaze B2 bucket under <em>B2 Cloud Storage</em> → <em>Buckets</em>. Pick (and remember) the name and keep it <em>Private</em>.</p><p>Now choose <em>Show Account ID and Application Key</em> just above the big <em>Create a Bucket</em> button. Create a key and limit it to your new bucket, and grab the resulting key (large) and keyId (small) you’ll find at the bottom in this screenshot:</p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/b2-keys.B0bW_1HG_1nP2Ue.webp" type="image/webp"><source src="https://workingconcept.com//_astro/b2-keys.B0bW_1HG_ZtnSzA.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/b2-keys.B0bW_1HG_1deoHK.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/b2-keys.B0bW_1HG_Z1izdvy.png" decoding="async" loading="lazy" alt="Screenshot of Backblaze B2 application key modal showing described settings and where to get keys" width="2423" height="1985"> </picture> <figcaption> <p> The important part is blurred at the bottom.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>We’re ready to continue.</p><h2>Step 1: Initialize</h2><p>SSH into your new server and become root.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">ssh</span><span style="color:#C3E88D"> forge@[SERVER</span><span style="color:#C3E88D"> IP]</span></span>
<span class="line"><span style="color:#FFCB6B">sudo</span><span style="color:#C3E88D"> bash</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Copy and paste that one-liner and hit return.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">curl</span><span style="color:#C3E88D"> -O</span><span style="color:#C3E88D"> https://raw.githubusercontent.com/workingconcept/forge-backup/master/restic-setup.sh</span><span style="color:#89DDFF"> &#x26;&#x26;</span><span style="color:#FFCB6B"> chmod</span><span style="color:#C3E88D"> +x</span><span style="color:#C3E88D"> restic-setup.sh</span><span style="color:#89DDFF"> &#x26;&#x26;</span><span style="color:#FFCB6B"> ./restic-setup.sh</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This will download <code>restic-setup.sh</code> from GitHub, make it executable, and then run it. That’ll look something like this:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">root@sudden-valley:~#</span><span style="color:#C3E88D"> curl</span><span style="color:#C3E88D"> -O</span><span style="color:#C3E88D"> https://raw.githubusercontent.com/workingconcept/forge-backup/master/restic-setup.sh</span><span style="color:#89DDFF"> &#x26;&#x26;</span><span style="color:#FFCB6B"> chmod</span><span style="color:#C3E88D"> +x</span><span style="color:#C3E88D"> restic-setup.sh</span><span style="color:#89DDFF"> &#x26;&#x26;</span><span style="color:#FFCB6B"> ./restic-setup.sh</span></span>
<span class="line"><span style="color:#FFCB6B">  %</span><span style="color:#C3E88D"> Total</span><span style="color:#C3E88D">    %</span><span style="color:#C3E88D"> Received</span><span style="color:#C3E88D"> %</span><span style="color:#C3E88D"> Xferd</span><span style="color:#C3E88D">  Average</span><span style="color:#C3E88D"> Speed</span><span style="color:#C3E88D">   Time</span><span style="color:#C3E88D">    Time</span><span style="color:#C3E88D">     Time</span><span style="color:#C3E88D">  Current</span></span>
<span class="line"><span style="color:#FFCB6B">                                 Dload</span><span style="color:#C3E88D">  Upload</span><span style="color:#C3E88D">   Total</span><span style="color:#C3E88D">   Spent</span><span style="color:#C3E88D">    Left</span><span style="color:#C3E88D">  Speed</span></span>
<span class="line"><span style="color:#FFCB6B">100</span><span style="color:#F78C6C">  8379</span><span style="color:#F78C6C">  100</span><span style="color:#F78C6C">  8379</span><span style="color:#F78C6C">    0</span><span style="color:#F78C6C">     0</span><span style="color:#F78C6C">  69825</span><span style="color:#F78C6C">      0</span><span style="color:#C3E88D"> --:--:--</span><span style="color:#C3E88D"> --:--:--</span><span style="color:#C3E88D"> --:--:--</span><span style="color:#F78C6C"> 69825</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Forge</span><span style="color:#C3E88D"> →</span><span style="color:#C3E88D"> B2</span><span style="color:#C3E88D"> Backup</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Enter</span><span style="color:#C3E88D"> B2</span><span style="color:#C3E88D"> application</span><span style="color:#C3E88D"> key</span><span style="color:#C3E88D"> ID:</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>It’s going to ask for your B2 application key ID, application key and bucket. Then it’ll install restic (<code>apt-get install restic</code>) which will look like this:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Installing</span><span style="color:#C3E88D"> restic...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Reading</span><span style="color:#C3E88D"> package</span><span style="color:#C3E88D"> lists...</span><span style="color:#C3E88D"> Done</span></span>
<span class="line"><span style="color:#FFCB6B">Building</span><span style="color:#C3E88D"> dependency</span><span style="color:#C3E88D"> tree</span></span>
<span class="line"><span style="color:#FFCB6B">Reading</span><span style="color:#C3E88D"> state</span><span style="color:#C3E88D"> information...</span><span style="color:#C3E88D"> Done</span></span>
<span class="line"><span style="color:#FFCB6B">Suggested</span><span style="color:#C3E88D"> packages:</span></span>
<span class="line"><span style="color:#FFCB6B">  libjs-jquery</span><span style="color:#C3E88D"> libjs-underscore</span></span>
<span class="line"><span style="color:#FFCB6B">The</span><span style="color:#C3E88D"> following</span><span style="color:#C3E88D"> NEW</span><span style="color:#C3E88D"> packages</span><span style="color:#C3E88D"> will</span><span style="color:#C3E88D"> be</span><span style="color:#C3E88D"> installed:</span></span>
<span class="line"><span style="color:#FFCB6B">  restic</span></span>
<span class="line"><span style="color:#FFCB6B">0</span><span style="color:#C3E88D"> upgraded,</span><span style="color:#F78C6C"> 1</span><span style="color:#C3E88D"> newly</span><span style="color:#C3E88D"> installed,</span><span style="color:#F78C6C"> 0</span><span style="color:#C3E88D"> to</span><span style="color:#C3E88D"> remove</span><span style="color:#C3E88D"> and</span><span style="color:#F78C6C"> 5</span><span style="color:#C3E88D"> not</span><span style="color:#C3E88D"> upgraded.</span></span>
<span class="line"><span style="color:#FFCB6B">Need</span><span style="color:#C3E88D"> to</span><span style="color:#C3E88D"> get</span><span style="color:#C3E88D"> 5,179</span><span style="color:#C3E88D"> kB</span><span style="color:#C3E88D"> of</span><span style="color:#C3E88D"> archives.</span></span>
<span class="line"><span style="color:#FFCB6B">After</span><span style="color:#C3E88D"> this</span><span style="color:#C3E88D"> operation,</span><span style="color:#F78C6C"> 16.4</span><span style="color:#C3E88D"> MB</span><span style="color:#C3E88D"> of</span><span style="color:#C3E88D"> additional</span><span style="color:#C3E88D"> disk</span><span style="color:#C3E88D"> space</span><span style="color:#C3E88D"> will</span><span style="color:#C3E88D"> be</span><span style="color:#C3E88D"> used.</span></span>
<span class="line"><span style="color:#FFCB6B">Get:1</span><span style="color:#C3E88D"> http://mirrors.linode.com/ubuntu</span><span style="color:#C3E88D"> bionic/universe</span><span style="color:#C3E88D"> amd64</span><span style="color:#C3E88D"> restic</span><span style="color:#C3E88D"> amd64</span><span style="color:#C3E88D"> 0.8.3+ds-1</span><span style="color:#EEFFFF"> [5,179 </span><span style="color:#C3E88D">kB]</span></span>
<span class="line"><span style="color:#FFCB6B">Fetched</span><span style="color:#C3E88D"> 5,179</span><span style="color:#C3E88D"> kB</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> 0s</span><span style="color:#EEFFFF"> (57.4 </span><span style="color:#C3E88D">MB/s</span><span style="color:#EEFFFF">)</span></span>
<span class="line"><span style="color:#FFCB6B">Selecting</span><span style="color:#C3E88D"> previously</span><span style="color:#C3E88D"> unselected</span><span style="color:#C3E88D"> package</span><span style="color:#C3E88D"> restic.</span></span>
<span class="line"><span style="color:#89DDFF">(</span><span style="color:#FFCB6B">Reading</span><span style="color:#C3E88D"> database</span><span style="color:#C3E88D"> ...</span><span style="color:#F78C6C"> 119910</span><span style="color:#C3E88D"> files</span><span style="color:#C3E88D"> and</span><span style="color:#C3E88D"> directories</span><span style="color:#C3E88D"> currently</span><span style="color:#C3E88D"> installed.</span><span style="color:#89DDFF">)</span></span>
<span class="line"><span style="color:#FFCB6B">Preparing</span><span style="color:#C3E88D"> to</span><span style="color:#C3E88D"> unpack</span><span style="color:#C3E88D"> .../restic_0.8.3+ds-1_amd64.deb</span><span style="color:#C3E88D"> ...</span></span>
<span class="line"><span style="color:#FFCB6B">Unpacking</span><span style="color:#C3E88D"> restic</span><span style="color:#EEFFFF"> (0.8.3+ds-1) ...</span></span>
<span class="line"><span style="color:#FFCB6B">Setting</span><span style="color:#C3E88D"> up</span><span style="color:#C3E88D"> restic</span><span style="color:#EEFFFF"> (0.8.3+ds-1) ...</span></span>
<span class="line"><span style="color:#FFCB6B">Processing</span><span style="color:#C3E88D"> triggers</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> man-db</span><span style="color:#EEFFFF"> (2.8.3-2ubuntu0.1) ...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Creating</span><span style="color:#C3E88D"> /root/restic...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Confirming</span><span style="color:#C3E88D"> MySQL</span><span style="color:#C3E88D"> setup...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Enter</span><span style="color:#C3E88D"> MySQL</span><span style="color:#C3E88D"> password</span><span style="color:#C3E88D"> for</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">forge</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">:</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Now you’ll be prompted for your default <code>forge</code> MySQL password, which will used (without being stored) to create a new read-only <code>backup</code> user with access to all databases.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#89DDFF">!!</span><span style="color:#FFCB6B"> created</span><span style="color:#C3E88D"> backup</span><span style="color:#C3E88D"> user</span><span style="color:#C3E88D"> with</span><span style="color:#C3E88D"> password:</span><span style="color:#C3E88D"> ••••••••••••••••••••</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Confirming</span><span style="color:#C3E88D"> restic</span><span style="color:#C3E88D"> repository...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">created</span><span style="color:#C3E88D"> /root/restic/conf/excludes.conf</span></span>
<span class="line"><span style="color:#FFCB6B">wrote</span><span style="color:#C3E88D"> B2</span><span style="color:#C3E88D"> settings</span></span>
<span class="line"><span style="color:#89DDFF">!!</span><span style="color:#FFCB6B"> generated</span><span style="color:#C3E88D"> restic</span><span style="color:#C3E88D"> repository</span><span style="color:#C3E88D"> password:</span><span style="color:#C3E88D"> ••••••••••••••••••••</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Do</span><span style="color:#C3E88D"> you</span><span style="color:#C3E88D"> want</span><span style="color:#C3E88D"> to</span><span style="color:#C3E88D"> initialize</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> restic</span><span style="color:#C3E88D"> repo</span><span style="color:#C3E88D"> now?</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Note the two passwords that were generated and stored, preceded with <code>!!</code> in the setup output.</p><p>Answer <code>y</code> to have restic to connect to B2 and establish its backup base. You’ll be prompted for that newly-generated restic repository password, which will be required for interacting with any of that backup data. It’s stored in <code>/root/restic/conf</code> along with the B2 and backup MySQL credentials and an exclude list for the backup set. (If there’s a way to automate backups without storing these secrets please drop me a line or submit a pull request!)</p><p>That’s it. If everything worked, you’ve established an encrypted B2 store and set up the pieces you can use to run and automate backups. Let’s try them!</p><h2>Step 2: Test</h2><p>Run a MySQL dump with the included shell script.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">/root/restic/mysql-backup.sh</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This will create a compressed dump for each database and store it in <code>/home/forge/backup/mysql/YYYY-MM-DD/DBNAME-{timestamp}.gz</code>.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Running</span><span style="color:#C3E88D"> MySQL</span><span style="color:#C3E88D"> backup</span><span style="color:#C3E88D"> routine...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">backing</span><span style="color:#C3E88D"> up</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">forge</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D"> →</span><span style="color:#C3E88D"> /home/forge/backup/mysql/2019-02-26/forge-014202.gz</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Pruning</span><span style="color:#C3E88D"> old</span><span style="color:#C3E88D"> backups...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Done.</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The timestamp will keep your dump archives from overwriting each other unless you can somehow run your backups more than once per second. By default, the backup routine will prune dumps that are more than seven days old.</p><p>Now run the backup script, which will ignore .git directories and otherwise back up everything in /home/forge to a new snapshot stored in your B2 bucket.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">/root/restic/restic-backup.sh</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Result:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Running</span><span style="color:#C3E88D"> restic</span><span style="color:#C3E88D"> backup</span><span style="color:#C3E88D"> →</span><span style="color:#C3E88D"> host...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Running</span><span style="color:#C3E88D"> MySQL</span><span style="color:#C3E88D"> backup</span><span style="color:#C3E88D"> routine...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">backing</span><span style="color:#C3E88D"> up</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">dbtwo</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D"> →</span><span style="color:#C3E88D"> /home/forge/backup/mysql/2019-02-25/dbtwo-110001.gz</span></span>
<span class="line"><span style="color:#FFCB6B">backing</span><span style="color:#C3E88D"> up</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">forge</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D"> →</span><span style="color:#C3E88D"> /home/forge/backup/mysql/2019-02-25/forge-110001.gz</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Pruning</span><span style="color:#C3E88D"> old</span><span style="color:#C3E88D"> backups...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">Done.</span></span>
<span class="line"><span style="color:#FFCB6B">using</span><span style="color:#C3E88D"> parent</span><span style="color:#C3E88D"> snapshot</span><span style="color:#C3E88D"> 5b0f1ba0</span></span>
<span class="line"><span style="color:#FFCB6B">scan</span><span style="color:#EEFFFF"> [/home/forge]</span></span>
<span class="line"><span style="color:#89DDFF">[</span><span style="color:#EEFFFF">0:00</span><span style="color:#89DDFF">]</span><span style="color:#EEFFFF"> 5027 directories, 22746 files, 382.857 MiB</span></span>
<span class="line"><span style="color:#FFCB6B">scanned</span><span style="color:#F78C6C"> 5027</span><span style="color:#C3E88D"> directories,</span><span style="color:#F78C6C"> 22746</span><span style="color:#C3E88D"> files</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> 0:00</span></span>
<span class="line"><span style="color:#89DDFF">[</span><span style="color:#EEFFFF">0:09</span><span style="color:#89DDFF">]</span><span style="color:#EEFFFF"> 100.00%  382.857 MiB / 382.857 MiB  27773 / 27773 items  0 errors  ETA 0:00 </span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">duration:</span><span style="color:#C3E88D"> 0:09</span></span>
<span class="line"><span style="color:#FFCB6B">snapshot</span><span style="color:#C3E88D"> f98c3c33</span><span style="color:#C3E88D"> saved</span></span>
<span class="line"><span style="color:#FFCB6B">snapshots</span><span style="color:#C3E88D"> for</span><span style="color:#EEFFFF"> (host [host], paths </span><span style="color:#89DDFF">[</span><span style="color:#EEFFFF">/home/forge</span><span style="color:#89DDFF">]</span><span style="color:#EEFFFF">):</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">keep</span><span style="color:#F78C6C"> 2</span><span style="color:#C3E88D"> snapshots:</span></span>
<span class="line"><span style="color:#FFCB6B">ID</span><span style="color:#C3E88D">        Date</span><span style="color:#C3E88D">                 Host</span><span style="color:#C3E88D">        Tags</span><span style="color:#C3E88D">        Directory</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">5b0f1ba0</span><span style="color:#C3E88D">  2019-02-25</span><span style="color:#C3E88D"> 11:00:07</span><span style="color:#C3E88D">  host</span><span style="color:#C3E88D">                    /home/forge</span></span>
<span class="line"><span style="color:#FFCB6B">f98c3c34</span><span style="color:#C3E88D">  2019-02-26</span><span style="color:#C3E88D"> 11:00:12</span><span style="color:#C3E88D">  host</span><span style="color:#C3E88D">                    /home/forge</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">2</span><span style="color:#C3E88D"> snapshots</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">counting</span><span style="color:#C3E88D"> files</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> repo</span></span>
<span class="line"><span style="color:#FFCB6B">building</span><span style="color:#C3E88D"> new</span><span style="color:#C3E88D"> index</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> repo</span></span>
<span class="line"><span style="color:#89DDFF">[</span><span style="color:#EEFFFF">0:42</span><span style="color:#89DDFF">]</span><span style="color:#EEFFFF"> 100.00%  99 / 99 packs</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">repository</span><span style="color:#C3E88D"> contains</span><span style="color:#F78C6C"> 99</span><span style="color:#C3E88D"> packs</span><span style="color:#EEFFFF"> (28410 </span><span style="color:#C3E88D">blobs</span><span style="color:#EEFFFF">) with 410.745 MiB</span></span>
<span class="line"><span style="color:#FFCB6B">processed</span><span style="color:#F78C6C"> 28410</span><span style="color:#C3E88D"> blobs:</span><span style="color:#F78C6C"> 0</span><span style="color:#C3E88D"> duplicate</span><span style="color:#C3E88D"> blobs,</span><span style="color:#C3E88D"> 0B</span><span style="color:#C3E88D"> duplicate</span></span>
<span class="line"><span style="color:#FFCB6B">load</span><span style="color:#C3E88D"> all</span><span style="color:#C3E88D"> snapshots</span></span>
<span class="line"><span style="color:#FFCB6B">find</span><span style="color:#C3E88D"> data</span><span style="color:#C3E88D"> that</span><span style="color:#C3E88D"> is</span><span style="color:#C3E88D"> still</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> use</span><span style="color:#C3E88D"> for</span><span style="color:#F78C6C"> 2</span><span style="color:#C3E88D"> snapshots</span></span>
<span class="line"><span style="color:#89DDFF">[</span><span style="color:#EEFFFF">0:01</span><span style="color:#89DDFF">]</span><span style="color:#EEFFFF"> 100.00%  2 / 2 snapshots</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">found</span><span style="color:#F78C6C"> 28410</span><span style="color:#C3E88D"> of</span><span style="color:#F78C6C"> 28410</span><span style="color:#C3E88D"> data</span><span style="color:#C3E88D"> blobs</span><span style="color:#C3E88D"> still</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> use,</span><span style="color:#C3E88D"> removing</span><span style="color:#F78C6C"> 0</span><span style="color:#C3E88D"> blobs</span></span>
<span class="line"><span style="color:#FFCB6B">will</span><span style="color:#C3E88D"> remove</span><span style="color:#F78C6C"> 0</span><span style="color:#C3E88D"> invalid</span><span style="color:#C3E88D"> files</span></span>
<span class="line"><span style="color:#FFCB6B">will</span><span style="color:#C3E88D"> delete</span><span style="color:#F78C6C"> 0</span><span style="color:#C3E88D"> packs</span><span style="color:#C3E88D"> and</span><span style="color:#C3E88D"> rewrite</span><span style="color:#F78C6C"> 0</span><span style="color:#C3E88D"> packs,</span><span style="color:#C3E88D"> this</span><span style="color:#C3E88D"> frees</span><span style="color:#C3E88D"> 0B</span></span>
<span class="line"><span style="color:#FFCB6B">counting</span><span style="color:#C3E88D"> files</span><span style="color:#C3E88D"> in</span><span style="color:#C3E88D"> repo</span></span>
<span class="line"><span style="color:#89DDFF">[</span><span style="color:#EEFFFF">0:11</span><span style="color:#89DDFF">]</span><span style="color:#EEFFFF"> 100.00%  99 / 99 packs</span></span>
<span class="line"></span>
<span class="line"><span style="color:#FFCB6B">finding</span><span style="color:#C3E88D"> old</span><span style="color:#C3E88D"> index</span><span style="color:#C3E88D"> files</span></span>
<span class="line"><span style="color:#FFCB6B">saved</span><span style="color:#C3E88D"> new</span><span style="color:#C3E88D"> indexes</span><span style="color:#C3E88D"> as</span><span style="color:#EEFFFF"> [3f3038a6]</span></span>
<span class="line"><span style="color:#FFCB6B">remove</span><span style="color:#F78C6C"> 2</span><span style="color:#C3E88D"> old</span><span style="color:#C3E88D"> index</span><span style="color:#C3E88D"> files</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">done</span></span>
<span class="line"><span style="color:#FFCB6B">create</span><span style="color:#C3E88D"> exclusive</span><span style="color:#C3E88D"> lock</span><span style="color:#C3E88D"> for</span><span style="color:#C3E88D"> repository</span></span>
<span class="line"><span style="color:#FFCB6B">load</span><span style="color:#C3E88D"> indexes</span></span>
<span class="line"><span style="color:#FFCB6B">check</span><span style="color:#C3E88D"> all</span><span style="color:#C3E88D"> packs</span></span>
<span class="line"><span style="color:#FFCB6B">check</span><span style="color:#C3E88D"> snapshots,</span><span style="color:#C3E88D"> trees</span><span style="color:#C3E88D"> and</span><span style="color:#C3E88D"> blobs</span></span>
<span class="line"><span style="color:#FFCB6B">no</span><span style="color:#C3E88D"> errors</span><span style="color:#C3E88D"> were</span><span style="color:#C3E88D"> found</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>As you can see from the output, it’s thoroughly scanning the backup target and making snapshots. It also prunes (removing duplicate data) and double-checks backup metadata against what’s actually stored. It’s nice.</p><p>Now try mounting your backup data like you’ve plugged in a thumb drive. (This happens via <code>fuse</code>, if you’re curious, which is included by default with Ubuntu.)</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">/root/restic/restic-mount.sh</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Result:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">Mounting</span><span style="color:#C3E88D"> backup</span><span style="color:#C3E88D"> at</span><span style="color:#C3E88D"> /mnt/restic...</span></span>
<span class="line"><span style="color:#FFCB6B">----------------------------------------</span></span>
<span class="line"><span style="color:#FFCB6B">password</span><span style="color:#C3E88D"> is</span><span style="color:#C3E88D"> correct</span></span>
<span class="line"><span style="color:#FFCB6B">Now</span><span style="color:#C3E88D"> serving</span><span style="color:#C3E88D"> the</span><span style="color:#C3E88D"> repository</span><span style="color:#C3E88D"> at</span><span style="color:#C3E88D"> /mnt/restic</span></span>
<span class="line"><span style="color:#FFCB6B">Don</span><span style="color:#FFCB6B">'t forget to umount after quitting!</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Now you can <code>cd /mnt/restic</code> to browse your snapshots.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">root@sudden-valley:/mnt/restic</span></span>
<span class="line"><span style="color:#FFCB6B">ls</span><span style="color:#C3E88D"> -la</span></span>
<span class="line"><span style="color:#FFCB6B">total</span><span style="color:#F78C6C"> 4</span></span>
<span class="line"><span style="color:#FFCB6B">dr-xr-xr-x</span><span style="color:#F78C6C"> 1</span><span style="color:#C3E88D"> root</span><span style="color:#C3E88D"> root</span><span style="color:#F78C6C">    0</span><span style="color:#C3E88D"> Feb</span><span style="color:#F78C6C"> 26</span><span style="color:#C3E88D"> 01:48</span><span style="color:#C3E88D"> .</span></span>
<span class="line"><span style="color:#FFCB6B">drwxr-xr-x</span><span style="color:#F78C6C"> 3</span><span style="color:#C3E88D"> root</span><span style="color:#C3E88D"> root</span><span style="color:#F78C6C"> 4096</span><span style="color:#C3E88D"> Feb</span><span style="color:#F78C6C"> 26</span><span style="color:#C3E88D"> 01:47</span><span style="color:#C3E88D"> ..</span></span>
<span class="line"><span style="color:#FFCB6B">dr-xr-xr-x</span><span style="color:#F78C6C"> 1</span><span style="color:#C3E88D"> root</span><span style="color:#C3E88D"> root</span><span style="color:#F78C6C">    0</span><span style="color:#C3E88D"> Feb</span><span style="color:#F78C6C"> 26</span><span style="color:#C3E88D"> 01:48</span><span style="color:#C3E88D"> hosts</span></span>
<span class="line"><span style="color:#FFCB6B">dr-xr-xr-x</span><span style="color:#F78C6C"> 1</span><span style="color:#C3E88D"> root</span><span style="color:#C3E88D"> root</span><span style="color:#F78C6C">    0</span><span style="color:#C3E88D"> Feb</span><span style="color:#F78C6C"> 26</span><span style="color:#C3E88D"> 01:48</span><span style="color:#C3E88D"> ids</span></span>
<span class="line"><span style="color:#FFCB6B">dr-xr-xr-x</span><span style="color:#F78C6C"> 1</span><span style="color:#C3E88D"> root</span><span style="color:#C3E88D"> root</span><span style="color:#F78C6C">    0</span><span style="color:#C3E88D"> Feb</span><span style="color:#F78C6C"> 26</span><span style="color:#C3E88D"> 01:48</span><span style="color:#C3E88D"> snapshots</span></span>
<span class="line"><span style="color:#FFCB6B">dr-xr-xr-x</span><span style="color:#F78C6C"> 1</span><span style="color:#C3E88D"> root</span><span style="color:#C3E88D"> root</span><span style="color:#F78C6C">    0</span><span style="color:#C3E88D"> Feb</span><span style="color:#F78C6C"> 26</span><span style="color:#C3E88D"> 01:48</span><span style="color:#C3E88D"> tags</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>When you’re done unmount that data with <code>umount</code>, which is not a typo.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">umount</span><span style="color:#C3E88D"> /mnt/restic</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>Step 3: Automate</h2><p>Add a scheduled task to run <code>/root/restic/restic-backup.sh &gt;/dev/null 2&gt;&1</code> at whatever interval you want.</p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/scheduler.CVbc4A_i_2fLd8t.webp" type="image/webp"><source src="https://workingconcept.com//_astro/scheduler.CVbc4A_i_Z2dAGqn.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/scheduler.CVbc4A_i_Z217Mcu.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/scheduler.CVbc4A_i_r0NMT.png" decoding="async" loading="lazy" alt="Screenshot of Forge New Scheduled Job pane." width="2538" height="1432"> </picture> <figcaption> <p> Choose your schedule, just run as root.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I adore that Forge makes it easy to check scheduled log output from its interface, so it’s easy to confirm that the routine is running like you’d expect.</p><p>As with all backups, it’s a good idea to mount and verify them every now and then to be sure they’re actually working. I set a monthly calendar reminder to do this.</p><p>Consider leaving a comment or sending an email if you have questions or suggestions!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Journey From Twig to Gatsby]]></title>
            <link>https://workingconcept.com/blog/journey-from-twig-to-gatsby</link>
            <guid>https://workingconcept.com/blog/journey-from-twig-to-gatsby</guid>
            <pubDate>Tue, 05 Nov 2019 14:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/oregon-trail.DAJkuLUJ_Z1LlCtR.webp" type="image/webp"><source src="https://workingconcept.com//_astro/oregon-trail.DAJkuLUJ_1FzaHR.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/oregon-trail.DAJkuLUJ_Z2982Qy.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/oregon-trail.DAJkuLUJ_23YIV8.jpg" decoding="async" loading="lazy" alt="oil painting of Oregon Trail party at sunset" width="1000" height="618"> </picture> <figcaption> <p> The Oregon Trail, 1869, oil on canvas  </p> </figcaption> </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p class="text-2xl pb-8">On April 11, 1849, William Swain rose early and began packing for a long overland journey that would take him far from home.</p><p>The twenty-seven-year-old was destined for the famed riches of the California gold rush. His brother George waited eagerly behind, vowing to make the trip soon after if what he read in the papers was true.<br><br>It did not go well for William.</p><p>The arduous journey nearly took his life, and he eventually arrived on the west coast to find a wild, rapidly-growing town where exorbitant prices catered to those that struck gold. William did not.</p><p>He wrote home in November 1850:</p><blockquote><p>“I have made up my mind that I have got enough of California and am coming home as fast as I can.”</p></blockquote><p>He returned home without the riches he was certain he’d find, but William was alive and spent years recounting stories of his journey.</p><hr><p>The words you’re reading were published on my first <a href="https://www.gatsbyjs.org/">GatsbyJS</a> frontend for <a href="https://craftcms.com/">Craft CMS</a>. It’s glowing with the white-hot magic of the <a href="https://jamstack.org/">JAMstack</a>.</p><p>I left my comfortable Twig microcosm to see what it looks like on the other side, and I’d like to invite you to be my George. To see if my journey might inform yours.</p><p>It took a few solid weeks before I was comfortable pointing workingconcept.com to <a href="https://www.netlify.com/">Netlify</a>, but I finally got there in spite of an ongoing list of problems to solve.</p><p>I was eager to see if I could pull off this stack jump without requiring compromises for a future client. I’d only like to build projects this way if the resulting experience could be indistinguishable for a content editor.</p><p>Maybe you’re curious too, busy with other things because you use your time wisely and didn’t run at this like a maniac. Great! I’d like to share what I’ve learned and what I have left to figure out.</p></div><div id="background" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Background</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><a href="{entry:8236@1:url||https://workingconcept.com/blog/craft-cms-and-jamstack}">I wrote earlier</a> about my interest in headless Craft projects. Aside from being new and interesting, they’d push me to build more dynamic frontends, improve site performance and security, and eliminate some of the burden of hosting. It used to be that a JavaScript-heavy frontend came with tradeoffs like poor SEO and a convoluted relationship to a CMS, but projects like <a href="https://www.gatsbyjs.org/">Gatsby</a> and <a href="https://gridsome.org/">Gridsome</a> make it easier to serve a performant, well-rounded website.</p><p>With a rush of enthusiasm from <a href="https://dotall.com/2019">Dot All</a>, it took me about two weeks to get to the point where I could switch to a Gatsby frontend. I made the leap once I figured no human or spider (hi, Google!) would spot an immediate difference.</p><p>But this didn’t take me two weeks.</p><p>I started more than a year ago and repeatedly smacked into hurdles only to wander off and return later. I kept my eye on all the new fancy and started noticing patterns, gently folding them into my work. This helped ease the conceptual transition between workflows and gives the false impression I had a plan:</p><ul><li>Challenged myself to build a simple set of components using <a href="https://vuejs.org/">Vue</a>, <a href="https://reactjs.org/">React</a>, <a href="https://svelte.dev/">Svelte</a> and <a href="https://stenciljs.com/">Stencil</a> and learned that they’re similar kinds of building blocks. (Seriously, build an accordion with each framework and you will feel your brain expand!)</li><li>Started organizing my Twig templates into more discrete nuggets, with the help of <a href="https://github.com/ben-rogerson/craft-storybook-starter">Ben Rogerson’s Storybook starter project</a>. This oriented my thinking toward components.</li><li>Painstakingly created and then ditched my custom webpack config for <a href="https://laravel-mix.com/">Laravel Mix</a>, which let me use webpack while tripping over it less.</li><li>Fully cached some Craft projects, HTML and all, with Cloudflare and CloudFront. I routed some form submissions to a Laravel handler turned Slim app <a href="https://github.com/workingconcept/serverless-form-handler">turned Lambda function</a>.</li><li>Built and deployed a handful of small Lambda functions, which <a href="https://serverless.com/">the Serverless framework</a> made easier and more fun.</li><li>Built a tiny, markdown-powered personal site using both Gridsome <em>and</em> Gatsby to get my first look at those projects and start querying things with GraphQL.</li><li>Tried and failed several times to rebuild this site with Gatsby.</li></ul></div><div id="mapping-things-out" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Mapping Things Out</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This site made a good test project because it’s fairly simple and I’ve rebuilt it bunches of times. It helped to translate a familiar thing rather than build something new from scratch.</p></div><div class="table-block pl-6 copy overflow-x-auto max-w-full lg:mx-auto lg:pl-0 lg:max-w-lg"> <table> <thead> <tr> <th class="text-left" width key="0"> Sections </th><th class="text-left" width key="1"> Singles </th><th class="text-left" width key="2"> Globals </th><th class="text-left" width key="3"> Locales </th><th class="text-left" width key="4"> Sites </th><th class="text-left" width key="5"> Forms </th><th class="text-left" width key="6"> Volumes </th> </tr> </thead> <tbody> <tr key="row-0"> <td key="row-0-column-0" class="text-left"> 4 </td><td key="row-0-column-1" class="text-left"> 12 </td><td key="row-0-column-2" class="text-left"> 4 fields </td><td key="row-0-column-3" class="text-left"> 1 </td><td key="row-0-column-4" class="text-left"> 1 </td><td key="row-0-column-5" class="text-left"> 4 </td><td key="row-0-column-6" class="text-left"> 8 (local) </td> </tr> </tbody> </table> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>It had a modest number Sections and Entries, along with a few forms that already posted to a <a href="https://github.com/workingconcept/serverless-form-handler">form handler Lambda function</a> that was part of an earlier quest. I was hosting images on local Asset Volumes behind Cloudflare, using <a href="https://plugins.craftcms.com/imager">Imager</a> for transforms.</p><p>I hand rolled SEO using templates for meta details, structured data and an XML sitemap, along with some simple fields for overriding defaults. The only third-party plugins responsible for content are <a href="https://plugins.craftcms.com/super-table">Super Table</a> and <a href="https://plugins.craftcms.com/tablemaker">Table Maker</a>. (Table Maker doesn’t yet support GraphQL so I’m using <a href="https://github.com/supercool/tablemaker/pull/19">a fork I submitted as a PR</a>.)</p><p>At a high level, this left me with a clear to do list:</p><ul class="todo"><li>Map Singles to Gatsby pages.</li><li>Generate blog, plugin, and tag pages.</li><li>Generate blog RSS, XML and JSON feeds so both readers continue to enjoy their subscriptions.</li><li>Fetch plugin changelogs at build time and make them available to plugin pages.</li><li>Maintain SEO sitemap, on-page meta and JSON-LD customization.</li><li>Figure out how to handle Asset Volumes since I want Craft managing Assets without those files being hosted on the CMS stack.</li><li>Choose a strategy for handling image transforms.</li><li>Translate the Twig frontend to React (and keep using Tailwind).</li><li>Get Live Preview working.</li><li>Pick a host for the static site and decide on a build strategy.</li><li>Set up separate domains for the static site, Craft CMS, and Assets.</li></ul><h3>Microservices</h3><p>This could be its own post, but I wanted to mention Lambda functions since they’re integral and I knew I’d rely on them <em>before</em> I mapped out my goals.</p><p>Reducing Craft to an API means other things have to pick up some slack. I’ve tried to adhere to The JAMstack Way™ by embracing microservices, using discrete little APIs rather than a pile of them on a specific stack. I did this by using a few Lambda functions, which are just bits of code AWS runs for you with on-demand infrastructure.<br></p><p>You write the code and get it into the AWS machinery, and AWS takes care of everything else. No servers to maintain or scale.</p><p>Like anything AWS, it’s also a double-edged sword. There’s a little universe of useful things you can tie together for your Lambda function if you want, like a CDN or caching or S3 storage. Machine learning, even. If you’re not a devops engineer setting it up might be enough to send you fleeing. I happily worked with <a href="https://serverless.com/">the Serverless framework</a>, which abstracted away configuration so I could focus on writing and deploying Lambda functions. I wrote some from scratch and forked an existing project for a minor adjustment, which we’ll come to later. </p><p>If you’d like to play with Lambda functions, I highly recommend starting with Serverless unless configuring AWS sparks joy.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>serverless deploy</span></span></code></pre> <figcaption class="-my-4"> <p>Deploying a Lambda function from your local project with Serverless.</p>
 </figcaption> </figure><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>serverless deploy --stage prod</span></span></code></pre> <figcaption class="-my-4"> <p>Deploying a Lambda function to production.</p>
 </figcaption> </figure><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/serverless-dashboard.DVV_D3r5_ZFdXXG.webp" type="image/webp"><source src="https://workingconcept.com//_astro/serverless-dashboard.DVV_D3r5_2cdHko.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/serverless-dashboard.DVV_D3r5_Zi1wGT.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/serverless-dashboard.DVV_D3r5_1H4qj0.png" decoding="async" loading="lazy" alt="screenshot of a form handler invocation in the Serverless dashboard with timing and debug info" width="2616" height="2078"> </picture> <figcaption> <p> Looking at a form handler invocation in the Serverless dashboard. Monitoring and performance metrics are nice perks.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you need a little API to do something and you can write code to do it in JavaScript<sup id="fnref1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>, you can write and deploy a Lambda function. It can be ten or ten thousand lines of code, whatever it takes to do the job.</p></div><div id="building-the-site-structure" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Building the Site Structure</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>Craftsby Connection</h3><p>The first step on the Craft CMS side was to have it speak GraphQL. The Element API would have worked too, but with more time to configure and adjust later.</p><p>I started with <a href="https://github.com/markhuot/craftql">CraftQL</a>, later replaced by Craft’s native GraphQL support. Now it’s as simple as setting up a private key for Gatsby and adding <code>'api' =&gt; 'graphql/api'</code> to <code>config/routes.php</code>.</p><p>On the Gatsby end I kicked off a new starter project with <code>gatsby new gatsby-site</code>, adding TailwindCSS and plugins mostly for SEO and minor site features. The complete list:</p><ul><li><code>gatsby-plugin-canonical-urls</code> for adding <code>rel=canonical</code> to pages</li><li><code>gatsby-plugin-catch-links</code> for handling internal links, which Gatsby wants in <code>&lt;Link /&gt;</code> components, when they’re otherwise embedded in markdown or HTML from the CMS</li><li><code>gatsby-plugin-disqus</code> for mostly silent blog comments</li><li><code>gatsby-plugin-fathom</code> for self-hosted <a href="https://usefathom.com/">Fathom</a> analytics</li><li><code>gatsby-plugin-manifest</code> for favicon + PWA theme stuff</li><li><code>gatsby-plugin-netlify</code> for easy redirects and URL wrangling with Netlify</li><li><code>gatsby-plugin-offline</code> for impossibly simple service worker setup</li><li><code>gatsby-plugin-postcss</code> for compiling PostCSS</li><li><code>gatsby-plugin-purgecss</code> for purging unused CSS</li><li><code>gatsby-plugin-react-helmet</code> for managing <code>&lt;head&gt;</code></li><li><code>gatsby-plugin-remove-trailing-slashes</code> for keeping trailing slashes out of generated URLs</li><li><code>gatsby-plugin-robots-txt</code> for generating a robots.txt file</li><li><code>gatsby-plugin-sharp</code> for manipulating some images at build time</li><li><code>gatsby-plugin-sitemap</code> for generating an XML sitemap</li><li><code>gatsby-plugin-svgr</code> for loading SVG files into templates</li></ul><p>A vital part of the Gatsby configuration, which arrived like mana from heaven, was <a href="https://gist.github.com/monachilada/af7e92a86e0d27ba47a8597ac4e4b105">Mike Pierce’s gatsby-config.js</a>. He shared it with the warning that it wouldn’t scale beautifully, but it solved two things I hadn’t.</p><p>The first was keeping the GraphQL endpoint and token in environment variables and out of git. Not revolutionary, but a sensible thing I wasn’t yet doing.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> craftGqlUrl </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF"> process</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">env</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">CRAFT_GQL_URL</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> craftGqlToken </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF"> process</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">env</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">CRAFT_GQL_TOKEN</span><span style="color:#89DDFF">;</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The second gem was something I tried and failed at, which was passing Craft’s Live Preview token on to Gatsby when it exists:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#FFCB6B">developMiddleware</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF;font-style:italic"> app</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#EEFFFF">  app</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">use</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">*</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> (</span><span style="color:#EEFFFF;font-style:italic">req</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> res</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> next</span><span style="color:#89DDFF">)</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    if</span><span style="color:#F07178"> (</span><span style="color:#EEFFFF">req</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">query</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">token</span><span style="color:#F07178">) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">      store</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">set</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">X-Craft-Token</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> req</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">query</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">token</span><span style="color:#F07178">)</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#82AAFF">      sourceNodes</span><span style="color:#F07178">()</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">    }</span></span>
<span class="line"><span style="color:#82AAFF">    next</span><span style="color:#F07178">()</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#F07178">)</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">},</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you would have figured this out in less than two days, you and I are different creatures. And you should probably hang out with Mike Pierce and all his unicorn friends.</p><p>Now for some pages.</p><h3>Singles</h3><p>I started by mapping Singles to Gatsby pages. So the <em>Homepage</em> and <em>About</em> Singles, for example, found their homes in <code>pages/index.js</code> and <code>pages/about.js</code> in Gatsby. People seem to have strong feelings about Singles and you may be one of them, so we’ll leave it at that.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>pages/</span></span>
<span class="line"><span>    404.js</span></span>
<span class="line"><span>    about.js</span></span>
<span class="line"><span>    blog.js</span></span>
<span class="line"><span>    blog/tags.js</span></span>
<span class="line"><span>    colophon.js</span></span>
<span class="line"><span>    feedback.js</span></span>
<span class="line"><span>    haq.js</span></span>
<span class="line"><span>    index.js</span></span>
<span class="line"><span>    plugins.js</span></span>
<span class="line"><span>    privacy.js</span></span>
<span class="line"><span>    project-brief.js</span></span>
<span class="line"><span>    services.js</span></span>
<span class="line"><span>    support.js</span></span>
<span class="line"><span>    work.js</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Each page has its own GraphQL query to grab whatever content it needs. It looks different in Gatsby and Gridsome templates, but it’s the exact same idea.</p><h3>Entries</h3><p>The same goes for generating dynamic (Entry detail) pages with Gatsby and Gridsome: use the project’s API to define, all in one place, the pages that need to be generated at build time.</p><p>With Gatsby, this happens in <code>gatsby-node.js</code>.</p><p>You’ll see examples of this in starter projects and documentation: get the results of a GraphQL query, then loop through them and tell Gatsby how to translate each item into a page.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>templates/</span></span>
<span class="line"><span>    blog/</span></span>
<span class="line"><span>        entry.js</span></span>
<span class="line"><span>        tag.js</span></span>
<span class="line"><span>    landing/</span></span>
<span class="line"><span>        entry.js</span></span>
<span class="line"><span>    plugins/</span></span>
<span class="line"><span>        changelog.js</span></span>
<span class="line"><span>        overview.js</span></span>
<span class="line"><span>        support.js</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>Synchronous Challenges<br></h3><p>This was painless until I needed two things that forced a lot of <del>swearing</del> learning:</p><ol><li>Grab each plugin’s GitHub changelog at build time, to be used on the detail page.</li><li>Generate blog RSS, Atom and JSON feeds <em>including full post content</em>, which is made up of Matrix blocks.</li></ol><p>Each task needed to happen at a specific point in the build process. </p><p>I first needed the plugin’s changelog in order to parse it for the plugin page template. I struggled enough with the build process that my first working solution was a tiny external API, built with a Lambda function, simply to take a changelog URL and return it as parsed JSON. This was an elaborate way of not learning to use promises, which is what I eventually did instead: used axios to fetch the changelog and <code>changelog-parser</code> to turn it into an object before dropping that object into the created page context.<br></p><p>I wound up building my feed content by waiting until after the blog post HTML was generated. It’s gross and it works.</p><p>When you’re using Gatsby’s <code>createPages</code> API, you’re returning a series of promises that can be executed in any sequence. If you’re going to need things to happen in a specific order, you’re going to need to get comfortable using promises and method chaining in order to do that.<br></p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#89DDFF">exports.</span><span style="color:#82AAFF">createPages</span><span style="color:#89DDFF"> =</span><span style="color:#C792EA"> async</span><span style="color:#89DDFF"> ({</span><span style="color:#EEFFFF;font-style:italic"> graphql</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> actions</span><span style="color:#89DDFF"> })</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  /**</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   * blogPosts, blogTags, plugins, landingPages and changelogPages</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   * are all promises following the pattern: </span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   * `const blogPosts = graphql(query).then(result => { </span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   *    result.posts.forEach(post => {</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   *      createPage() </span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   *    })</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   *  })`</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">   */</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#FFCB6B"> Promise</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">all</span><span style="color:#F07178">([</span><span style="color:#EEFFFF">blogPosts</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> blogTags</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> plugins</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> landingPages</span><span style="color:#F07178">])</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">then</span><span style="color:#F07178">(</span><span style="color:#89DDFF">()</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">    // changelogPages come last</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    return</span><span style="color:#FFCB6B"> Promise</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">all</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">changelogPages</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre> <figcaption class="-my-4"> <p>Distilled example of how I needed to chain promises so changelog pages were built last.</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The changelog pages also include a round trip to fetch the changelog markdown from GitHub, but the promise chaining works in the same way.</p><p>Building blog feeds was something that would’ve taken minutes using Twig but had me stuck for hours trying to get my head around SSR, or Server Side Rendering.</p><p>You already know what Server Side Rendering is because that’s all Twig has ever done: it executes server-side PHP to generate markup and return it all at once to the browser. React uses node to render something like HTML skeletons at build time—you’ll see the static HTML files in the build folder—and these are important for things like SEO and RSS feeds that can’t rely on JavaScript to pop in and manage what’s sent to the browser.</p><p>Precisely how this happens is still a mystery to me.</p><p>My struggle with the feeds was the content. Because it’s markup generated from a series of Matrix blocks, it’s not as simple as working with a single markdown or rich text field that can be primped for the feed. Some transformation into HTML is required, and yet rendering nested React components in node is not trivial.</p><p><code>ReactDOMServer.renderToString()</code> exists for this purpose, but I tried and failed to get it to generate my post content. This subject could be a long, sad post of its own but I ended up getting around it for now by...</p><ul><li>using <code>node-html-parser</code> in <code>gatsby-node.js</code> to pull the content from already-generated blog post pages</li><li>adding some code in <code>gatsby-node.js</code> to escape the post content’s code snippets and replace markup intended for lazyloading</li><li>using the <code>exenv</code> package’s <code>canUseDOM</code> as a switch in my <code>Picture</code> component to avoid component lazyloading in a node context</li></ul><p>I’m not proud of this, but it’s a shiningly clear example of how Twig and React are different animals.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#546E7A;font-style:italic">/**</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * Extracts &#x3C;article> from generated HTML and modifies its format for syndication.</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * </span><span style="color:#89DDFF;font-style:italic">@</span><span style="color:#C792EA;font-style:italic">param</span><span style="color:#89DDFF;font-style:italic"> {</span><span style="color:#FFCB6B;font-style:italic">*</span><span style="color:#89DDFF;font-style:italic">}</span><span style="color:#EEFFFF;font-style:italic"> entry</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> */</span></span>
<span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> getPostContent </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF;font-style:italic"> entry</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> filepath</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">./public/</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> +</span><span style="color:#EEFFFF"> entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">uri</span><span style="color:#89DDFF"> +</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">/index.html</span><span style="color:#89DDFF">'</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> generatedMarkup</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> fs</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">readFileSync</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">filepath</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">utf-8</span><span style="color:#89DDFF">'</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> pageDOM</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> HTMLParser</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">parse</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">generatedMarkup</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> {</span><span style="color:#F07178"> pre</span><span style="color:#89DDFF">:</span><span style="color:#FF9CAC"> true</span><span style="color:#89DDFF"> }</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> article</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> pageDOM</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">querySelector</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">article</span><span style="color:#89DDFF">'</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // redacted for author's dignity; lots of string-manipulating HTML</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#EEFFFF"> articleMarkup</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">/**</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * Builds a single feed object to be output in different formats.</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * https://www.npmjs.com/package/feed</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> */</span></span>
<span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> buildFeed </span><span style="color:#89DDFF">=</span><span style="color:#EEFFFF;font-style:italic"> entries</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> feed</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> new</span><span style="color:#82AAFF"> Feed</span><span style="color:#F07178">(</span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">    // ...</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">  entries</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">forEach</span><span style="color:#F07178">(</span><span style="color:#EEFFFF;font-style:italic">entry</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#EEFFFF">    feed</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">addItem</span><span style="color:#F07178">(</span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#F07178">      title</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">title</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      id</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">https://workingconcept.com/</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> +</span><span style="color:#EEFFFF"> entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">uri</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      link</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">https://workingconcept.com/</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> +</span><span style="color:#EEFFFF"> entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">uri</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      published</span><span style="color:#89DDFF">:</span><span style="color:#82AAFF"> moment</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">postDate</span><span style="color:#F07178">)</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">toDate</span><span style="color:#F07178">()</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      date</span><span style="color:#89DDFF">:</span><span style="color:#82AAFF"> moment</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">dateUpdated</span><span style="color:#F07178">)</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">toDate</span><span style="color:#F07178">()</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      content</span><span style="color:#89DDFF">:</span><span style="color:#82AAFF"> getPostContent</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">entry</span><span style="color:#F07178">)</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      author</span><span style="color:#89DDFF">:</span><span style="color:#F07178"> [</span></span>
<span class="line"><span style="color:#89DDFF">        {</span></span>
<span class="line"><span style="color:#F07178">          name</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">author</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">name</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">          email</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> entry</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">author</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">email</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">        },</span></span>
<span class="line"><span style="color:#F07178">      ]</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    }</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#EEFFFF"> feed</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">exports.</span><span style="color:#82AAFF">onPostBuild</span><span style="color:#89DDFF"> =</span><span style="color:#C792EA"> async</span><span style="color:#89DDFF"> ({</span><span style="color:#EEFFFF;font-style:italic"> graphql</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF;font-style:italic"> actions</span><span style="color:#89DDFF"> })</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> blogPosts</span><span style="color:#89DDFF"> =</span><span style="color:#82AAFF"> graphql</span><span style="color:#F07178">(</span><span style="color:#89DDFF">`</span></span>
<span class="line"><span style="color:#C3E88D">    query {</span></span>
<span class="line"><span style="color:#C3E88D">      craftGql {</span></span>
<span class="line"><span style="color:#C3E88D">        entries(section: "blog", limit: 50, orderBy: "postDate desc") {</span></span>
<span class="line"><span style="color:#C3E88D">          id</span></span>
<span class="line"><span style="color:#C3E88D">          title</span></span>
<span class="line"><span style="color:#C3E88D">          postDate</span></span>
<span class="line"><span style="color:#C3E88D">          dateUpdated</span></span>
<span class="line"><span style="color:#C3E88D">          uri</span></span>
<span class="line"><span style="color:#C3E88D">          author {</span></span>
<span class="line"><span style="color:#C3E88D">            name</span></span>
<span class="line"><span style="color:#C3E88D">            email</span></span>
<span class="line"><span style="color:#C3E88D">          }</span></span>
<span class="line"><span style="color:#C3E88D">        }</span></span>
<span class="line"><span style="color:#C3E88D">      }</span></span>
<span class="line"><span style="color:#C3E88D">    }</span></span>
<span class="line"><span style="color:#89DDFF">  `</span><span style="color:#F07178">)</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">then</span><span style="color:#F07178">(</span><span style="color:#C792EA">async</span><span style="color:#EEFFFF;font-style:italic"> result</span><span style="color:#C792EA"> =></span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    if</span><span style="color:#F07178"> (</span><span style="color:#EEFFFF">result</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">errors</span><span style="color:#F07178">) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">      console</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">log</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">result</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">errors</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">      return</span><span style="color:#82AAFF"> reject</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">result</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">errors</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA">    const</span><span style="color:#EEFFFF"> publicPath</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> `</span><span style="color:#C3E88D">./public</span><span style="color:#89DDFF">`</span></span>
<span class="line"><span style="color:#C792EA">    const</span><span style="color:#EEFFFF"> posts</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> result</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">data</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">craftGql</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">entries</span></span>
<span class="line"><span style="color:#C792EA">    const</span><span style="color:#EEFFFF"> feed</span><span style="color:#89DDFF"> =</span><span style="color:#82AAFF"> buildFeed</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">posts</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    await</span><span style="color:#EEFFFF"> fs</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">writeFile</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">publicPath</span><span style="color:#89DDFF"> +</span><span style="color:#89DDFF"> `</span><span style="color:#C3E88D">/blog/atom.xml</span><span style="color:#89DDFF">`</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> feed</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">atom1</span><span style="color:#F07178">())</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    await</span><span style="color:#EEFFFF"> fs</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">writeFile</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">publicPath</span><span style="color:#89DDFF"> +</span><span style="color:#89DDFF"> `</span><span style="color:#C3E88D">/blog/rss.xml</span><span style="color:#89DDFF">`</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> feed</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">rss2</span><span style="color:#F07178">())</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    await</span><span style="color:#EEFFFF"> fs</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">writeFile</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">publicPath</span><span style="color:#89DDFF"> +</span><span style="color:#89DDFF"> `</span><span style="color:#C3E88D">/blog/feed.json</span><span style="color:#89DDFF">`</span><span style="color:#89DDFF">,</span><span style="color:#EEFFFF"> feed</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">json1</span><span style="color:#F07178">())</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#FFCB6B"> Promise</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">all</span><span style="color:#F07178">([</span><span style="color:#EEFFFF">blogPosts</span><span style="color:#F07178">])</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre> <figcaption class="-my-4"> <p>Abbreviated version of using Gatsby’s <code>onPostBuild</code> hook to generate blog feeds after pages were built.</p>
 </figcaption> </figure><div id="templates-and-styles" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Templates &amp; Styles</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>Twig → JSX</h3><p>I had a lot of fun converting my component-esque Twig templates into React components.</p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/twig-vs-react.DUuVU009_19KIzA.webp" type="image/webp"><source src="https://workingconcept.com//_astro/twig-vs-react.DUuVU009_Z1skrK3.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/twig-vs-react.DUuVU009_Z1lFaGE.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/twig-vs-react.DUuVU009_Z3RhnM.png" decoding="async" loading="lazy" alt="screenshot of a Twig template on the left and a JSX template on the right" width="2400" height="1006"> </picture> <figcaption> <p> It&#39;s all just code.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Getting comfortable with JSX was awkward at first, especially compared to single-file Vue components.</p><p>My old friend <code>class</code> would always insist on being called <code>className</code>, and I’m sure that my first wobbly JSX components were terrible. (Now they’re probably <em>okay</em>.)</p><p>After a few days of classic type-and-squint confusion, however, it became oddly satisfying to organize components, use my familiar Tailwind classes, and get instant visual feedback in the browser with Gatsby’s hot module reloading (HMR). A subtle perk to HMR with React was having component states persist even as my changes were applied. This may sound trivial, but having a little menu stay expanded while styling it is delightful. No page refresh and clicking it back open. Things like this reduced friction and made it easier to stay focused.</p><p>I also found myself abandoning my comfortable four-space indents for just two and allowing <a href="https://prettier.io/">Prettier</a> to format my JSX templates on save. What started uncomfortably quickly turned into a much faster pace, no longer wasting time pushing characters around. What? You already work this way? Good for you, showoff.</p><p>There was a moment when I realized single-file Vue components seem more cleanly-structured and aesthetically pleasing, but that JSX is powerful because JavaScript can be freely woven into any part of it. In that way, working with JSX is sort of like the PHP we used to blend with HTML. </p><p>I’m still uncomfortable with this thought.</p><h3>Tailwind</h3><p>Andrew <a href="https://nystudio107.com/blog/using-tailwind-css-with-gatsby-react-emotion-styled-components">wrote about how he used Tailwind for CSS-in-JS with Emotion</a>, but I kept it simple (familiar) simply applying my utility classes and relying a global stylesheet. I’d like to embrace CSS-in-JS, but I’m not in any rush with this small site. If you don't know what that is and you want to hear me be confused about it, <a href="https://devmode.fm/episodes/css-in-js-an-emotional-topic">this podcast episode</a> has you covered.</p><h3>Links</h3><p>Gatsby handles routing in ways I probably don’t even appreciate yet, and the only time I had to care was remembering to use the provided <code>&lt;Link&gt;</code> component for links between pages on the site. Some internal anchors arrive within content, like HTML in a Redactor field, but luckily <code>gatsby-plugin-catch-links</code> exists for automatically catching and handling those.<br></p></div><aside class="mx-auto px-6 md:px-0 max-w-md text-teal font-sans text-base leading-normal relative"> <span class="w-4 h-4 inline-block absolute md:-ml-8" style="top:0.75rem"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="max-w-full h-auto" fill="currentColor"> <path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path> </svg> </span> <div class="ml-8 md:ml-0"><p>If following a link on your Gatsby site navigates with an unexpected page refresh, you probably used <code>&lt;a href=&quot;&quot;&gt;</code> where you should’ve used <code>&lt;Link to=&quot;&quot;&gt;</code>.</p>
</div> </aside><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>Obscure SVG Renderer</h3><p>I’ve published two blog posts that use charts.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/chart.CfD_mnoc_1qvOpw.webp" type="image/webp"><source src="https://workingconcept.com//_astro/chart.CfD_mnoc_Z1QmD0B.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/chart.CfD_mnoc_1RVNjk.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/chart.CfD_mnoc_e4JzU.png" decoding="async" loading="lazy" alt="screenshot of a chart comparing VPS Geekbench scores" width="1734" height="1202"> </picture> <figcaption> <p> A chart from another post, built the hard way.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>To avoid making anything easy, I wrote a Craft module to take Table Maker content and render it into an SVG bar chart so I could have complete control over how it looked and privately struggle with simple math again. </p><p>Because of that choice, I had to port that PHP into a React component. </p><p>To my surprise, it was actually fun! Using React to render SVG is actually fun! Some of the first things I built on the internet were in Flash, and my first experience with programming on the web was in ActionScript—it was fun to write code that directly translated to dynamic visuals in a browser, and this little exercise (again with HMR) helped me see what fun can be had in client-side, componentized SVG.</p><p>Building the frontend wasn’t all laughter and happy little surprises, though.</p><h3>Form Validation</h3></div><aside class="mx-auto px-6 md:px-0 max-w-md text-teal font-sans text-base leading-normal relative"> <span class="w-4 h-4 inline-block absolute md:-ml-8" style="top:0.75rem"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="max-w-full h-auto" fill="currentColor"> <path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path> </svg> </span> <div class="ml-8 md:ml-0"><p>If you are new to React and need little bits of instant gratification to build confidence, do not start with form validation.</p>
</div> </aside><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Write down the term “stateful component” and plan on coming back to it.</p><p>I learned the difference, first with form validation and again with a subnavigation component, between stateless and stateful components. If you’re a methodical learner you probably encountered this somewhere in your React introduction. </p><p>If you’re me, you learned by first doing it the wrong way that your state-dependent things are going to need</p><ul><li>a class-based React component </li><li>to be tied to properties literally held in an aptly-named <code>state</code> object of said component</li></ul><p>You can work with Vue and not fully realize this, but React lets you both completely disregard state and <em>requires</em> you to explicitly manage it when it’s important.<br></p><p>Writing a stateful component isn’t hard.</p><ol><li>Make sure your JSX component is a class that extends <code>React.Component</code>.</li><li>Set your initial <code>this.state</code> object in the constructor.</li><li>Change state properties with <code>this.setState()</code>.</li></ol><p>My form validation still feels like it could be refined, and it wasn’t all that fun to solve, but a combination of <code>simple-react-validator</code> and some attention to state management got my form validation working in a way I could live with.</p><h3>JSON-LD</h3><p>Lastly, my struggle with JSON-LD was awful. Using Helmet to manage metadata was pleasant. Building schema.org structures felt inherently closer to “the metal” in JavaScript land. But rendering those JSON structures in <code>&lt;head&gt;</code> with Helmet turned out to be deeply frustrating.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/fighting-helmet.WazIG3yq_Z2exq9H.webp" type="image/webp"><source src="https://workingconcept.com//_astro/fighting-helmet.WazIG3yq_Z2dflhD.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/fighting-helmet.WazIG3yq_10krML.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/fighting-helmet.WazIG3yq_1DXLjJ.png" decoding="async" loading="lazy" alt width="1578" height="1006"> </picture> <figcaption> <p> The author’s silent, failed struggle with duplicate structured data.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Not because it was hard to do or find examples. I’ve used several methods, and currently I’m prepping an object and giving it to Helmet’s <code>script</code> prop:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#546E7A;font-style:italic">// SEO.jsx</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">// schemaOrgGraph combines site-wide default and stuff added by pages</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> schema </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#F07178">  type</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">application/ld+json</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">  innerHTML</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> JSON</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">stringify</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#F07178">@context</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">http://schema.org</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#F07178">@graph</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> schemaOrgGraph</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">  }</span><span style="color:#EEFFFF">)</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">return</span><span style="color:#EEFFFF"> (</span></span>
<span class="line"><span style="color:#89DDFF">  &#x3C;</span><span style="color:#FFCB6B">Helmet</span><span style="color:#C792EA"> script</span><span style="color:#89DDFF">={</span><span style="color:#EEFFFF">[schema]</span><span style="color:#89DDFF">}>&#x3C;/</span><span style="color:#FFCB6B">Helmet</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#EEFFFF">)</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Perhaps more aesthetically pleasing and just as valid is adding the script tag as Helmet’s child:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#546E7A;font-style:italic">// SEO.jsx</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">// schemaOrgJSONLD: same but with `@context` and `@graph` at the top level</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#FFCB6B">Helmet</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">  &#x3C;</span><span style="color:#F07178">script</span><span style="color:#C792EA"> type</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">application/ld+json</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">    {</span><span style="color:#EEFFFF">JSON</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">stringify</span><span style="color:#EEFFFF">(schemaOrgJSONLD)</span><span style="color:#89DDFF">}</span></span>
<span class="line"><span style="color:#89DDFF">  &#x3C;/</span><span style="color:#F07178">script</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;/</span><span style="color:#FFCB6B">Helmet</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>No matter how I get that structured data in there, when using Helmet I randomly get duplicate JSON-LD structures on some pages. </p><p>It took more digging into Gatsby and Helmet for me to learn that it’s probably because of how Gatsby determines when to split code. I think the JSON-LD looks like a normal script reference to Gatsby, as though I’m looking to load a <code>&lt;script src="foo.js"&gt;</code> tag in the <code>&lt;head&gt;</code> element. When Gatsby sees a script used frequently enough, it’ll have webpack split it into a chunk that gets loaded with the page. In this case the JSON-LD gets rendered into <code>&lt;head&gt;</code> <em>and</em> loaded in a chunk, so anything reading JSON-LD that executes JavaScript—like <a href="https://search.google.com/structured-data/testing-tool/u/0/">Google’s Structured Data Testing Tool</a>—will see duplicate versions of each schema.org entity.</p><p>Google doesn’t consider this a problem so I’m trying not to either for now, but it drives me bonkers that I can’t figure out how to get around it without abandoning Helmet.</p></div><div id="image-handling" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Image Handling</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Many of you already store Craft Assets on S3 or Digital Ocean Block Storage or some cloud storage. All I did was join you on that front, moving my local Asset Volumes to S3 to get them off Craft’s server.</p><p>I’ve kept to local Asset Volumes and increasingly more powerful servers to handle image transforms, but I decided that in decoupling I’d like to leave Craft (and its webserver) with as <em>little</em> to do as possible. That means offloading image transforms someplace else.</p><h3><del>Transforms at Build Time</del></h3><p>Ideally this would happen at build time. Craft could supply source URLs, while Gatsby and Sharp could size, crop, create alternates and optimize as dictated by the frontend. Static files would be published, everything would be super fast, and Craft wouldn’t need to concern itself with the intensive task of manipulating images.</p><p>But Gatsby’s convenient Sharp abstractions are meant to work with images in the local filesystem, and the only way to have Gatsby transform remote images is to use <code>gatsby-plugin-remote-images</code> <em>and describe every point in your schema where image URLs exist</em>. It seems like that’d just duplicate the maintenance of any structural content changes, so I chose not to bother.</p><p>I decided I’d keep the frontend simple and give it the simple role of building the URLs needed for image transforms. Something else could do the heavy lifting.</p><h3>imgproxy Transforms</h3><p>Instead, I first used Docker to spin up <a href="https://github.com/imgproxy/imgproxy">imgproxy</a> to use like my own private <a href="https://www.imgix.com/">Imgix</a>. imgproxy is an open source image resizing server written in Go that can work with images from any source. It was surprisingly fast and it didn’t take me long to sign its URLs to prevent being <em>everybody’s</em> Imgix. It can also handle resizing images with a precise focal point, which I’ll come back to.</p><p>I was happy with this solution until my favorite troll pointed out this journey is supposed to result in <em>fewer</em> servers to maintain, not more.</p><h3>Serverless Transforms</h3><p>So I pivoted to <a href="https://github.com/awslabs/serverless-image-handler">an AWS lambda function</a> for serverless image transforms. </p><p>Andrew <a href="https://nystudio107.com/blog/setting-up-your-own-image-transform-service">wrote a post about it</a> I figured I could refer to if I got stuck. (Do not tell him.) And though it’s out of character for me, it was the path of least resistance: AWS writes and maintains the code and it’s supposedly production ready. Nothing to write, trivial to set up, and probably not much work to maintain. It’s also cheap and scalable.</p><p>The only work I needed to do was write code for building the transform URLs.<br></p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#546E7A;font-style:italic">/**</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * Gets the extension from a URL, blindly trusting it has one.</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> */</span></span>
<span class="line"><span style="color:#C792EA">function</span><span style="color:#82AAFF"> getExtension</span><span style="color:#89DDFF">(</span><span style="color:#EEFFFF;font-style:italic">url</span><span style="color:#89DDFF">)</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> urlParts</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> url</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">split</span><span style="color:#F07178">(</span><span style="color:#89DDFF">`</span><span style="color:#C3E88D">.</span><span style="color:#89DDFF">`</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#EEFFFF"> urlParts</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">pop</span><span style="color:#F07178">()</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">/**</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * Generates an image transform URL for AWS Serverless Image Handler.</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> */</span></span>
<span class="line"><span style="color:#C792EA">function</span><span style="color:#82AAFF"> getAwsLambdaTransformUrl</span><span style="color:#89DDFF">(</span><span style="color:#EEFFFF;font-style:italic">props</span><span style="color:#89DDFF">)</span><span style="color:#89DDFF"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // don't attempt to transform unless we have an Asset in our bucket</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  if</span><span style="color:#F07178"> (</span><span style="color:#89DDFF">!</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">url</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">includes</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">assetBucketName</span><span style="color:#F07178">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">    return</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">url</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // determine target extension if one wasn't provided</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  if</span><span style="color:#F07178"> (</span><span style="color:#89DDFF">!</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">extension</span><span style="color:#F07178">) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">extension</span><span style="color:#89DDFF"> =</span><span style="color:#82AAFF"> getExtension</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">url</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  if</span><span style="color:#F07178"> (</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">extension</span><span style="color:#89DDFF"> ===</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">jpg</span><span style="color:#89DDFF">'</span><span style="color:#F07178">) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">    // set valid type for sharp (which doesn't want `jpg`)</span></span>
<span class="line"><span style="color:#EEFFFF">    props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">extension</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">jpeg</span><span style="color:#89DDFF">'</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> edits</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#89DDFF">    ...</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">filters</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">    resize</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#F07178">      width</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">width</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      height</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">height</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      fit</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">type</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">      quality</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">quality</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">    },</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">  edits</span><span style="color:#F07178">[</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">extension</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#F07178">    quality</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">quality</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  if</span><span style="color:#F07178"> (</span><span style="color:#EEFFFF">props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">extension</span><span style="color:#89DDFF"> ===</span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">webp</span><span style="color:#89DDFF">'</span><span style="color:#F07178">) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    edits</span><span style="color:#F07178">[</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">webp</span><span style="color:#89DDFF">'</span><span style="color:#F07178">]</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">lossless</span><span style="color:#89DDFF"> =</span><span style="color:#FF9CAC"> true</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // get the folder and filename relative to the bucket</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> key</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> props</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">url</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">replace</span><span style="color:#F07178">(</span><span style="color:#89DDFF">`</span><span style="color:#C3E88D">https://</span><span style="color:#89DDFF">${</span><span style="color:#EEFFFF">assetBucketName</span><span style="color:#89DDFF">}</span><span style="color:#C3E88D">/</span><span style="color:#89DDFF">`</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> ``</span><span style="color:#F07178">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // prep the object we'll send</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> request</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#F07178">    bucket</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> assetBucketName</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">    key</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> key</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#F07178">    edits</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> edits</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">  // base64-encode encode parameters</span></span>
<span class="line"><span style="color:#C792EA">  const</span><span style="color:#EEFFFF"> enc</span><span style="color:#89DDFF"> =</span><span style="color:#EEFFFF"> Buffer</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">from</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">JSON</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">stringify</span><span style="color:#F07178">(</span><span style="color:#EEFFFF">request</span><span style="color:#F07178">))</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">toString</span><span style="color:#F07178">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">base64</span><span style="color:#89DDFF">'</span><span style="color:#F07178">)</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">  return</span><span style="color:#89DDFF"> `${</span><span style="color:#EEFFFF">serverlessTransformEndpoint</span><span style="color:#89DDFF">}</span><span style="color:#C3E88D">/</span><span style="color:#89DDFF">${</span><span style="color:#EEFFFF">enc</span><span style="color:#89DDFF">}`</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre> <figcaption class="-my-4"> <p>URL-building for image transforms.</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This setup is what I’m using now, though the AWS function comes with a few drawbacks:</p><ul><li><a href="https://github.com/lovell/sharp">sharp</a>, on which the serverless image handler relies, is blazing fast but doesn’t support precise focal point handling. You can define “gravity” in general regions, but not the exact point as defined in the Craft control panel. Lovers of precision need to know this.</li><li>The AWS image handler is not set up to use URLs with file extensions (like <code>.jpg</code>), return a MIME type specific to the returned image, or supply ideal cache headers.</li><li>Transforms still happen on the fly and there are now separate cache invalidating concerns—both reasons I’d still prefer the simplicity of generating image variants at build time.</li></ul><p>I solved the second problem by making a quick change to the Lambda function. It wasn’t any more complicated than editing a .js file and re-uploading it to AWS.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#546E7A;font-style:italic">// getResponseHeaders() already existed</span></span>
<span class="line"><span style="color:#C792EA">const</span><span style="color:#EEFFFF"> headers </span><span style="color:#89DDFF">=</span><span style="color:#82AAFF"> getResponseHeaders</span><span style="color:#EEFFFF">()</span><span style="color:#89DDFF">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#546E7A;font-style:italic">// I added this elegant part</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic">// request.edits has settings for sharp to do the transform</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">if</span><span style="color:#EEFFFF"> (request</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">edits</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">hasOwnProperty</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">png</span><span style="color:#89DDFF">"</span><span style="color:#EEFFFF">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    headers</span><span style="color:#F07178">[</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">Content-Type</span><span style="color:#89DDFF">"</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">image/webp</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span><span style="color:#89DDFF;font-style:italic"> else</span><span style="color:#89DDFF;font-style:italic"> if</span><span style="color:#EEFFFF"> (request</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">edits</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">hasOwnProperty</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">jpeg</span><span style="color:#89DDFF">"</span><span style="color:#EEFFFF">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    headers</span><span style="color:#F07178">[</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">Content-Type</span><span style="color:#89DDFF">"</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">image/jpeg</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span><span style="color:#89DDFF;font-style:italic"> else</span><span style="color:#89DDFF;font-style:italic"> if</span><span style="color:#EEFFFF"> (request</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">edits</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">hasOwnProperty</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">png</span><span style="color:#89DDFF">"</span><span style="color:#EEFFFF">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    headers</span><span style="color:#F07178">[</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">Content-Type</span><span style="color:#89DDFF">"</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">image/png</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span><span style="color:#89DDFF;font-style:italic"> else</span><span style="color:#89DDFF;font-style:italic"> if</span><span style="color:#EEFFFF"> (request</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">edits</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">hasOwnProperty</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">webp</span><span style="color:#89DDFF">"</span><span style="color:#EEFFFF">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    headers</span><span style="color:#F07178">[</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">Content-Type</span><span style="color:#89DDFF">"</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">image/webp</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span><span style="color:#89DDFF;font-style:italic"> else</span><span style="color:#89DDFF;font-style:italic"> if</span><span style="color:#EEFFFF"> (request</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">edits</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">hasOwnProperty</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">gif</span><span style="color:#89DDFF">"</span><span style="color:#EEFFFF">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    headers</span><span style="color:#F07178">[</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">Content-Type</span><span style="color:#89DDFF">"</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">image/gif</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span><span style="color:#89DDFF;font-style:italic"> else</span><span style="color:#89DDFF;font-style:italic"> if</span><span style="color:#EEFFFF"> (request</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">edits</span><span style="color:#89DDFF">.</span><span style="color:#82AAFF">hasOwnProperty</span><span style="color:#EEFFFF">(</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">tiff</span><span style="color:#89DDFF">"</span><span style="color:#EEFFFF">)) </span><span style="color:#89DDFF">{</span></span>
<span class="line"><span style="color:#EEFFFF">    headers</span><span style="color:#F07178">[</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">Content-Type</span><span style="color:#89DDFF">"</span><span style="color:#F07178">] </span><span style="color:#89DDFF">=</span><span style="color:#89DDFF"> "</span><span style="color:#C3E88D">image/tiff</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">}</span></span></code></pre> <figcaption class="-my-4"> <p>The dazzling code I used to have the image handler return an appropriate <code>Content-Type</code> header.</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>Lazyloading</h3><p>Dropping <a href="https://github.com/aFarkas/lazysizes">lazysizes</a> into any project is easy until it’s a React project. With Gatsby it’s easy to lazyload anything at all, but the more complex process of loading things means that lazyloading images is a bit less straightforward.</p><p>I ended up spending a few days on a utility for generating image transform URLs along with a <code>&lt;Picture&gt;</code> class that takes advantage of srcset and lazyloading with either a dominant image color (thanks <a href="https://plugins.craftcms.com/imager">Imager</a>!) or blur effect. It could be better, but it works alright and behaves both in browser and node/SSR context.</p></div><div id="live-preview" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Live Preview</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>As a developer it can be easy to forget how important Live Preview is for content authors. </p><p>We spin up a local environments, poke at things in the web inspector and use tools like <a href="https://www.browserstack.com/">BrowserStack</a> and <a href="https://sizzy.co/">Sizzy</a> to examine a site from many angles. A content author, the one for whom we’re building the site, probably does none of these things. Seeing their work and safely experimenting with it is critically important, and Live Preview may be the only tool available for that.</p><p>Live Preview is challenging with a decoupled frontend where Craft and that frontend know nothing about each other. I paused the headless quest more than once because Live Preview was impossible or excessively difficult or fragile. Someone else’s client found me looking for help with a headless Craft project built to their detriment: deplorable SEO, no Live Preview and a maintenance nightmare. To me that’s backward and unethical.</p><p>So I consider Live Preview to be an essential part of the puzzle. We can’t pass the problem on to the content editor, we have to solve it.</p><p>Luckily Craft 3.2 added Preview Targets: add whatever options you’d like to the Live Preview pane and they’ll appear in a dropdown menu. Craft loads your specified URL into that <code>iframe</code>, offers a token that can be redeemed for the content that’s needed, and waits hopefully. That URL you provided needs to get the token, present it in exchange for content, and then display that content in a way that’s useful (and expected?) to the content editor.</p><p>That feature, combined with <a href="https://gist.github.com/monachilada/af7e92a86e0d27ba47a8597ac4e4b105">the miraculous Mike Pierce gatsby-config.js</a>, means Gatsby can preview content if it’s running in development mode.</p><p>The challenge becomes having Gatsby always running in development mode without falling over, while also keeping up to date with changes to its source code. In other words, I need to run <code>gatsby develop</code> on a web server, keep it going and restart it when the project repository changes. But I can’t trust that process to keep running (because software) and I certainly don’t want to SSH in to start and stop it all the time.</p><h3><del>Supervisor</del></h3><p>I first tried to do this using <code>supervisor</code>, a Linux system process that exists for the sole purpose of keeping processes running. It’s easy to work with in what Laravel Forge labels <em>Daemons</em>. Despite the rule to run only one process, it <em>sort of</em> worked but would eventually spawn multiple children that ate each other and ruined the show.</p><p>I don’t know why, because I discovered <code>pm2</code> and haven’t looked back.</p><h3>pm2</h3><p><a href="https://pm2.keymetrics.io/">pm2</a> is apparently what you would use to keep your node app running on a web server. Unlike <code>supervisor</code>, it’s specific to node apps. It’s also wildly configurable, and it’s been unfailingly stable since I started using it. The preview domain will go down for a minute or two when <code>gatsby develop</code> gets restarted, but there’s no avoiding that since the initial build needs time.<br></p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/pm2.Duy4SAoF_ZnqtJh.webp" type="image/webp"><source src="https://workingconcept.com//_astro/pm2.Duy4SAoF_Z13Pndt.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/pm2.Duy4SAoF_ZmQP9A.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/pm2.Duy4SAoF_wNmFR.png" decoding="async" loading="lazy" alt="Screenshot of `pm2 list` command, displaying status of gatsby process." width="1448" height="255"> </picture> <figcaption> <p> pm2 makes it easy to start, stop, monitor, and manage node processes.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>So I have a dedicated preview domain, behind which <code>pm2</code> keeps <code>gatsby develop</code> alive, and my entire Forge deploy script is...</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">git</span><span style="color:#C3E88D"> pull</span><span style="color:#C3E88D"> origin</span><span style="color:#C3E88D"> master</span></span>
<span class="line"><span style="color:#FFCB6B">npm</span><span style="color:#C3E88D"> install</span></span>
<span class="line"><span style="color:#FFCB6B">pm2</span><span style="color:#C3E88D"> restart</span><span style="color:#C3E88D"> gatsby</span></span></code></pre> <figcaption class="-my-4"> <p>Forge deploy script for the Gatsby preview.</p>
 </figcaption> </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I use nginx to reverse proxy preview.workingconcept.com to Gatsby’s dev server port, and it works pretty well.</p><p>The input lag and refresh aren’t ideal, but I expect this will improve over time with Gatsby’s evolution and hopefully a Gatsby Craft source plugin. &#x1f91e;<br></p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <img src="https://workingconcept.com//_astro/live-preview.DWHboHTW.gif" width="1426" height="1172" decoding="async" loading="lazy">  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The only major downside in this department is that I haven’t found a way to preview the first draft of an Entry. This is unfortunately the most important time to preview an Entry—before it’s been published—and Gatsby doesn’t know about the route or have a way to query the draft.</p></div><div id="hosting" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Hosting</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I have a strange love for comparing web hosts, so the simple task of hosting a static frontend gave me plenty of interesting things to overthink.</p><p>I think I made two people uncomfortable sharing a FreshPing page I made to see how the exact same site behaved on <a href="https://www.netlify.com/">Netlify</a>, <a href="https://zeit.co">Zeit</a>, <a href="https://surge.sh/">Surge</a> and <a href="https://blog.cloudflare.com/workers-sites/">Cloudflare Workers Sites</a>. (Actually I’m sure it was the fact that I deployed the same project to Netlify, Zeit, Surge and Cloudflare Workers Sites.)</p><p>Cloudflare Workers Sites was the only one that wasn’t free, Surge was the only one that saw many outages during that recent S3 DNS issue, and both of those require first building the site locally before pushing it to their CDN.</p><p>So it was down to Netlify and Zeit.</p><p>Each offers a generous free tier, is fun to work with, and is improving all the time. Zeit has the better average response time with less variation, even though that’s un-scientific, and I’ve always had a really smooth experience doing anything I’ve tried with Netlify.</p><p>I ended up going with Netlify when I found that Zeit’s redirects are limited to the same project, and I have a single and very unimportant redirect to another unimportant domain. (Sorry Zeit. If it helps, I love <a href="https://hyper.is/">Hyper</a> and nobody cares which one I picked.)</p><p>Netlify’s redirects can also do a lot, including reverse-proxy paths. Once I settled on Netlify, <code>gatsby-plugin-netlify</code> and <code>gatsby-plugin-remove-trailing-slashes</code> were helpful for maintaining URLs without much effort.</p><p>I still need my LEMP stack to run Craft and the Gatsby server for Live Preview. My four-core VPS handles both at the same time with resources to spare, so I’ve now got the following domain layout:</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/domain-layout.Ce7fXTlh_Z1jWwRl.webp" type="image/webp"><source src="https://workingconcept.com//_astro/domain-layout.Ce7fXTlh_188pAW.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/domain-layout.Ce7fXTlh_1eMGEl.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/domain-layout.Ce7fXTlh_2wAzXd.png" decoding="async" loading="lazy" alt="flow chart: bare domain to Netlify, cms+preview subdomains to VPS, assets subdomain to S3, img+forms domains to AWS Lambda" width="1432" height="1060"> </picture> <figcaption> <p> Actual size.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h3>Build Process</h3><p>Once the frontend is just a static site, it makes it easy to take a YOLO approach to Craft updates, PHP version changes, and other things that could hose your Craft install. Nobody will know because it will have zero impact on the frontend. That part’s liberating.</p><p>Netlify can watch the Gatsby project repository and build the site in response to a git push on whatever branch(es) you’d like.</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>After editing a few Craft Entries and visiting Netlify’s control panel to redeploy, I used the <a href="https://plugins.craftcms.com/webhooks">Webhooks</a> plugin to post to a Netlify Build Hook instead. This was good because every content edit would kick off a build, but also bad because every content edit would kick off a build.<br></p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/netlify-build.FwnRQjpj_Z2eOBN.webp" type="image/webp"><source src="https://workingconcept.com//_astro/netlify-build.FwnRQjpj_mr8Er.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/netlify-build.FwnRQjpj_Z1qVqvh.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/netlify-build.FwnRQjpj_ZIUDWG.png" decoding="async" loading="lazy" alt="screenshot of the Netlify control panel building the master branch in production" width="1542" height="508"> </picture> <figcaption> <p> Netlify building this site for the eighty jillionth time.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Rather than deploy on a schedule that’d be building unchanged content most of the time, I wrote <a href="https://github.com/workingconcept/trigger-craft-plugin">a small Craft plugin</a> to debounce these edits. Now every three minutes cron runs the CLI command to check whether a new build should start. If so, it sends a request to the Netlify Build Hook and switches off the plugin’s build flag. Editing Entries (but not Drafts!) will flip that flag back on again, and any edits in a three-minute period will be picked up in the next build.<br></p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/trigger-settings.qIS6YYfa_Z1roknT.webp" type="image/webp"><source src="https://workingconcept.com//_astro/trigger-settings.qIS6YYfa_Z1rlLLL.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/trigger-settings.qIS6YYfa_1V6RVq.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/trigger-settings.qIS6YYfa_1imjXS.png" decoding="async" loading="lazy" alt="Trigger Craft CMS plugin settings: Deploy Webhook field, Active switch, and Deploy Waiting switch" width="1432" height="906"> </picture> <figcaption> <p> Settings for the deploy-debouncing Trigger plugin.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I may still adjust the check interval to balance instant gratification with fewer builds, but so far it’s been good for triggering just enough but not too many builds. Build time is a precious resource on the free tier—there’s lots of it but I’ve found I can also use it quickly with enough projects and previews.</p></div><div id="unfinished-business" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Unfinished Business</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’m proud that I’m only living with a few compromises right now.<br></p><p>First is that inability to preview unpublished first drafts. Not a deal-breaker for me, but I wouldn’t ask a client to live with that.</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Next: with Twig I provided a <code>potentialAction</code> in my structured data that could allow searching blog articles directly from a search engine. I haven’t found a simple way to make that happen with Gatsby, so for now that API isn’t available. There is no way you noticed.</p><p>Lastly, Storybook.</p><p><a href="https://github.com/ben-rogerson/craft-storybook-starter">Ben Rogerson’s starter project</a> tricked me into using Storybook to organize and document my Twig components. Not only is it a nice way to catalog a site’s pieces, it’s fun to use and even a useful way to develop components in an isolated context.</p></div><div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/storybook.hNkUY4C4_1IXMea.webp" type="image/webp"><source src="https://workingconcept.com//_astro/storybook.hNkUY4C4_2kN1tf.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/storybook.hNkUY4C4_Z2wUd6N.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/storybook.hNkUY4C4_Z4LB6p.png" decoding="async" loading="lazy" alt="screenshot of Storybook interface with a sidebar menu of components and a rendered pull quote with testing tools" width="2016" height="1446"> </picture> <figcaption> <p> A Matrix pull quote block as seen in Storybook, wherein most components are not yet represented.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I’ve been getting in the habit of using Storybook with client work since it gives everyone a shared point of reference, outside production but built directly from production code. It’s been helpful to be able to link content editors directly to Matrix blocks, for example, whether they’re being developed or simply options in Craft they’d like to <em>see</em> without having to add each with Live Preview. (They get a rich, responsive preview and I don’t have to build documentation with screenshots and descriptions when things get more complex.)<br></p><p>A conflict between Storybook (really <code>fs</code>) and Tailwind has me unable to import Tailwind’s config for visualizing project color, typography, breakpoints, etc. So I can’t offer a working example of that right now.</p><p>It builds automatically and exactly like Gatsby onto Netlify, just with <code>npm run build-storybook</code> instead of <code>gatsby build</code>.</p></div><div id="what-ive-learned" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>What I’ve Learned</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>I learned a lot from this experiment and won’t be rushing to build every new project with a headless frontend.</p><ul></ul><p>Working this way doesn’t eliminate pain points, it just shuffles them around. You’ll have to find new solutions to problems Twig already solves, no matter what kind of projects you work on. I got a personal boost of developer confidence that’s usually in short supply, ultimately navigating some challenges to produce what we can all agree is a website. </p><p>The places I struggled the most were...</p><ul><li>Learning to use promises effectively to generate pages in <code>gatsby-node.js</code>.</li><li>Getting more comfortable with modern JavaScript and JSX. (Loved dropping semicolons, auto-formatting, object spread operator, and arrow functions—missed the null coalescing operator.)</li><li>Developing more awareness for SSR (Server Side Rendering), where Gatsby <del>magically</del> carefully takes your dynamic frontend and splits it into SEO-friendly HTML that’s available behind the scenes of the dazzling JavaScript frontend. I still don’t fully grasp everything it does, but I know it forces you to be careful with component state, aware of whether you’re writing JavaScript for the server (node) or the browser, and immediately appreciative of how much easier some things come with Twig.</li></ul><p>But building with Gatsby was <em>fun</em>. </p><p>Preconfigured webpack, HMR and easy deployment helped me focus more on building things instead of configuring tools. It’s hard to overstate how nice this part was. Instant visual feedback made me feel more connected to my work and iterate faster. Building with components felt inherently more organized and reusable. The improved focus had little frontend ideas bubbling through my head, some of which I implemented. The experience was exciting enough to renew my interest in fixing HMR problems in my Twig workflow. </p><p>The frontend freedom is, without question, my favorite part about working with Gatsby or Gridsome and the main reason I’ll keep trying to iron out the wrinkles of the decoupled frontend.<br></p><p>I also won’t try to build just any project this way. </p><p>Smaller projects, absolutely! Ones with low page counts and higher frontend complexity, probably. </p><p>But I approach most projects with Craft in mind because it’s a blank slate that can handle almost anything out of the box. If not out of the box, then with third-party plugins. If not out of the box or with third-party plugins, with a custom module or plugin I could write. This all assumes Craft is running the way I’m used to thinking of it: as an application on a web server responding directly to requests. </p><p><strong>Planning for headlessness means I need to develop mindfulness for the implications of reducing Craft’s responsibility to an API provider.</strong> With Craft in this role, a plugin or custom module cannot reach every corner of a public request. That puts more responsibility on the static frontend or microservices and in a place I have less experience. And even with more experience, those things still may or may not be the best tools for the job at hand.</p><p>Building projects with headless frontends is not a silver bullet, it’s a different way to split up the responsibilities involved in delivering a website to the visitor. I’ve added some nice tools and experience to my bucket of tricks and expanded my conceptual appreciation for how a project can be built, but I’m not sure yet how exactly how that’ll influence projects to come. </p></div><div id="should-you-go-for-it" class="anchored-heading"> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"> <h2>Should You Go For It?</h2> </div> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>If you’re curious and motivated enough to plow through a series of challenges, there’s plenty to learn having your own run at this. You might even have fun. If everything here seems new and foreign, and you’re pressed for time or resources, it might be daunting to wing it with a new project.</p><p>If you’re like me, too many new concepts all at once will cause brain meltdown and require time to re-organize and recover. My best advice would be to create a shopping list of things you’d need to understand, then approach them one at a time like I happened to over the past year.<br></p><p>Set up a GraphQL endpoint and play with it in <a href="https://github.com/prisma-labs/graphql-playground">GraphQL Playground</a> or <a href="https://insomnia.rest/">Insomnia</a>.</p><p>Start building some Gatsby or Gridsome templates and publish anything at all to Netlify or Zeit as early as you can. </p><p>Do not start with form validation. </p><p>Consider Vue if you’re not super comfortable with JavaScript. Component examples will be more consistent and easier to follow. </p><p>Figure out who’s neck-deep in this stuff and pay attention to what they’re saying. <a href="https://twitter.com/pauloelias">Paulo Elias</a> has made a number of sobering comments about how things work at a larger scale, <a href="https://twitter.com/JakeDohm">Jake Dohm</a> is often helping everyone and he’s insufferably cheery about it. <a href="https://github.com/craftcms/cms/issues?q=is%3Aopen+is%3Aissue+label%3A%22graphql+%3Amag%3A%22">Craft’s own GitHub issues</a> reveal who’s busy with GraphQL and what its limitations may be. </p><p>If you’re still on the fence, get in touch with me if you think chatting might help! You don’t want me teaching you React or explaining SSR, but I’m happy to share what my journey’s been like especially if it might be useful for you on yours.</p></div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><hr><ol><li id="fn:1"><p>There are a few languages available and you can even use PHP with <a href="https://bref.sh/">Bref</a>, I just embraced JavaScript for the learning exercise. <a href="#fnref1" rev="footnote" class="footnote-backref">↩</a></p></li></ol></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Craft Plugins: Snipcart and Stack Exchange]]></title>
            <link>https://workingconcept.com/blog/craft-plugins-snipcart-stack-exchange</link>
            <guid>https://workingconcept.com/blog/craft-plugins-snipcart-stack-exchange</guid>
            <pubDate>Tue, 24 Jun 2014 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>The <a href="http://craftcms.stackexchange.com/">Craft Stack Exchange site</a> (now in public beta!) has me more focused on Craft than ever. Yesterday I finally cleaned up two plugins and released them into the wild.</p><h2>Snipcart</h2><p>This one makes it possible, thanks to <a href="http://docs.snipcart.com/api-reference/introduction">Snipcart’s API</a>, to browse order information from the Craft control panel. I’ve got a working (but half-baked) web hook in there which currently sends an order confirmation email to a hardcoded address.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/snipcart-orders.Chuk3W9v_Z1yJ2gS.webp" type="image/webp"><source src="https://workingconcept.com//_astro/snipcart-orders.Chuk3W9v_Z1a33YD.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/snipcart-orders.Chuk3W9v_26KuDz.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/snipcart-orders.Chuk3W9v_Z2gpQBL.png" decoding="async" loading="lazy" alt="Screenshot of Snipcart Craft CMS plugin displaying Snipcart orders between two dates" width="957" height="638"> </picture>  </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><a href="https://github.com/mattstein/snipcart-craft-plugin">https://github.com/mattstein/snipcart-craft-plugin</a></p><h2>Stack Exchange</h2><p>A plugin that does one thing: it makes Craft CMS Stack Exchange info available for your templates, via the Stack Exchange API.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="twig"><code><span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">set</span><span style="color:#EEFFFF"> se</span><span style="color:#89DDFF"> = </span><span style="color:#EEFFFF">craft</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">stackExchange</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">getProfile</span><span style="color:#89DDFF">(</span><span style="color:#F78C6C">22</span><span style="color:#89DDFF">) %}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">reputation_change_day</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 7 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">badge_counts</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">bronze</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 18 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">badge_counts</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">silver</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 2 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">badge_counts</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">gold</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 0 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">badge_counts</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">location</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# "Seattle" #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">profile_image</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# "http://i.stack.imgur.com/zwqV6.jpg?s=128&#x26;amp;g=1" #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">last_access_date</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 1403550544 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">accept_rate</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 100 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">link</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# "http://craftcms.stackexchange.com/users/22/matt-stein" #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">user_id</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 22 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">reputation_change_week</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 12 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">is_employee</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# false #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">website_url</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# "http://workingconcept.com/" #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">creation_date</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 1402531180 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">reputation_change_year</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 722 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">reputation</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 723 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">last_modified_date</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 1403051477 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">reputation_change_quarter</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 722 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">user_type</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# "registered" #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">account_id</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 482701 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">age</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 30 #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">display_name</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# "Matt Stein" #}</span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#EEFFFF">se</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">reputation_change_month</span><span style="color:#89DDFF"> }}</span><span style="color:#546E7A;font-style:italic"> {# 722 #}</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><a href="https://github.com/mattstein/stackexchange-craft-plugin">https://github.com/mattstein/stackexchange-craft-plugin</a></p><p>Each is a work in progress, but could probably be useful to someone as-is. I’m open to any thoughts, suggestions, pull requests, complaints, or exclamations anyone might have.</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Closing the PageSpeed Gap]]></title>
            <link>https://workingconcept.com/blog/closing-pagespeed-gap</link>
            <guid>https://workingconcept.com/blog/closing-pagespeed-gap</guid>
            <pubDate>Thu, 28 Jul 2016 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>My <a href="https://workingconcept.com//blog/going-static-quickly">recent move to a Static site generator</a> and full-site caching wasn’t enough to get a flawless score from <a href="https://developers.google.com/speed/pagespeed/insights/">Google PageSpeed Insights</a>, so I kept on and finally achieved success. You probably don’t care, but I’m going to tell you about it anyway.</p>
<p>There haven’t been any frills around here; no jQuery except for lazy form validation on one-off pages, sparing use of imagery, lean markup and stylesheets. It’s mostly text. Even still, Google docked the site points because it used too much render-blocking JavaScript and CSS. "<a href="https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent">Prioritize Visible Content</a>!," it pleaded. </p>
<p>The render-blocking JavaScript turned out to be Typekit’s allegedly-but-not-actually asychronous standard embed snippet, which I replaced with the "advanced" version and added <a href="https://github.com/morris/typekit-cache">a localStorage JavaScript utility</a> to help minimize FOUT. The trickier part, and relatively new to me, was including a subset of CSS for above-the-fold page content and then asynchronously loading the rest. Despite my unease with the phrase "above the fold" in the context of web development, Google is (annoyingly) right: there’s no good reason to have a visitor waiting on styles that are mostly for things she’ll not see until further down a page or even further into the site. </p>
<p>Embedding a streamlined bit of CSS isn’t complicated, but knowing what to lift out and doing it in perpetuity creates a workflow problem. Most projects that I work on use a single combined stylesheet, and this change now requires maintaining two of them and one must always account for above-the-fold styles. Thankfully <a href="https://nystudio107.com/">Andrew Welch</a> pointed me to <a href="https://github.com/filamentgroup/loadCSS">loadCSS</a> and <a href="https://github.com/filamentgroup/criticalCSS">criticalCSS</a><sup id="fnref1:1"><a href="{fn:1:url||}" class="footnote-ref">1</a></sup>, and <a href="https://mildlygeeky.com/">Patrick Harrington</a> offered helpful advice about handling FOUT/FOIT and incorporating criticalCSS into the build process.</p>
<p>Sculpin’s Twig search paths are still a bit confusing to me, but now my gulp build process uses the <code>critical</code> package to output a few different CSS files (for different layouts) in source/_layouts/criticalcss, and I can use a little bit of Twig to inline them:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="twig"><code><span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">if</span><span style="color:#EEFFFF"> inlineCssFile</span><span style="color:#89DDFF"> is not </span><span style="color:#EEFFFF">defined</span><span style="color:#89DDFF"> %}</span></span>
<span class="line"><span style="color:#89DDFF">     {% </span><span style="color:#89DDFF;font-style:italic">set</span><span style="color:#EEFFFF"> inlineCssFile</span><span style="color:#89DDFF"> = </span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">criticalcss/default.css</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> %}</span></span>
<span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">else</span><span style="color:#89DDFF"> %}</span></span>
<span class="line"><span style="color:#89DDFF">     {% </span><span style="color:#89DDFF;font-style:italic">set</span><span style="color:#EEFFFF"> inlineCssFile</span><span style="color:#89DDFF"> = </span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">criticalcss/</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> ~ </span><span style="color:#EEFFFF">inlineCssFile</span><span style="color:#89DDFF"> %}</span></span>
<span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">endif</span><span style="color:#89DDFF"> %}</span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#FFCB6B">style</span><span style="color:#89DDFF">></span></span>
<span class="line"><span style="color:#89DDFF">{{ </span><span style="color:#82AAFF">source</span><span style="color:#89DDFF">(</span><span style="color:#EEFFFF">inlineCssFile</span><span style="color:#89DDFF">) }}</span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;/</span><span style="color:#FFCB6B">style</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>When I want to reference a different critical CSS set, I can just choose a different file from the sub template, like this for a blog post detail:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="twig"><code><span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">set</span><span style="color:#EEFFFF"> inlineCssFile</span><span style="color:#89DDFF"> = </span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">blog-post.css</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> %}</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Since nothing beats simply removing stuff that isn’t essential, I took a few bold steps:</p>
<ol>
<li>
<p><strong>Removed Google Analytics entirely.</strong><br>
I’m not promoting my brand or measuring conversions or even using insights to offer more to my audience. I’m just rambling here about things I’ve learned. CloudFlare still gives me broad pageview stats so I’m not completely in the dark, but now I have one less thing to check obsessively. </p>
</li>
<li>
<p><strong>Replaced my Typekit fonts with self-hosted ones.</strong><br>
It took a long time, and lots of experimenting, to end up with <a href="https://www.dardenstudio.com/typefaces/freight_text">Freight Text Pro</a>, but I think it’s a clear improvement in terms of legibility, file weight, and caching performance. (No more Typekit pings or slowdowns!) </p>
</li>
<li><strong>Improved font loading.</strong><br>
As Patrick pointed out, it’s infuriating to have to wait on fonts and styles to load if you’re on crappy wifi and just want to see text. For that reason, I’m now using <a href="https://github.com/typekit/webfontloader">typekit/webfontloader</a> and dropping my fancy styles if the visitor’s waiting around for more than two seconds. It’s as easy as dropping <code>timeout: 2000</code> into the config, and it seems to at least work well with simulated dial-up connections.</li>
</ol>
<p>After a few typographic adjustments, fixing some long-standing annoyances, and sprinkling in just a hint of structured data, I think I’ll be content with the site for at least another week.</p>
<div class="footnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://gist.github.com/scottjehl/87176715419617ae6994">The evolution and discussion</a> of Scott Jehl’s loadCSS solution is pretty interesting. <a href="{fnref1:1:url||}" rev="footnote" class="footnote-backref">↩</a></p>
</li>
</ol>
</div></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Imager + fortrabbit Object Storage]]></title>
            <link>https://workingconcept.com/blog/imager-fortrabbit-object-storage</link>
            <guid>https://workingconcept.com/blog/imager-fortrabbit-object-storage</guid>
            <pubDate>Tue, 21 Aug 2018 19:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/fortrabbit-imager@2x.COXWVXd9_2lhj0s.webp" type="image/webp"><source src="https://workingconcept.com//_astro/fortrabbit-imager@2x.COXWVXd9_ZaOoiP.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/fortrabbit-imager@2x.COXWVXd9_ZLs2nn.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/fortrabbit-imager@2x.COXWVXd9_1fBGgv.png" decoding="async" loading="lazy" alt width="1300" height="600"> </picture>  </figure> </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>After working with <a href="https://github.com/aelvan/Imager-Craft">Imager</a> in a fortrabbit-hosted <a href="https://craftcms.com/">Craft 3</a> project, I published a storage driver that André claimed would be easy to write. (It was.) The <a href="https://github.com/workingconcept/craft-fortrabbit-object-storage-driver">Imager Storage Driver for fortrabbit Object Storage</a> makes it possible to store Imager’s transforms in fortrabbit’s Object Storage Volumes, which you’ll want to do if you’re working with fortrabbit’s <a href="https://www.fortrabbit.com/pricing-pro">Pro Stack</a>. (Imager’s <code>aws</code> driver doesn’t work because fortrabbit’s endpoints are different.)</p><p>If you have an app running on the Pro Stack—<em>not</em> the Univeral Stack, which uses something different called Web Storage—you can use Imager for your transforms and have access to all kinds of magic. Here’s how to get those transforms stored on fortrabbit’s Object Storage.</p><p>What you need:</p><ul><li>a site built with Craft CMS 3+</li><li>Imager 2+</li><li>fortrabbit Pro Stack with Object Storage</li></ul><h2>1. Install the Imager plugin if you haven’t already.</h2></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">composer</span><span style="color:#C3E88D"> require</span><span style="color:#C3E88D"> aelvan/imager</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Require the Imager from your project directory, then install from <em>Settings</em> → <em>Plugins</em> in the Craft control panel or via <code>./craft install/plugin imager</code>.</p><h2>2. Install the Imager fortrabbit Object Storage driver.</h2></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#FFCB6B">composer</span><span style="color:#C3E88D"> require</span><span style="color:#C3E88D"> workingconcept/imager-fortrabbit-object-storage-driver</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Again install via <em>Settings</em> → <em>Plugins</em> in the Control panel or <code>./craft install plugin imager-fortrabbit-object-storage-driver</code>.</p><h2>3. Configure the driver for your site.</h2><p>If you don’t already have Imager creating transforms in your templates, pick an Asset somewhere and set up a test:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="twig"><code><span class="line"><span style="color:#546E7A;font-style:italic">{# get an Asset or URL #}</span></span>
<span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">set</span><span style="color:#EEFFFF"> resizedImage</span><span style="color:#89DDFF"> = </span><span style="color:#EEFFFF">craft</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">imager</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">transformImage</span><span style="color:#89DDFF">(</span><span style="color:#EEFFFF">imageAsset</span><span style="color:#89DDFF">, { </span><span style="color:#EEFFFF">width</span><span style="color:#89DDFF">: </span><span style="color:#F78C6C">500</span><span style="color:#89DDFF"> }) %}</span></span>
<span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">img</span><span style="color:#C792EA"> src</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"{{ </span><span style="color:#EEFFFF">resizedImage</span><span style="color:#89DDFF">.</span><span style="color:#EEFFFF">url</span><span style="color:#89DDFF"> }}"</span><span style="color:#C792EA"> alt</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">test</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">/></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>You should get a nicely-resized image, because Imager will store its transforms in <code>web/imager</code> by default. Next we’ll have it store those transforms with fortrabbit Object Storage instead of local storage.</p><p>Create <code>config/imager.php</code> if you don’t already have it. There are <a href="https://github.com/aelvan/Imager-Craft/blob/craft3/src/models/Settings.php">loads of available settings</a>, so while you should season to taste we’ll focus only on transforms here. If you configured your site using environment variables like fortrabbit suggests and want transforms stored in a folder called “transforms,” this should <em>almost</em> work right out of the gate:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="php"><code><span class="line"><span style="color:#89DDFF">&#x3C;?</span><span style="color:#EEFFFF">php</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic">return</span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">storageConfig</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">        '</span><span style="color:#C3E88D">fortrabbit</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">accessKey</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">OBJECT_STORAGE_KEY</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">secretAccessKey</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">OBJECT_STORAGE_SECRET</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">endpoint</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">https://</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> .</span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">OBJECT_STORAGE_SERVER</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">region</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">OBJECT_STORAGE_REGION</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">bucket</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">OBJECT_STORAGE_BUCKET</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">folder</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">transforms</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#89DDFF">            '</span><span style="color:#C3E88D">requestHeaders</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [],</span></span>
<span class="line"><span style="color:#89DDFF">        ]</span></span>
<span class="line"><span style="color:#89DDFF">    ],</span></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">storages</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> [</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">fortrabbit</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">],</span></span>
<span class="line"><span style="color:#89DDFF">    '</span><span style="color:#C3E88D">imagerUrl</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#82AAFF"> getenv</span><span style="color:#89DDFF">(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">IMAGER_URL</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">),</span></span>
<span class="line"><span style="color:#89DDFF">];</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>You’ll need to add an <code>IMAGER_URL</code> environment variable set to your public, absolute Object Storage URL. That’ll look something like <code>https://my-app.objects.frb.io/</code>. The entire .env would look something like this, where <code>my-app</code> is a placeholder for your actual app handle:</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#EEFFFF">OBJECT_STORAGE_BUCKET</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">my-app</span></span>
<span class="line"><span style="color:#EEFFFF">OBJECT_STORAGE_HOST</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">my-app.objects.frb.io</span></span>
<span class="line"><span style="color:#EEFFFF">OBJECT_STORAGE_KEY</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">my-app</span></span>
<span class="line"><span style="color:#EEFFFF">OBJECT_STORAGE_REGION</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">us-east-1</span></span>
<span class="line"><span style="color:#EEFFFF">OBJECT_STORAGE_SECRET</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">••••••••••••••••••••••••••</span></span>
<span class="line"><span style="color:#EEFFFF">OBJECT_STORAGE_SERVER</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">objects.us1.frbit.com</span></span>
<span class="line"><span style="color:#EEFFFF">IMAGER_URL</span><span style="color:#89DDFF">=</span><span style="color:#C3E88D">https://my-app.objects.frb.io/transforms/</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>You could also just hardcode that URL into <code>config/imager.php</code> if you wanted to, but what fun is that?</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="php"><code><span class="line"><span style="color:#89DDFF">'</span><span style="color:#C3E88D">imagerUrl</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF"> =></span><span style="color:#89DDFF"> '</span><span style="color:#C3E88D">https://my-app.objects.frb.io/transforms/</span><span style="color:#89DDFF">'</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><h2>4. Make sure it works!</h2><p>Clear out Imager’s transform caches and refresh your test page. You should see your new transforms being stored to and served from fortrabbit’s Object Storage.</p><p>Leave a comment or <a href="https://github.com/workingconcept/craft-fortrabbit-object-storage-driver/issues">open an issue on GitHub</a> if you run into any trouble, and otherwise enjoy those fortrabbit-stored transforms!</p></div> </article>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Preventing Widows in Responsive Layouts]]></title>
            <link>https://workingconcept.com/blog/preventing-widows-responsive-layouts</link>
            <guid>https://workingconcept.com/blog/preventing-widows-responsive-layouts</guid>
            <pubDate>Mon, 21 Jan 2019 23:45:00 GMT</pubDate>
            <content:encoded><![CDATA[<article class="blog-post-content"> <div class="block images-block max-w-lg mx-auto my-12">  </div> <div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><div class="larger text-center"><p>Nothing diminishes the impact of a headline like having its last word break and dangle helplessly.</p></div>
<p><strong>Update 1/31/19:</strong> I updated my approach here after finding that it could be simpler and was previously capable of displaying two horizontally-aligned words without a space between them at just the right size.</p>
<hr>
<p>You’re likely aware that this dangling specimen is typographically referred to as a <em>widow</em> or an <em>orphan</em>, best avoided since it’s a ding to readability.</p>
<p>In a print layout you might adjust the tracking between letters to either pull that widow back into the previous line or add a sense break so there’s a natural continuation point to a line with its own clause. But this is the interweb!</p>
<h2>The Non-Breaking Space</h2>
<p>One of my favorite ways to prevent widows in web layouts has been to add a non-breaking space between the last two words of a line so they’re forced to stay together if anything wraps.</p>
<p>The following headline, for example…</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">The quick fox jumps over the lazy dog</span><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>…would become…</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">The quick fox jumps over the lazy&#x26;nbsp;dog</span><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This way, the words <em>lazy</em> and <em>dog</em> will be visually separated but functionally glued together. <em>Dog</em> won’t ever wrap onto a new line all by itself.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/widow-example.B2dHQ0dE_2pnBBX.webp" type="image/webp"><source src="https://workingconcept.com//_astro/widow-example.B2dHQ0dE_Z2g7xTI.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/widow-example.B2dHQ0dE_10G0Iu.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/widow-example.B2dHQ0dE_1HGMh5.png" decoding="async" loading="lazy" alt="Visual example of widow prevention." width="1306" height="608"> </picture> <figcaption> <p> A non-breaking space keeps dog from being alone.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Various CMS plugins have made this easy, like the free and excellent <a href="https://github.com/TopShelfCraft/Wordsmith" target="_blank">Wordsmith Craft plugin</a> that allows fine-tuning of widow prevention beyond the norm. You can also write a pretty simple regular expression to swap out the last space in any common programming or templating language.</p>
<h2>The Problem With the Non-Breaking Space</h2>
<p>One problem with this approach, however, is that a non-breaking chunk of text can actually push a narrower (mobile) layout out of its normal bounds if the joined words are long enough.</p>
<p>There’s plenty of room for the adjusted last line at a wider viewport, but out triumphantly-paired last words force a <em>narrower</em> viewport to scroll.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/widow-example-wide.DUvRSCqX_1MLRTT.webp" type="image/webp"><source src="https://workingconcept.com//_astro/widow-example-wide.DUvRSCqX_2rXlRV.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/widow-example-wide.DUvRSCqX_EbpfW.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/widow-example-wide.DUvRSCqX_smKec.png" decoding="async" loading="lazy" alt="Visual example of desirable wrap with sufficient width." width="1306" height="554"> </picture> <figcaption> <p> Looking great when there&#39;s enough width.  </p> </figcaption> </figure><figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/widow-example-narrow.D6xPPk6r_cB0md.webp" type="image/webp"><source src="https://workingconcept.com//_astro/widow-example-narrow.D6xPPk6r_Z2juGW5.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/widow-example-narrow.D6xPPk6r_2a3MMj.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/widow-example-narrow.D6xPPk6r_ZS3BmJ.png" decoding="async" loading="lazy" alt="Visual example of non-wrapped type forcing the layout open." width="1046" height="946"> </picture> <figcaption> <p> When the viewport is narrower than the joined words, it forces the layout to scroll.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Dan Mall shared <a href="http://v3.danielmall.com/articles/responsive-line-breaks/" target="_blank">two strategies for supporting responsive line breaks</a> that got me thinking, but those are about breaking lines rather than preventing widows.</p>
<h2>Breaking the Non-Breaking Space</h2>
<p>I found a solution (I doubt I’m first to think of) that achieves each of my goals:</p>
<ul>
<li>Prevent widows.</li>
<li>Avoid forcing narrower layouts to scroll.</li>
<li>Avoid getting JavaScript involved in favor of a CSS-only approach.</li>
<li>Maintain normal spacing if the stylesheet is completely removed.</li>
</ul>
<p>The trick is to <del>wrap the non-breaking space with a classed span, to which I can add a psuedo-element to allow breaking at a narrower viewport width</del> replace the non-breaking space with a classed <em>regular</em> space and use the <code>white-space</code> property to control it at different breakpoints. In other words I…</p>
<ol>
<li>Keep relying on Wordsmith’s <code>widont</code> Twig filter, which is a champ about adding that non-breaking space as I mentioned above. </li>
<li>Use a Twig macro to <del>wrap</del> replace the non-breaking space with a classed span wrapping a normal space I can style with CSS. </li>
<li><del>Insert a hidden psuedo element that gets <code>display: inline-block</code> at a narrower breakpoint that would risk being held open.</del> Use <code>white-space: nowrap</code> to prevent widows when there’s room, and <code>white-space: normal</code> to allow breaking when things get tight.</li>
</ol>
<p><strong>Starting point</strong>: widow-prone headline.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">The quick fox jumps over the ostentatious hippopotamus</span><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>Twig macro</strong> that adds a non-breaking space between the last two words, then wraps that non-breaking space in a <code>span.widont</code>.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="twig"><code><span class="line"><span style="color:#89DDFF">{% </span><span style="color:#89DDFF;font-style:italic">macro</span><span style="color:#89DDFF"> responsive_widont(</span><span style="color:#EEFFFF">string</span><span style="color:#89DDFF">) -%}</span></span>
<span class="line"><span style="color:#89DDFF">    {{ </span><span style="color:#EEFFFF">string</span><span style="color:#89DDFF"> | </span><span style="color:#EEFFFF">widont</span><span style="color:#89DDFF"> | replace(</span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">&#x26;nbsp;</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">, </span><span style="color:#89DDFF">'</span><span style="color:#C3E88D">&#x3C;span class="widont"> &#x3C;/span></span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">) | </span><span style="color:#EEFFFF">raw</span><span style="color:#89DDFF"> }}</span></span>
<span class="line"><span style="color:#89DDFF">{%- </span><span style="color:#89DDFF;font-style:italic">endmacro</span><span style="color:#89DDFF"> %}</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>Resulting markup</strong> ready to be styled.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">The quick fox jumps over the ostentatious</span><span style="color:#89DDFF">&#x3C;</span><span style="color:#F07178">span</span><span style="color:#C792EA"> class</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#C3E88D">widont</span><span style="color:#89DDFF">"</span><span style="color:#89DDFF">></span><span style="color:#89DDFF"> &#x3C;/</span><span style="color:#F07178">span</span><span style="color:#89DDFF">></span><span style="color:#EEFFFF">hippopotamus</span><span style="color:#89DDFF">&#x3C;/</span><span style="color:#F07178">h1</span><span style="color:#89DDFF">></span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p><strong>CSS class</strong> to maintain a non-breaking space but let it wrap when the viewport narrows.</p></div><figure class="code max-w-lg mx-auto px-6"> <pre class="astro-code material-theme" style="background-color:#263238;color:#EEFFFF; overflow-x: auto;" tabindex="0" data-language="css"><code><span class="line"><span style="color:#546E7A;font-style:italic">/**</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> * Control wrapping with CSS! :)</span></span>
<span class="line"><span style="color:#546E7A;font-style:italic"> */</span></span>
<span class="line"><span style="color:#89DDFF">.</span><span style="color:#FFCB6B">widont</span><span style="color:#89DDFF"> {</span></span>
<span class="line"><span style="color:#B2CCD6">    white-space</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> nowrap</span><span style="color:#89DDFF">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#EEFFFF">    @media screen and (</span><span style="color:#B2CCD6">max-width</span><span style="color:#89DDFF">:</span><span style="color:#F78C6C"> 576px</span><span style="color:#EEFFFF">) {</span></span>
<span class="line"><span style="color:#B2CCD6">        white-space</span><span style="color:#89DDFF">:</span><span style="color:#EEFFFF"> normal</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#89DDFF">    }</span></span>
<span class="line"><span style="color:#EEFFFF">}</span></span></code></pre>  </figure><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>Once the viewport width is less than 576px, the span is forced shut so it doesn’t occupy any horizontal or vertical space—but it’s not <em>hidden</em> because the empty pseudo element pops into place and once again allows the text to wrap.</p></div><div class="block images-block max-w-md mx-auto my-12"> <figure class="text-center"> <picture> <source src="https://workingconcept.com//_astro/responsive-widow-break.D9lc8qzf_Z1E8wfe.webp" type="image/webp"><source src="https://workingconcept.com//_astro/responsive-widow-break.D9lc8qzf_1dja3Q.jpeg" type="image/jpeg"><source src="https://workingconcept.com//_astro/responsive-widow-break.D9lc8qzf_Z1gV4Xr.avif" type="image/avif">  <img src="https://workingconcept.com//_astro/responsive-widow-break.D9lc8qzf_I9S2s.png" decoding="async" loading="lazy" alt="Visual example of result with each layout accommodating type as expected." width="1306" height="946"> </picture> <figcaption> <p> Widow prevented when there&#39;s room, layout maintained when there&#39;s not.  </p> </figcaption> </figure> </div><div class="max-w-md mx-auto my-8 px-6 md:px-0 copy"><p>This isn’t perfect because it only addresses simple containers that are normally wide and end up hugging the viewport as they scale down; there could be trickier situations like columns or changing card sizes that this wouldn’t address.</p>
<p>That said, it seems to work well and I’d love to know if you’ve handled this differently or have any ideas to make it better!</p></div> </article>]]></content:encoded>
        </item>
    </channel>
</rss>