<?xml version="1.0" encoding="UTF-8"?><rss 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" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[jamesfacts]]></title><description><![CDATA[Hi, I'm James. I'm a developer and writer focused on digital publishing.]]></description><link>https://jamesfacts.com/</link><image><url>https://jamesfacts.com/favicon.png</url><title>jamesfacts</title><link>https://jamesfacts.com/</link></image><generator>Ghost 4.48</generator><lastBuildDate>Sun, 05 Apr 2026 15:21:01 GMT</lastBuildDate><atom:link href="https://jamesfacts.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Deploying a Roots Sage theme to WPEngine, with a bonus debugging tool]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I love the <a href="http://roots.io">Sage starter theme</a> and I love WPEngine, but WPEngine doesn&apos;t seem to love Sage v9+. Once you&apos;ve got your Sage theme running in WPE, chances are it will run pretty great, but it takes a little effort to pull off your first deploy.</p>]]></description><link>https://jamesfacts.com/deploying-a-roots-sage-theme-to-wpengine/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdba5</guid><category><![CDATA[development]]></category><category><![CDATA[WordPress]]></category><category><![CDATA[WPEngine]]></category><category><![CDATA[Sage 9]]></category><category><![CDATA[Gitlab]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Thu, 11 Apr 2019 03:57:24 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2019/04/deploy-sage-wpe.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2019/04/deploy-sage-wpe.jpg" alt="Deploying a Roots Sage theme to WPEngine, with a bonus debugging tool"><p>I love the <a href="http://roots.io">Sage starter theme</a> and I love WPEngine, but WPEngine doesn&apos;t seem to love Sage v9+. Once you&apos;ve got your Sage theme running in WPE, chances are it will run pretty great, but it takes a little effort to pull off your first deploy.</p>
<h2 id="hereshowiapproachmysagewpeworkflow">Here&apos;s how I approach my Sage + WPE workflow:</h2>
<ul>
<li><strong>Build on a Gitlab runner.</strong> Sage needs to be installed in a complete, functioning WordPress install before the Blade templates can be compiled. Gitlab runners are an easy way to get access to a MySQL server so it&apos;s possible to spin up a functional WP install.</li>
<li><strong>Vendor files are required.</strong> You can&apos;t run a theme, even after it&apos;s compiled, without the vendor files generated by Composer. Make sure these files are included in your built push to WPE.</li>
<li><strong>Move the Blade template dir.</strong> WPE doesn&apos;t allow php files outside the theme or plugin dir. I place my compiled templates in a <code>mu-plugin</code> dir.</li>
<li><strong>Always Use Production &quot;Sites&quot;.</strong> WPE offers multiple site installs (Dev, Stage, Prod) inside a top-level site container. Confusingly, they also allow you to have both a regular production environment and a &quot;legacy staging&quot; environment for all of those site sites... for a total of six environments. This is both overkill and misleading, because the legacy staging server config is not the same as a production config. Always use production sites to avoid disappointment when you deploy the live site.</li>
<li><strong>Test Your Pipeline Locally.</strong> Running the entire Gitlab script as you test your pipeline is tedious. I created a <a href="https://github.com/the-baffler-foundation/sage-wpe-runner">Docker config along with basic build deploy / scripts</a> to make it possible to step through the deployment process interactively.</li>
<li><strong>Ask WPE support to set a caching exemption</strong> WPE has aggressive object-caching that can interfere with your Blade templates. Ask their support team to exempt your compiled templates from their internal caching.</li>
</ul>
<p>Depending on the exact version of Sage that you&apos;re running, and the Composer packages that you&apos;re using, it might be necessary to tweak these files, but here&apos;s what I use in my live Gitlab runner:</p>
<p><a href="https://gist.github.com/jamesfacts/c850c615bd0eb4d84e79596b6ef46023"><code>.gitlab-ci.yml</code></a><br>
<a href="https://gist.github.com/jamesfacts/45b8726b2b4418285fa3acc37988990c"><code>bin/deploy</code></a></p>
<p>Good luck! If all goes well, here&apos;s what you&apos;ll see:<br>
<img src="/content/images/2019/04/successful-deployment.jpg" alt="Deploying a Roots Sage theme to WPEngine, with a bonus debugging tool" loading="lazy"></p>
<p><small>Lead photo: <a href="https://unsplash.com/@cleversparkle">Clever Sparkle</a></small></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Running a local WP instance exported from WPEngine]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>While WPE is a fantastic host for managed WordPress sites, with helpful environment options... there will probably come a day when it&apos;s easier to work with a local instance of your site. Maybe you&apos;d like to make some tweaks to a theme, or you need to</p>]]></description><link>https://jamesfacts.com/running-a-local-wp-instance-exported-from-wpengine/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdba4</guid><category><![CDATA[WordPress]]></category><category><![CDATA[development]]></category><category><![CDATA[WPEngine]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Wed, 13 Mar 2019 01:07:43 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2019/03/wpe-local-install-copy-tape.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2019/03/wpe-local-install-copy-tape.jpg" alt="Running a local WP instance exported from WPEngine"><p>While WPE is a fantastic host for managed WordPress sites, with helpful environment options... there will probably come a day when it&apos;s easier to work with a local instance of your site. Maybe you&apos;d like to make some tweaks to a theme, or you need to run a CLI tool that WPE doesn&apos;t allow. There are a couple small quirks in the WPE environment to contend with, but process of configuring a local copy of a WPEngine is mostly painless.</p>
<h2 id="downloadacopyofyoursiteswpfiles">Download a copy of your site&apos;s WP files</h2>
<p>Under the &apos;Backup Points&apos; section of your WPE dashboard, there should be a set of daily checkpoints, each of which contains all the non-database assets needed to run your site. Select the most recent backup and choose &quot;Download Zip.&quot; It will take a few minutes to prepare the download if your site contains a large amount of images, but not as long as you might fear. I prefer to download the <strong>entire</strong> backup, with <code>/uploads</code> and <code>/plugins</code> to ensure you fully simulate the remote site locally. The backup point includes all the WP core files, too&#x2014;it&apos;s a complete install.</p>
<p><img src="/content/images/2019/03/Screen-Shot-2019-03-12-at-5.05.56-PM.png" alt="Running a local WP instance exported from WPEngine" loading="lazy"></p>
<p>Uncompress the backup package and point your local WP server (Valet, Trellis, VVV) at the new site directory.</p>
<h2 id="createalocalcopyoftheremotesitesmysqldatabase">Create a local copy of the remote site&apos;s MySQL database</h2>
<p>Now that you have the WordPress files in a local dir, it&apos;s time to snag the database used by the live site, too. The WPE backup point does include a copy of the db under <code> wp-content/mysql.sql</code> but I find it&apos;s easier to use phpMyAdmin, to get a little more control over how the export is formatted. Inside the WPE admin panel, there&apos;s a link directly to the WPE phpMyAdmin. Under the &apos;Export&apos; tab in phpMyAdmin, you&apos;ll see the option to download a direct copy of the live db. Choose the &apos;custom&apos; option so you can pick just the main db. You want the one that starts with &apos;wp_&apos;, not &apos;snapshot_&apos;... the snapshot one is a WPE system file.</p>
<p><img src="/content/images/2019/03/Screen-Shot-2019-03-12-at-5.29.02-PM.png" alt="Running a local WP instance exported from WPEngine" loading="lazy"></p>
<p>Most likely the default custom export settings will be ok, but you should pick a <strong>Compression</strong> option, to speed the download and import process. On the local side, fire up your local phpMyAdmin and import the downloaded file. If it&apos;s a big db and you&apos;re running MAMP, there&apos;s a good chance the db will exceed MAMP&apos;s mystifyingly small upload limits. In the free edition of MAMP you can <a href="https://timmyomahony.com/blog/MAMP-phpmyadmin-and-big-database-imports-errors-or-timeouts/">easily update your php ini files</a>, and in the paid version of MAMP you will need to <a href="https://stackoverflow.com/questions/13213141/php-ini-resetting-on-mamp-server-restart/45376353#45376353">use the &apos;template&apos; interface</a>. Also worth noting it&apos;s simple to import a MySQL db through the command line.</p>
<h2 id="modifythelocalwpconfigtoremovewpesystemsettings">Modify the local WP config to remove WPE system settings</h2>
<p>Here&apos;s the tricky part: removing any remaining WPE artifacts that might interfere with viewing the local site. WPE adds a lot of their own variables inside <code>wp-config.php</code> that you can easily live without. It&apos;s critical that you delete two settings in particular: <code>WP_SITEURL</code> and <code>WP_HOME</code>. These variables will cause your browser to redirect to the live URL, not your local site. Change your &apos;Database Configuration&apos; within wp-config, too. <code>DB_NAME</code>, <code>DB_USER</code>, <code>DB_PASSWORD</code> are hopefully self-explanitory, but the WPE config for <code>DB_HOST</code> and <code>DB_HOST_SLAVE</code> may be subtly different from your local setup. There are also <a href="https://wpengine.com/support/best-practices-uploading-wp-engine-site-another-environment/">seven WPE plugins that are no longer necessary on a local site</a>:</p>
<pre><code>wp-content/mu-plugins/mu-plugin.php
wp-content/mu-plugins/wpengine-common/
wp-content/mu-plugins/slt-force-strong-passwords.php
wp-content/mu-plugins/force-strong-passwords/
wp-content/mu-plugins/stop-long-comments.php
wp-content/advanced-cache.php
wp-content/object-cache.php
</code></pre>
<p>Finally, you will want to edit the db file directly in phpMyAdmin to update the two db values that tell WordPress how to build URLs to your site. <code>siteurl</code> and <code>home</code> both live under <code>wp_options</code> in the database. Once you&apos;ve modified <code>wp-config.php</code> it should be possible to access your site locally and log into the WP admin area. From here, <s>I like to use the simple and effective <a href="https://wordpress.org/plugins/search-regex/">Search Regex plugin</a> to update links inside posts</s> use the <code>wp cli</code>&apos;s <a href="https://developer.wordpress.org/cli/commands/search-replace/">built-in find and replace</a>. So much easier than a WP admin plugin!  Remember that your live install was https and the local install might not be, so you will need to include the full url in your queries.</p>
<p>Once you have your local setup running, you might want to add a Git integration to your WPE install, or create a CI pipeline to deploy to WPE for you.</p>
<p><small>Photo: <a href="https://unsplash.com/photos/mwWZTLr9Tcg">Ingo Shulz</a></small></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Set Multiple WordPress Page Titles With The SEO Framework]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I love the plugin <a href="https://theseoframework.com/">The SEO Framework</a> by Sybre Waaijer. It&apos;s a breath of fresh air compared to the bloated, distracting, and overly-promotional SEO plugins that have been so popular in WP for years. One advantage of using a plugin that&apos;s well-written and accessible is that</p>]]></description><link>https://jamesfacts.com/set-multiple-wordpress-page-titles-with-seo-framework/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdba3</guid><category><![CDATA[development]]></category><category><![CDATA[WordPress]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Wed, 11 Jul 2018 16:36:04 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2018/07/custom-wp-title-palm.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2018/07/custom-wp-title-palm.jpg" alt="Set Multiple WordPress Page Titles With The SEO Framework"><p>I love the plugin <a href="https://theseoframework.com/">The SEO Framework</a> by Sybre Waaijer. It&apos;s a breath of fresh air compared to the bloated, distracting, and overly-promotional SEO plugins that have been so popular in WP for years. One advantage of using a plugin that&apos;s well-written and accessible is that it&apos;s easier to make little tweaks to fit the plugin to better suit your needs. In my case, I recently needed the option to display multiple page titles: one title for browsers, and a custom override title for Facebook, whose algorithm now censors content with rude words.</p>
<p>Sybre&apos;s source code is easy to follow, and the documentation is pretty good, but I thought I&apos;d post my specific configuration in case anyone is looking to perform similar modifications.</p>
<p>This hook calls a custom function when The SEO Framework is preparing to output the title:</p>
<p><code>add_filter( &apos;the_seo_framework_ogtitle_output&apos;, &apos;og_title_override&apos;, 10, 2 );</code></p>
<p>This custom function checks to see if a particular meta value is set on the post in question. If the meta value exists, it returns that value as the title, otherwise it returns the title set by the SEO Framework.</p>
<pre><code>/**
 * Injects a replacement Open Graph title when user fills out custom metabox.
 *
 * @param string $title The current title.
 * @param int $id The Post, Page or Term ID.
 * @return string Title. User input title is santized on metabox save
 */

function og_title_override( $title = &apos;&apos;, $post_id = 0) {

    $user_title = get_post_meta( $post_id, &apos;_fb_override_og_title&apos;, true );

    if  ( $user_title != &apos;&apos; ) {
      $title = $user_title;
    }

    return $title;
}
</code></pre>
<p>A very simple function, made easy to implement by a well-written plugin.</p>
<p><small>Photo credit: <a href="https://unsplash.com/@abrkett">Adam Birkett</a></small></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Building a Continuous Integration Pipeline for Ghost Themes Using Git and Codeship]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>One of the best things about the Ghost blogging platform is the impressive flexibility in development and deployment options. Of course, with that flexibility comes a commitment to doing a lot of things yourself. I recently configured a Codeship Continuous Integration pipeline for deploying changes to a Ghost theme I</p>]]></description><link>https://jamesfacts.com/building-a-continuous-integration-pipeline-for-your-ghost-theme/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdba1</guid><category><![CDATA[development]]></category><category><![CDATA[ghost]]></category><category><![CDATA[codeship]]></category><category><![CDATA[continuous integration]]></category><category><![CDATA[git]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Tue, 03 Apr 2018 20:39:15 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2018/04/ci-arrows.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2018/04/ci-arrows.jpg" alt="Building a Continuous Integration Pipeline for Ghost Themes Using Git and Codeship"><p>One of the best things about the Ghost blogging platform is the impressive flexibility in development and deployment options. Of course, with that flexibility comes a commitment to doing a lot of things yourself. I recently configured a Codeship Continuous Integration pipeline for deploying changes to a Ghost theme I maintain, and while it was fairly straightforward, I&apos;m documenting the process for posterity. Feel free to skip over all the CI steps if you&apos;re only interested in using Git push to update a theme on a remote server.</p>
<p>Even though I&apos;m the only developer working on this project&#x2014;I know, I won&apos;t enjoy all the benefits of CI&#x2014;I like using Codeship to run the build and test processes for my theme. This way, I get to keep a nice clean local working repo, and avoid any build scripts on my live server.</p>
<h3 id="thefullstackformyghostthemelookslikethis">The full stack for my Ghost theme looks like this:</h3>
<ul>
<li>Local development Ghost 1.20.3 server</li>
<li>Ghost theme running Webpack and various JS / CSS processors</li>
<li>Origin repo in Github</li>
<li>Free tier Codeship account using node 6.9</li>
<li>Digital Ocean Ubuntu 16.04 droplet also running Ghost 1.20.3</li>
</ul>
<h3 id="broadlyspeakingtheciprocesslookslikethis">Broadly speaking, the CI process looks like this:</h3>
<ul>
<li>Make local changes, commit, push to Github</li>
<li>Github triggers Codeship build</li>
<li>Codeship compiles the theme for production, swaps .gitignore files</li>
<li>Codeship server pushes to repo on production server</li>
<li>Production server receives updated theme files, restarts to display changes</li>
</ul>
<h2 id="pushyourthemetoaremoterepo">Push your theme to a remote repo</h2>
<p>You can&apos;t push a project directly to Codeship. When you create a Codeship project you&apos;ll have a choice to integrate to either Gitlab, Bitbucket or Github. I chose to use Github, but they&apos;re all roughly equivalent. So, from your local machine, prepare a repo from your working files. In the primary <code>.gitignore</code> file, you should ignore any production or build files. My <code>.gitignore</code> looks like this:</p>
<pre><code># ignore development / production files 

/node_modules
/assets
</code></pre>
<p>Next, create a <code>.gitignore</code> file to be used after Codeship builds your theme. This ignore list should specify working files, and allow production files to be committed. I call my build ignore file <code>.codeshipignore</code> and include these files:</p>
<pre><code># allow assets to be pushed from codeship

/node_modules
/src
build.gitignore
</code></pre>
<p>Once you have all the appropriate local files, go ahead and push to your remote repo, then create a Codeship project and give it access to your origin repo.</p>
<h2 id="configurecodeshiptobuildyourtheme">Configure Codeship to build your theme</h2>
<p>Codeship will prompt you for some &quot;Setup Commands.&quot; This is where you can specify the environment required for your theme&apos;s tests. If you don&apos;t have any test commands to run, don&apos;t worry about these fields. After you save this setup, you&apos;ll want to click the &quot;project settings&quot; button, and navigate over to the &quot;Deploy&quot; tab. This is where Codeship runs build processes.</p>
<p>Because I&apos;m pushing to a theme that&apos;s on a production server, I used the <code>master</code> branch as my deployment trigger. If you push to multiple servers, i.e., a staging and a production server, you might want to configure multiple pipelines.</p>
<p>Once you&apos;ve established a pipeline, it&apos;s time to add your build script (pick the &apos;custom script&apos; option). These are commands you feed the Codeship server to be executed just like you&apos;re in a terminal session.</p>
<p>Here&apos;s what my build script looks like:</p>
<pre><code>git config --global user.email &quot;xxxxx&quot;
git config --global user.name &quot;xxxxxx&quot;
git checkout master
mv .gitignore build.gitignore
mv .codeshipignore .gitignore
nvm install 6.9.0
nvm use 6.9.0
npm install
npm run build:prod
ls -la
git add .
git commit -m &quot;deploy&quot;
git remote add live ssh://git@xxx.xx.xx.xxx/home/git/xxxx-ghost/
git push live master -f
</code></pre>
<p>It&apos;s a fairly straightforward process: ensure you&apos;re on the right version of Node, install Node modules, build the theme from source, swap the .gitinore files, add your server&apos;s repo, and push. Codeship&apos;s environments reset after each build so it&apos;s necessary to add the remote every time.</p>
<h2 id="configuringyourliveservertoreceivecodeshippushes">Configuring your live server to receive Codeship pushes</h2>
<p>Next, you need to be sure your live server can receive the Codeship repo. Log into your remote server through ssh, and create a bare repo for Codeship to push to, inside the home dir for the <code>git</code> user (this user should be created already, assuming you have git installed on this machine). You will also need to ensure the Ghost CLI is installed on your server.</p>
<pre><code>cd /home/git
mkdir yourblog.git
cd yourblog.git
git --bare init
</code></pre>
<p>Jump into the &apos;hooks&apos; directory and create a file called <code>post-receive</code>.</p>
<pre><code>cd /home/git/yourblog.git/hooks
touch post-receive
</code></pre>
<p>Open the new file with the editor of your choice, and paste the following lines in, adjusting for the path to your ghost install / theme:</p>
<pre><code>#!/bin/sh
git --work-tree=/var/www/yourblog/content/themes/xxxx-xxxx --git-dir=/home/git/yourblog.git/ checkout -f
cd /var/www/yourblog
sudo /usr/lib/node_modules/ghost-cli/bin/ghost restart
</code></pre>
<p>This script will direct git to place the repo files in your existing ghost theme directory, and to use the CLI to restart your blog instance.</p>
<p>Next, we need to ensure this script can actually execute. Change the permissions for <code>post-receive</code>:</p>
<pre><code>chmod +x post-receive
</code></pre>
<p>...and it would be nice if Git owned the proper directories, too:</p>
<pre><code>cd /home/git
chown -R git:git yourblog.git
cd /var/www/yourblog/content/themes
chown -R git:git yourtheme
</code></pre>
<p>At this point, we need to ensure Codeship can push to the remote server.  Under the General Project Settings tab in Codeship, copy the &quot;SSH public key&quot; and paste it into the <code>git</code> .ssh dir:</p>
<pre><code>cd /home/git/.ssh
touch authorized_keys
nano authorized_keys
[paste the key and close/save]
</code></pre>
<p>There&apos;s one last task to complete! We need to ensure the <code>git</code> user can run the Ghost CLI&apos;s restart command without being prompted for a password, since the Codeship server is unable to respond interactively. Open up the sudoers file with this command:</p>
<pre><code>visudo
</code></pre>
<p>On newer flavors of Ubuntu this opens up a special session in nano, ensuring that the syntax of your sudoers is correct before saving, minimizing the chance that you blow up your system permissions. Go ahead and insert these lines at the end of the file, where <code>ghost_instance</code> is the name of the specific Ghost blog you have hooked the Git repo to:</p>
<pre><code># Allow git to restart ghost blogs
git ALL = NOPASSWD: /usr/lib/node_modules/ghost-cli/bin/ghost restart ghost_instance
</code></pre>
<p>The line added translates like this:<br>
<code>git ALL</code> == allow connections from all hosts<br>
<code>NOPASSWD:</code> == don&apos;t require the pw for sudo actions for...<br>
<code>/usr/lib/node_modules/ghost-cli/bin/ghost</code> == the absolute path to the Ghost CLI</p>
<p>Source your bash profile to reload the sudoers list:</p>
<pre><code>. ~/.bashrc
</code></pre>
<p>That ought to do it.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Attractive CSS Color Keywords]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>When I&apos;m developing a new layout, I like to label components with background colors so I can see exactly where the position boundaries lay. Unfortunately, I can never seem to remember what CSS named colors are the most attractive, and it starts to strain my eyes coloring boxes</p>]]></description><link>https://jamesfacts.com/attractive-css-color-keywords/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdba0</guid><category><![CDATA[development]]></category><category><![CDATA[css]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Thu, 08 Mar 2018 22:25:28 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2018/03/color-rugs.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2018/03/color-rugs.jpg" alt="Attractive CSS Color Keywords"><p>When I&apos;m developing a new layout, I like to label components with background colors so I can see exactly where the position boundaries lay. Unfortunately, I can never seem to remember what CSS named colors are the most attractive, and it starts to strain my eyes coloring boxes all &quot;yellow&quot; and &quot;red&quot;. So, here&apos;s a handy chart to the CSS colors that slightly less painful to paint with.<br>
<br></p>
<section style="max-width: 600px; margin-left: auto; margin-right: auto; text-align: center; width: 100%">
  <figure style="background-color: FireBrick; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">FireBrick</h4>
  </figure>
  <figure style="background-color: LightCoral; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">LightCoral</h4>
  </figure>
  <figure style="background-color: Coral; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">Coral</h4>
  </figure>
  <figure style="background-color: DarkOrange; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">DarkOrange</h4>
  </figure>
  <figure style="background-color: Chocolate; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">Chocolate</h4>
  </figure>
  <figure style="background-color: Khaki; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">Khaki</h4>
  </figure>
  <figure style="background-color: NavajoWhite; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">NavajoWhite</h4>
  </figure>
  <figure style="background-color: SlateBlue; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">SlateBlue</h4>
  </figure>
  <figure style="background-color: BlueViolet; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">BlueViolet</h4>
  </figure>
  <figure style="background-color: Indigo; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">Indigo</h4>
  </figure>
  <figure style="background-color: SpringGreen; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">SpringGreen</h4>
  </figure>
  <figure style="background-color: DarkGreen; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">DarkGreen</h4>
  </figure>
  <figure style="background-color: LightSeaGreen; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">LightSeaGreen</h4>
  </figure>
  <figure style="background-color: CadetBlue; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">CadetBlue</h4>
  </figure>
  <figure style="background-color: CornflowerBlue; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">CornflowerBlue</h4>
  </figure>
  <figure style="background-color: DodgerBlue; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">DodgerBlue</h4>
  </figure>
  <figure style="background-color: Brown; padding: 1rem; margin: 0;">
    <h4 style="color: white; width: 100%; margin: 0.5em 0 .5em;">Brown</h4>
  </figure>
</section>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Travel Hacking in the Gig Economy]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>For the <em>Baffler</em>, I recently wrote about the internet subculture surrounding airline miles, and my own interest in the various scams invented to collect travel points. From a distance, travel hacking sure seems glamorous, but up close, the hobby looks an awful lot like a useless part-time job.</p>
<blockquote>
<p>Travel-hacking is</p></blockquote>]]></description><link>https://jamesfacts.com/travel-hacking-in-the-gig-economy/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb9f</guid><category><![CDATA[Internet culture]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Wed, 29 Nov 2017 21:29:19 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>For the <em>Baffler</em>, I recently wrote about the internet subculture surrounding airline miles, and my own interest in the various scams invented to collect travel points. From a distance, travel hacking sure seems glamorous, but up close, the hobby looks an awful lot like a useless part-time job.</p>
<blockquote>
<p>Travel-hacking is accessible mostly to the right kind of people&#x2014;the people who would likely be doing pretty well under capitalism already.</p>
</blockquote>
<p><a href="https://thebaffler.com/latest/the-longest-miles-white" class="btn-wrap"><button class="outline">Read the Whole Story <span class="arrow">&#x219D;</span></button></a></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Configuring Ghost and Let's Encrypt SSL with a www redirect]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p><em>note</em>: this is a pretty old post, and both Ghost and Acme.sh have changed quite a bit! This guide is now probably the best resource on configuring LE SSL with Ghost in the year of our lord 2020: <a href="https://ghost.org/docs/api/v3/ghost-cli/knowledgebase/#ssl-for-additional-domains">https://ghost.org/docs/api/v3/ghost-cli/knowledgebase/#ssl-for-additional-domains</a></p>
<p>This tutorial</p>]]></description><link>https://jamesfacts.com/configuring-ghost-and-ssl-with-a-www-redirect/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb8d</guid><category><![CDATA[development]]></category><category><![CDATA[letsencrypt]]></category><category><![CDATA[ssl]]></category><category><![CDATA[ghost]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Thu, 02 Nov 2017 22:30:29 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2017/11/ssl-lock.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2017/11/ssl-lock.jpg" alt="Configuring Ghost and Let&apos;s Encrypt SSL with a www redirect"><p><em>note</em>: this is a pretty old post, and both Ghost and Acme.sh have changed quite a bit! This guide is now probably the best resource on configuring LE SSL with Ghost in the year of our lord 2020: <a href="https://ghost.org/docs/api/v3/ghost-cli/knowledgebase/#ssl-for-additional-domains">https://ghost.org/docs/api/v3/ghost-cli/knowledgebase/#ssl-for-additional-domains</a></p>
<p>This tutorial assumes that you want to self-host your Ghost site under the root address of your domain, i.e. <a href="https://somedomain.com">https://somedomain.com</a>, and redirect all other domain permutations to this address. So, rather than resolve the non-SSL version of the root, the non-SSL wwww version or a <a href="https://www">https://www</a> version&#x2014;you want to 301 everything to a single canonical address. I wrote these instructions using Ghost 1.15.0, and Ghost CLI 1.2.1.</p>
<p>First, make sure that your domain is pointed to the correct nameservers, and that you have both an &apos;@&apos; and a &apos;www&apos; record for your domain. It might be helpful to run the <code>host</code> shell command to verify that your domain is networked correctly to your server.</p>
<p>Next, set up your ghost site at the primary address you plan to use. If you want your site to live at <code>https://lemontree.com</code>, run the Ghost CLI installer and tell it the new install belongs at <code>https://lemontree.com</code>. The Ghost CLI will volunteer to set up a Let&apos;s Encrypt SSL cert for you at this address, and it will save you a lot of time to accept this offer. It&apos;s also easiest to allow Ghost to configure nginx.</p>
<p>The CLI will install a shell script called <a href="https://github.com/neilpang/acme.sh">acme.sh</a> in order to ping the Let&apos;s Encrypt service to issue certificates for the exact install domain.</p>
<p>Now, you should have a functional site, live on the internet. Go ahead and <code>curl</code> the URL you gave the Ghost installed.</p>
<p><code>curl http://lemontree.com</code> <em>no https</em> will produce a <code>Moved Permanently. Redirecting...</code> message.<br>
<code>curl https://lemontree.com</code> will return the full body of your Ghost homepage.</p>
<p>So far, so good. On to the next two domains, <code>http://www.lemontree.com</code> and <code>https://www.lemontree.com</code>. This step requires a little more legwork. <code>curl http://www.lemontree.com</code> should give you the default nginx install page, and <code>curl https://www.lemontree.com</code> <em>with the https</em> will result in an SSL error. This is because the SSL cert issued by Let&apos;s Encrypt covers only the non-www domain. You need to issue a second cert specifically for <code>https://www.lemontree.com</code>, even though we are planning to redirect this traffic. A browser pointed to a https address will confirm SSL certificate details immediately, before processing a redirect request.</p>
<p>In order to issue a certificate for our www domain, we need to configure nginx to respond to the LE verification request&#x2014;this is what associates our certificate to our domain.</p>
<p>Navigate to the nginx config files Ghost setup for you. They live in <code>/var/www/[lemontree]/system/files</code>, where <code>[lemontree]</code> is the name of your install. Open up the non-ssl config file, <code>lemontree.com</code>. You should see an existing server block that redirects traffic from the non-https address to an SSL scheme. Add a new block below to accept requests from the LE verification request, and ensure this block redirects all other traffic to your primary SSL domain:</p>
<pre><code>server {
   listen 80;
   listen [::]:80;

   server_name www.lemontree.com lemontree.com;
   root /var/www/lemontree;

    location ~ /.well-known {
        allow all;
    }
    
    location / {
        return 301 https://lemontree.com$request_uri;
    }

    client_max_body_size 50m;
}
</code></pre>
<p><em>By default, <code>/var/www/lemontree.com</code> will be the root dir of your Ghost install, though this may vary.</em></p>
<p>It&apos;s a nice idea to run a test on your nginx config files each time you edit them, before restarting nginx. <code>sudo nginx -t</code> will confirm a valid config. Assuming there are no syntax errors, <code>sudo service nginx restart</code> will gracefully reload your nginx config.</p>
<p>Remember that script acme.sh? Navigate to the directory where that script lives: <code>/home/[user]/.acme.sh/</code>, where <code>[user]</code> is the user account that ran the Ghost install commands.</p>
<p>Now, issue a certificate through acme.sh for your <a href="https://www">https://www</a> address:</p>
<p><code>acme.sh --issue -d www.lemontree.com -w /var/www/lemontree.com</code></p>
<p><code>/var/www/lemontree.com</code> needs to match the dir you specified in the nginx conf.</p>
<p>After <code>acme.sh</code> runs, you should see a message with the location of your new certificate files. Take note of these locations! They will look something like this:</p>
<pre><code>Your cert is in  /home/serveruser/.acme.sh/www.lemontree/www.lemontree.cer
Your cert key is in  /home/serveruser/.acme.sh/www.lemontree/www.lemontree.key
The intermediate CA cert is in  /home/serveruser/.acme.sh/www.lemontree/ca.cer
And the full chain certs is there:  /home/serveruser/.acme.sh/www.lemontree/fullchain.cer
</code></pre>
<p>Next, we want to configure nginx to use these newly issued www cert files. I didn&apos;t have any luck using the acme.sh automated install command. Here&apos;s how I manually added the certificates. Head back over to the nginx config files Ghost created, in <code>/var/www/[lemontree]/system/files</code>. There will be an existing SSL config called lemontree.com-ssl.conf. Copy this to a new file, www.lemontree.com-ssl.conf. Edit the servername to match the www version of your domain. Comment out the line <code>root /var/www/lemontree/system/nginx-root;</code>&#x2014;we&apos;re not going to serve any content from this address. Under the <code>server_name</code> line, add a 301 redirect: <code>return 301 $scheme://lemontree.com$request_uri;</code>. Finally, there will be two lines that begin <code>ssl_certificate</code> and <code>ssl_certificate_key</code> respectively. Edit the paths after these keywords to match the path acme.sh returned. (Should be as simple as adding www ahead of your domain.)</p>
<p>Now we need to add symlinks from the nginx dir to the new www conf file in your Ghost dir. Head over to the dir <code>/etc/nginx/sites-available</code> and add a symlink to Ghost:</p>
<p><code>sudo ln -s /var/www/lemontree/system/files/www.lemontree.com-ssl.conf www.lemontree.com-ssl.conf</code></p>
<p>Finally, link sites-enabled to sites-available: <code>sudo ln -s ../sites-available/www.lemontree.com-ssl.conf www.lemontree.com-ssl.conf</code></p>
<p>Run one last <code>sudo nginx -t</code> test and restart nginx. Now, when you curl your www addresses, both the https and non-https variants should return a 301 redirect to the canonical, non-www https address. Congrats!</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Uber, And The Apps That Isolate In A Digital Economy]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I wrote a brief essay about why &#x2018;convenience&#x2019; apps like Uber are so successful: they shield us from interacting with behind-the-scenes workers, most often from very different economic, cultural and social strata than our elite consumers. <a href="https://medium.com/@jamesfacts/what-the-uber-backlash-means-in-a-digital-economy-b7fc02d3f46f">Read the story over on Medium &gt;&gt;</a></p>
<!--kg-card-end: markdown-->]]></description><link>https://jamesfacts.com/uber-and-the-apps-that-isolate-in-a-digital-economy/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb99</guid><category><![CDATA[App Economy]]></category><category><![CDATA[Medium]]></category><category><![CDATA[Internet culture]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Sat, 22 Nov 2014 14:45:10 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2017/11/weaving-loom.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2017/11/weaving-loom.png" alt="Uber, And The Apps That Isolate In A Digital Economy"><p>I wrote a brief essay about why &#x2018;convenience&#x2019; apps like Uber are so successful: they shield us from interacting with behind-the-scenes workers, most often from very different economic, cultural and social strata than our elite consumers. <a href="https://medium.com/@jamesfacts/what-the-uber-backlash-means-in-a-digital-economy-b7fc02d3f46f">Read the story over on Medium &gt;&gt;</a></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[So You Want to Hire a Freelance Developer]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>There&#x2019;s an acute lack of digital talent out in the wild. Every business &#x2014; worm farmers to art dealers &#x2014; knows it&#x2019;s critical to be online. That means building a web site, creating a marketing presence and being active in social media. If you&#x2019;re</p>]]></description><link>https://jamesfacts.com/five-things-hire-freelance-developer/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb98</guid><category><![CDATA[development]]></category><category><![CDATA[freelancing]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Thu, 02 Oct 2014 07:52:24 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2017/11/workstation.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2017/11/workstation.jpg" alt="So You Want to Hire a Freelance Developer"><p>There&#x2019;s an acute lack of digital talent out in the wild. Every business &#x2014; worm farmers to art dealers &#x2014; knows it&#x2019;s critical to be online. That means building a web site, creating a marketing presence and being active in social media. If you&#x2019;re an employer, implementing this digital infrastructure can be confusing territory. Odds are, you don&#x2019;t need a fulltime developer / web marketing expert / community manager on your team. But acquiring a freelancer means hiring and evaluating a skillset in which you may have little expertise. The internet landscape shifts so quickly, it&#x2019;s hard to track exactly what is effective NOW unless you live and breathe web.</p>
<p>From a freelancer&#x2019;s perspective, even one prepared with skills in strong demand, <strong>new clients are shrouded in uncertainty and frustration.</strong> Working productively means understanding a new brand and business goals in short order, while gently educating the client: what tools are necessary? what features are cruft? what content is useful?</p>
<p>For those freelancers fighting the good fight, Godspeed. For employers in need of freelance talent, a few suggestions:</p>
<h3 id="1comepreparedwithatightconciseconceptofwhatyoudliketobuild">1). Come prepared with a tight, concise concept of what you&#x2019;d like to build.</h3>
<p>At the onset of a web project, every client has an idea of what they want to end up with &#x2014; even if it&#x2019;s only in their head. There&#x2019;s nothing worse than vague directions from a client that send freelancers back to the drawing board when deliverables don&#x2019;t match poorly articulated expectations.</p>
<p>It&#x2019;s ok if you&#x2019;re not a designer or information architect. Sit down and map out exactly what you want as the final project. Sketch concepts on paper, make note of similar websites doing something you like, and make sure you consider what folks in your organization need from a backend. A minute spent on detailed specifications saves an hour of back and forth or wasted development effort.</p>
<h3 id="2bewarefeaturecreep">2). Beware feature creep.</h3>
<p>Closely tied to number one &#x2013; make sure you nail down ALL the required functionality before work starts. There are many good reasons to avoid feature creep. New features add to the scope, which will result in additional cost to the employer. It&#x2019;s generally harder to integrate new features once half the code is prepared. Most importantly: the new features are almost uniformly bad ideas.</p>
<p>There&#x2019;s a certain pressure that clients feel to squeeze everything in once work is underway. This can lead to an insistence on building spur-of-the-moment ideas. Tread cautiously. Generally, clients and developers remember the truly important features before getting started. See a project to completion and live with it for a bit before deciding to add on extra bells and whistles.</p>
<p>It really messes up freelancers to extend a project, because missed deadlines cascade over into work that&#x2019;s lined up in the future. Planning work schedules and accounting for uncertain income is hard enough when projects wrap up as expected</p>
<h3 id="3limitpartiesallowedtoprovidefeedback">3). Limit parties allowed to provide feedback.</h3>
<p>What&#x2019;s the best strategy to avoid feature creep? Limit feedback on prototypes to only the most essential staff. Additional eyeballs reviewing freelancer work make it harder to reach consensus. Even if you are lucky enough to have all reviewing parties on the same page, getting everyone to take a peek wastes time and keeps your freelancer idle. Identify who needs to sign off on work at the onset, and don&#x2019;t introduce commentary from the peanut gallery mid-process.</p>
<h3 id="4howtounderstanddelays">4). How to understand delays:</h3>
<p>Delays are justifiably frustrating to clients. When a freelancer promises a product in 30 days, and fails to keep his or her word, the likeliest explanation seems to be that the developer is a liar, or a dolt. Unfortunately, even the best managed projects often (if not always!) turn more complex than anticipated.</p>
<p><a href="http://qr.ae/ZToTm">Michael Wolfe offers a brilliant analogy</a> in to explain how this happens. When you consider a project in broad strokes, say, as one might look at a length of distance down the California coast along a map, there&#x2019;s a clear path from A to B. Once you&#x2019;re actually on the ground, it&#x2019;s likely there are many obstacles that weren&#x2019;t clear from the map. There&#x2019;s no method to accurately predict <em>every</em> complication in most digital initiatives, other than rolling up your sleeves and starting work.</p>
<p>Some freelancers like to take a guess at delays that may arise and quote high. I prefer an initial quote based on the best information at hand, and an employer who appreciates that there is always a possibility that information may change.</p>
<h3 id="5drawupacontract">5). Draw up a contract.</h3>
<p>When I first began freelancing, I didn&#x2019;t mind winging a project without a contract. <strong>You are correct:</strong> was that ever a bad idea. At face value, contracts are useful in resolving legal disputes, but I&#x2019;ve never been so unfortunate as to experience that first hand.</p>
<p>The real value in a contract is codifying everyone&#x2019;s expectations and laying out a framework for the project. Insist on a detailed contract outlining scope, milestones, communication expectations, and a process for missed deadlines or additional work requests. Clarity in terms of engagement go a long way toward working effectively once you&#x2019;re close to the finish line.</p>
<p>It&#x2019;s tempting to manage a freelancer on the fly &#x2014; but the most effective, and satisfying process is always building a tight, precise framework at the onset. <strong>When you and your freelancer share a clear vision, your project will turn out great.</strong></p>
<p>*PS: I&#x2019;m very grateful for <a href="http://sivers.org/how2hire">Derek Sivers&#x2019; excellent guide to hiring freelancers</a>. Four years old and every bit as useful today.&#xA0;*</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Valuing Technology Fairly – Case Study In Seamless And Grubhub]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Quartz news recently <a href="http://qz.com/182961/grubhub-and-seamless-take-a-13-5-cut-of-their-average-delivery-order/">posted an analysis of the commission GrubHub and Seamless charge</a> to restaurants that participate in their advertising network. Working backward from a claimed billion dollars in delivery orders, QZ calculated that GrubHub and Seamless collected $137 million in revenue through an average commission that approaches <strong>14 percent.</strong></p>]]></description><link>https://jamesfacts.com/valuing-technology-fairly/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb95</guid><category><![CDATA[App Economy]]></category><category><![CDATA[Internet culture]]></category><category><![CDATA[Seamless And Grubhub]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Tue, 29 Apr 2014 21:21:29 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2017/11/grub-hub.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2017/11/grub-hub.jpg" alt="Valuing Technology Fairly &#x2013; Case Study In Seamless And Grubhub"><p>Quartz news recently <a href="http://qz.com/182961/grubhub-and-seamless-take-a-13-5-cut-of-their-average-delivery-order/">posted an analysis of the commission GrubHub and Seamless charge</a> to restaurants that participate in their advertising network. Working backward from a claimed billion dollars in delivery orders, QZ calculated that GrubHub and Seamless collected $137 million in revenue through an average commission that approaches <strong>14 percent.</strong></p>
<p>This is completely coconuts, of course,&#xA0;and it got me thinking about how to fairly value technology. Technology can do wonderful things to improve our lives, but most often it seems improve the fortunes of those who are already well off. In this case, technological profit seems to come at the cost of those already marginalized.</p>
<p>Seamless and GrubHub frame their narrative as noble. They&#x2019;ve struck upon an incredible breakthrough to fix a system and improve lives. In return, they deserve a lavish commission and untold IPO riches.</p>
<p>In my view, this is closer to reality: GrubHub and Seamless happen to control a small amount technological knowledge, which gives them leverage over business owners who are unsophisticated in their knowledge of internet. They&#x2019;re unneeded middleman, and their compensation is out of line when compared against the value of their service.</p>
<p>The technical solution created by GrubHub and Seamless is hardly groundbreaking. It&#x2019;s a shopping cart for restaurants, <a href="http://www.law360.com/articles/462593/seamless-grubhub-keep-tie-up-on-track-with-eatery-deal">solidified by an anticompetitive merger</a>. In a world where restaurateurs were web savvy, individual eateries could easily implement these systems. The real power in GrubHub and Seamless is that they &#x2018;invented&#x2019; online restaurant ordering &#x2013; introducing the concept to diners, and locking them into their ecosystem before restaurants had a chance to respond.</p>
<p>Let&#x2019;s suppose that the core team of technologists here is no bigger than 50 &#x2013; 100 employees. Their company hauls in $137MM in revenue. The entire operation is closer to 700 employees, according to GrubHub&#x2019;s IPO prospectus, but it&#x2019;s fair to assume the vast majority of these employees clock in at GrubHub for customer service positions, or are part of their mammoth sales force. I doubt many of those employees hold significant stock options. This slim collection of technologists are skimming a very significant percentage of the money that helps support tens of thousands of restaurant workers.</p>
<p>Based on a <a href="http://rrgconsulting.com/ten_restaurant_financial_red_flags.htm">2010 study by the National Restaurant Association</a> (and Deloitte &amp; Touche LLP), average pre-tax profit margins in the &#xA0;trade range from 2-6 percent. It&#x2019;s an extremely tight business, staffed with mostly low wage workers: according to the Bureau of Labor Statistics, <a href="http://www.bls.gov/ooh/Food-Preparation-and-Serving/Food-and-beverage-serving-and-related-workers.htm">average pay in the &#xA0;industry is just $18,130</a>. &#xA0;It&#x2019;s hardly a leap to suggest spiraling &#x2018;web&#x2019; commissions will impact these wages. One might argue that GrubHub and Seamless are bringing in new business, but it&#x2019;s more likely they&#x2019;re supplanting what were once phone orders. The volume of take-out food orders is tied more closely to broader economic health than order-placing technology.</p>
<p>But wait &#x2014;** it gets worse**! Once a restaurant is doing business with Seamless, they&#x2019;re pressured to pay extra for better placement within the site. While consumers believe they&#x2019;re shown restaurants based on ratings, or other objective criteria, <strong>placement is actually determined by fee</strong>. Their commission can in fact range as high as 18 percent. On top of that, there are monthly &#x201C;partnership fees&#x201D; to remain active, in addition to the commissions. Plus, when an order is placed on Seamless or GrubHub, they keep all the cash upfront, and send <a href="http://www.gourmetmarketing.net/seamless-review-is-it-good-or-bad-for-restaurants/">what&#x2019;s left over after commissions at their leisure</a>.</p>
<p>What happens when all restaurants are paying ~20 percent of their revenue to continue the business they had before GrubHub and Seamless arrived on the scene? It&#x2019;s bad for consumers, much worse for restaurants, and another example of technology that benefits only a small cadre of insiders.</p>
<p>I can&#x2019;t say for certain what compensation is reasonable for a shopping cart software company, but I do recognize what it looks like when one takes advantage of it&#x2019;s clients. When I order takeout, I pick up the phone.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[There's No Parking In Brooklyn - A Visual Study]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I&#x2019;m fascinated by the diverse array of &#x201C;No Parking&#x201D; signs that sprout behind Brooklyn curb cuts. They&#x2019;re little slices of folk art hidden in plain sight.</p>
<p>Despite the universal need for &#x201C;NO PARKING&#x201D; signage, there&#x2019;s little consensus on their appearance.</p>]]></description><link>https://jamesfacts.com/no-parking-in-brooklyn-study/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb94</guid><category><![CDATA[Internet culture]]></category><category><![CDATA[visuals]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Sat, 04 Jan 2014 11:56:00 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2017/11/no-parking.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2017/11/no-parking.jpg" alt="There&apos;s No Parking In Brooklyn - A Visual Study"><p>I&#x2019;m fascinated by the diverse array of &#x201C;No Parking&#x201D; signs that sprout behind Brooklyn curb cuts. They&#x2019;re little slices of folk art hidden in plain sight.</p>
<p>Despite the universal need for &#x201C;NO PARKING&#x201D; signage, there&#x2019;s little consensus on their appearance. I see old-school Brooklyn, hipster Bushwick and graffiti kids in every driveway.</p>
<p>Signs can be threatening or matter of fact; professional or comically childish. A ubiquitous, yet hardly uniform feature of the urban landscape.</p>
<p>You can find the complete collection of <a href="http://jamesfacts.tumblr.com/post/71907027819/no-parking-in-brooklyn-a-visual-study">2013 Brooklyn No Parking photos over on my Tumblr</a>.</p>
<p><img src="/content/images/2017/11/no-parking-sign.jpg" alt="There&apos;s No Parking In Brooklyn - A Visual Study" loading="lazy"></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Why do Seamless ads appeal to Recluses and Trolls?]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p><img src="/content/images/2017/11/sub-reddit-food-instructions.jpg" alt="sub-reddit-food-instructions" loading="lazy"></p>
<p>If you&#x2019;ve been in the NYC subway recently, you&#x2019;ve probably seen a very thorough campaign run by Seamless.com. It seems to me this campaign strikes an odd tone that fundamentally misunderstands the value in their service.</p>
<p>I love Seamless&#xA0;&#x2013; when I order through</p>]]></description><link>https://jamesfacts.com/seamless-for-recluses/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb92</guid><category><![CDATA[Internet culture]]></category><category><![CDATA[subway ads]]></category><category><![CDATA[Seamless And Grubhub]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Tue, 05 Mar 2013 18:13:55 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p><img src="/content/images/2017/11/sub-reddit-food-instructions.jpg" alt="sub-reddit-food-instructions" loading="lazy"></p>
<p>If you&#x2019;ve been in the NYC subway recently, you&#x2019;ve probably seen a very thorough campaign run by Seamless.com. It seems to me this campaign strikes an odd tone that fundamentally misunderstands the value in their service.</p>
<p>I love Seamless&#xA0;&#x2013; when I order through their service, I see all my local takeout offers, without a drawer of crumpled, out of date menus. The problem with old-fashioned delivery ordering isn&#x2019;t the phone call &#x2013; it&#x2019;s the selection process. Yet this campaign overlooks Seamless.com&#x2019;s capabilities as a super-menu&#xA0;in favor of pitching the site as a service for recluses and trolls.</p>
<p><img src="/content/images/2017/11/meetup-seamless.jpg" alt="meetup-seamless" loading="lazy"></p>
<p>I&#x2019;m fairly certain Seamless&#x2019;s user base isn&#x2019;t built on sociopaths so terrified of human interaction that they&#x2019;ve formed a subreddit to instruct minimum-wage food workers. And I know I&#x2019;m not such a tech-worker caricature that I lack the ability to pick up a phone.</p>
<p><img src="/content/images/2017/11/i-havent-met-one-neighbor.jpg" alt="i-havent-met-one-neighbor" loading="lazy"></p>
<p><img src="/content/images/2017/11/carrier-pidgeon.jpg" alt="carrier-pidgeon" loading="lazy"></p>
<p>Now, <strong>who wants to join a meetup</strong> to protest getting their mailbox stuffed with menu flyers? (you could always just <a href="http://twitter.com/jamesfacts">follow @jamesfacts</a> on Twitter, too!)</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Things I Learned When I Started an Auto Shop]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>When I was a college sophomore in 2005, I opened a <a href="http://eastcoastswappers.com">specialty auto shop</a> with two friends. We had classes and regular jobs, but we thought a small shop could be an interesting side project to utilize a unique skillset we happened to possess. The three of us had experience</p>]]></description><link>https://jamesfacts.com/starting-an-auto-shop/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb91</guid><category><![CDATA[Cars]]></category><category><![CDATA[ECS]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Wed, 20 Feb 2013 23:55:29 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>When I was a college sophomore in 2005, I opened a <a href="http://eastcoastswappers.com">specialty auto shop</a> with two friends. We had classes and regular jobs, but we thought a small shop could be an interesting side project to utilize a unique skillset we happened to possess. The three of us had experience conducting a <em>very</em> specific&#xA0;modification&#xA0;&#x2013; retrofitting older Subaru models with newer, more powerful factory drivetrains. The shop soon grew faster than any of us would have guessed &#x2013; until running it had entirely consumed each of our lives.</p>
<p>There was no way I could have managed the level of commitment that the shop required. I moved on, but the shop did well and remains in business today.</p>
<p><img src="/content/images/2017/11/seth.jpg" alt="seth" loading="lazy"><br>
<em>An early shot inside the shop</em></p>
<p>My stint at <a href="http://www.eastcoastswappers.com/">East Coast Swappers</a> was brief, and richly educational. I learned about work / life balance, client relations, financial management, and running a business. Luckily enough,&#xA0;the process of starting ECS is what led me into a career path after I graduated with a journalism degree. Promoting the business through web forums and search traffic, while teaching myself HTML and CSS was a tremendous lesson in the power of the internet for small business.</p>
<p>I learned a few other things along the way:</p>
<p><img src="/content/images/2017/11/motors-trans-subaru.jpg" alt="motors-trans-subaru" loading="lazy"><br>
<em>Subaru motors and transmissions awaiting installation</em></p>
<p>Others want to see you succeed. When we first opened our doors, I imagined our competition would badmouth us, and we&#x2019;d labor to overcome objections from new clients. As it turned out, our niche was growing, and competitors didn&#x2019;t feel threatened. Instead, they were excited to have company. Our customers were happy to refer our services &#x2013; to the point where we had a waiting list months long. And our neighbors; other small businesses in a Western&#xA0;Massachusetts&#xA0;industrial park, were among our greatest cheerleaders &#x2013; sharing equipment and&#xA0;wisdom, even when we were reluctant to ask.</p>
<p>Everything doesn&#x2019;t have to be perfect. I&#x2019;m an inveterate perfectionist. When we set up shop, I found pitfalls in garage space that was in fact an incredible find. When we needed a website, I delayed the project to tweak each pixel and character. Our customers didn&#x2019;t care what our business cards looked like or who delivered us rags and coveralls. What mattered was we offered a specialized service very much in demand &#x2013; and it was more important to move quickly and generate cashflow. This is one lesson I&#x2019;m still learning.</p>
<p><img src="/content/images/2017/11/first-four.jpg" alt="first-four" loading="lazy"><br>
<em>The very first four cars we built at ECS</em></p>
<p>Greater hours &#x2260;&#xA0;greater productivity. Working in a startup venture made it seem that long hours were a given, and I ended up regularly putting in 60 hour weeks. I was also going to school full-time in a town an hour away. It wasn&#x2019;t until after I&#x2019;d left that I realized those late nights made little contribution to the business. In fact, I made the most clear decisions after returning from long weekends and time off.</p>
<p>You can probably wait to make investments. My partners and I began ECS with essentially no budget at all. We didn&#x2019;t have significant savings or investors, so any purchases we made had to be funded from cash flow. Initially I thought we could never finish our customers&#x2019; projects without lifts, air tools, welders and other equipment professional mechanics typically have on hand. My partners were more optimistic, more resourceful, and taught me how to get a job done with only the basics. We grew the business organically, and avoided significant debt.</p>
<p><img src="/content/images/2017/11/early-days.jpg" alt="early-days" loading="lazy"><br>
<em>The early days of the shop - before we had anything beyond hand tools</em></p>
<p>Know when you got lucky. When I mention my experience building our auto business, people are often surprised by what I accomplished as a 19 year-old without prior experience. The truth is, I was in the right place at the right time. I met the right people while a certain automotive model was gaining popularity in the States. I was lucky &#x2013; and I&#x2019;m content with that.&#xA0;Business isn&#x2019;t always about a groundbreaking idea. Often, it&#x2019;s about making the most of an obvious opportunity.</p>
<p>Your friends aren&#x2019;t necessarily partners. This was a tough lesson to learn. I considered my partners among my best friends before we opened the shop, and by the time I left we were hardly on speaking terms. Business partnerships require just the right mix of personalities &#x2013; and it&#x2019;s not the same mixture required of friends.</p>
<p>Savor your victories. At ECS, I was focused on building the future of the business: new clients, new services, better facilities. It was difficult to enjoy what we had already accomplished. During our first open house, an event that drew more than 130 cars to visit, I ended up in the hospital after crushing one of my fingers while trying to finish a last-minute project. This lesson was quite literally painful to learn &#x2013; perhaps that&#x2019;s why I think of it so often.</p>
<p><img src="/content/images/2017/11/open-house.jpg" alt="open-house" loading="lazy"><br>
<em>A sampling of the turnout at an early ECS open house</em></p>
<p>Working in the shop taught me more than&#xA0;anything&#xA0;I studied in school. While I&#x2019;ve long since left the automotive industry, I never lost my sense of&#xA0;entrepreneurialism. Starting, and running something of your own is terrifying, exhausting and absolutely thrilling. There&#x2019;s really no substitute for experiencing it firsthand.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Streaming Music Makes Me Sad]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Tunes, it seems, have finally become entrenched in the cloud. Why download and store your music locally when subscription services (either very inexpensive, or based on incessant, <a href="http://marketisdale.net/2011/12/17/spotify-annoying/">obnoxious social media posts</a>) ? I&#x2019;ll admit it: Pandora is awesome. Rhapsody has a huge library and catchy TV advertising. Spotify has</p>]]></description><link>https://jamesfacts.com/streaming-music-makes-me-sad/</link><guid isPermaLink="false">6115bd2e16399a1bbcefdb8f</guid><category><![CDATA[Internet culture]]></category><dc:creator><![CDATA[James White]]></dc:creator><pubDate>Thu, 12 Apr 2012 21:05:18 GMT</pubDate><media:content url="https://jamesfacts.com/content/images/2017/11/spinning-record.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://jamesfacts.com/content/images/2017/11/spinning-record.jpg" alt="Streaming Music Makes Me Sad"><p>Tunes, it seems, have finally become entrenched in the cloud. Why download and store your music locally when subscription services (either very inexpensive, or based on incessant, <a href="http://marketisdale.net/2011/12/17/spotify-annoying/">obnoxious social media posts</a>) ? I&#x2019;ll admit it: Pandora is awesome. Rhapsody has a huge library and catchy TV advertising. Spotify has the ability to annoy me. YouTube seems to have just about everything.</p>
<p>I know the inextricable march of technology pauses for no man. I understand streaming is a superior music delivery, but it mostly makes me sad.</p>
<p>I started collecting MP3s in middle school. I&#x2019;m pretty sure my first was the classic <a href="http://www.inquisitr.com/wp-content/2012/01/play-nickelback-backwards.jpg">Nickelback</a> jam <em>Leader of Men</em>, but over time, my taste improved. Fourteen years later, I have a tad under 12,000 MP3s in my iTunes collection. I doubt it&#x2019;s the world&#x2019;s largest collection of MP3s, but I picked out each one myself. I think most of them are even legal, ripped from CDs, vinyl, or&#xA0;curated&#xA0;from sampler playlists. &#xA0;I feel like High Fidelity&#x2019;s Rob Fleming &#x2013; obsessing over the perfect playlist, constantly re-arranging my file structure, fixing MP3 tags, tracking down album covers and finding the right balance between mainstream and esoteric artists. It&#x2019;s an unconscious expression of my deepest, fullest self.</p>
<p>Is this the equivalent of my generation&#x2019;s vinyl? Will the kids of someday start downloading files on the sly, saving them to a nostalgic external hard drive? Will physical storage become a status symbol? Street cred acknowledged among cool kids at exclusive parties? I can&#x2019;t help but feel that&#x2019;s an outside&#xA0;possibility&#xA0;at best. And that&#x2019;s why, even knowing that streaming music is easier than editing my collection to fit a 32gb iPhone, I still want <em>my MP3s</em>, and <em>my music</em>.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>