a new year, a new blog
Well it's been a little over two years since I last completely reconfigured this blog, and now that it actually (mostly) works I felt like it would be fun to take a little tour of the more interesting features of this blog. The current iteration of the blog is written in Ember.js, and the entire blog application lives on your browser, hosted via Amazon's CDN. This post explains how I built a pseudo-static web site with Ember and Ember Data.
why rewrite your blog again?
I had a perfectly good Rails app running what kinda felt like Jekyll inside the auspices of ActionView. I was also able to use the asset pipeline and other Rails utilities to make it a great experience in writing code. So why should I change from that?
First of all, a Rails app requires a Ruby application server, which needs Ruby and probably a bunch of other shit. So I'll need to set up on something like Heroku, purely because I didn't feel like paying for an instance on DigitalOcean or AWS. I gradually got tired of waiting around for my app to respond, and sought out other alternatives.
ember saves the day
I've been following Ember.js since they started work on it, and right after they had renamed it from "SproutCore 2.0". Since people like Yehuda Katz and Tom Dale were running the show, I was confident Ember was going to at least be interesting. So far, I've been blown away by its adoption of ever-changing and new standards while simultaneously maintaining backwards compatibility with older versions, giving long deprecation warnings and "feature flags" which allow you to enable or disable new framework features as you incorporate them into your app. Ember seems committed to making sense out of what sometimes feels like chaos in web development, and writing in Ember 2.0 style doesn't feel much different than Ember 1.0 style. Even Ember Data has matured over time with a solid API that's both consistent and easy to reason about.
The only problem with Ember is that in my line of work, I don't get a lot of chances to play with up-and-coming JavaScript frameworks. Most Rails shops I've worked for are firmly implanted in the "we serve HTML from the server" tradition and not interested in long-living apps that are entirely served from the client side. So, realistically...the reason I wanted to rebuild my blog was to learn more about how Ember.js works and whether or not it's the framework of my dreams.
what were the requirements?
I chose to lay out my hard requirements here in case anyone is reading this thinking they can use Ember.js for anything. I'm sure you will find use cases that Ember is not meant for, but for my use case, Ember was a fine choice. Here's what I needed out of a rewrite of my blog:
- Since I already had some links to my articles floating around the
interwebs, I needed to preserve the URLs to all of my posts. To do this,
I needed to remain following the
/:yyyy/:mm/:dd/:title
format of my previous app. - As mentioned before, hosting must be cheap and easy to deploy to using a Travis CI continuous deployment lifecycle. If at all possible, avoid purchasing a server.
- Articles must be easy to create, so I don't have to copy files around. Typing a one-line command in the shell to generate the file structure for an article is preferable.
Ember fares pretty well against these requirements. Here's how I used Ember, Ember CLI and Amazon S3/CloudFront to solve the above requirements:
- Preserve the URLs: Ember.js has a powerful routing system that make practically any configuration of URLs possible. Given the framework's encouragement to make everything in your app routable, I felt like this wasn't going to be a problem.
- Hosting must be cheap: Since basically all of my code (minus the content compiler) lives on the client-side, I don't need to pay for a server or VM instance anywhere. Instead, I host the entire app from Amazon S3 and Amazon CloudFront. There's a script in the codebase to invalidate the CDN cache every time a deployment succeeds, so that the files on each browser are re-fetched as quickly as possible. I pay less than $3/month to host this entire blog that has hundreds of page views a day.
- Articles must be easy to create: Ember CLI has an amazing blueprints feature, one which I found powerful and expressive. You can generate blueprints, which power the generators in Ember CLI themselves. I generated an article blueprint and formatted the file path so that it includes today's date automatically. This preserves the file name convention, file format, and basic contents of each article (and page).
the meat and potatoes
The first thing I had to do in order to get this blog up and running was figure out a way to replicate the "YAML Front Matter" and Markdown processing that I was used to in my last blog. All of the articles follow this format, and as long as I can compile them down to HTML I can probably render them to a web page.
In the current incarnation, all of the Markdown processing is done on the client-side, with the "server" (actually just static JSON files) rendering out the raw data as it came down from the YAML Front Matter compiler. I used front-matter, a great little npm package that was simple and gave me exactly what I wanted.
organizing my javascript
With the new ECMAScript 2015 features, JavaScript has become much easier to use and organize. To maintain a simple "shell" interface to the whole thing, I used a function that takes a directory argument and instantiates the objects that are required to compile the files in that directory:
import path from 'path'
import fs from 'fs'
import Template from './template'
import Index from './index'
export default function compile(directory) {
let root = process.cwd(),
source = path.join(root, 'app', directory),
destination = path.join(root, 'public', directory),
index = new Index(destination, directory)
fs.mkdir(destination, function (error) {
// swallow errors
})
fs.readdir(source, function (dirReadError, files) {
if (dirReadError) {
throw dirReadError
}
files.forEach(function (filename) {
fs.readFile(
path.join(source, filename),
{ encoding: 'utf-8' },
function (fileReadError, contents) {
if (fileReadError) {
throw fileReadError
}
let template = new Template(
filename,
contents,
destination,
directory,
)
if (template.publishable) {
template.compile()
index.push(template)
} else {
console.warn('Template', template.id, 'will not be published.')
}
},
)
})
})
}
This is a lot of code, but trust that it all makes sense to live inside
this function. Most of the code here deals with iterating over the
directory of files given, reading their contents, and instantiating
workers that compile them into JSON. The Template
class here does
most of the work in that department, taking a filename, the entire
contents of the file, as well as its destination name and directory. It
will only compile a new article or page if the publishable
key has not
been explicitly set to true
(the key defaults to true, so if not set,
the article will be published). By running compile()
on the newly
instantiated Template, we are writing a new JSON file to disk.
There is an additional object generated here, the Index
. An Index is
basically a collection of Template
objects that have been compiled and
should show up in the /articles.json
or /pages.json
file. By
default, Ember.js will look in this file for the article to display on a
category or index page. Index is pretty simple, acting like an Array
by implementing the push()
method, we can add entire Template objects
to the Index and let the Index deal with what attributes it picks out of
there. By calling push()
, we are also implicitly writing to the JSON
file on disk that the index represents, as described above.
don't fight the framework
While writing this code, I made sure I was doing things "The Ember Way": using the same tools and components that Ember.js developers use. The app was built using Ember CLI and I used generators whenever possible. I've also been keeping up to date on the latest news regarding the changes in Ember.js and the road to Ember 2.0 so that when Ember 2.0 is finally released, it won't be too difficult to upgrade. Ember.js is known for being easy to upgrade due to its support of feature flags and reasonable deprecation policies.
That said, there were a few modifications I needed to make to Ember's
provided components, such as DS.JSONAPIAdapter
, in order to get it to
read the flat JSON file. The only way I could get S3 (or my local
server) to serve the JSON file as the application/json
Content-Type
was to actually give it a .json extension. So I needed to make the
following change to my ApplicationAdapter
in order to get Ember-Data
to read its "API" properly:
import DS from 'ember-data'
export default DS.JSONAPIAdapter.extend({
/**
* Extend the JSON API adapter to use the +.json+ file extension so
* that the static JSON files are found.
*
* @param string modelName
* @param string id
*/
_buildURL: function (modelName, id) {
return this._super(modelName, id) + '.json'
},
})
The models were made as conventionally as possible. For example, here is
the definition of Page
:
import DS from 'ember-data'
export default DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string'),
})
Page is rather simple, only having static data and not needing to be
sorted in any special way. Compare that with the source for the
Article
model:
import DS from 'ember-data'
export default DS.Model.extend({
title: DS.attr('string'),
date: DS.attr('date'),
category: DS.attr('string'),
tags: DS.attr('string'),
body: DS.attr('string'),
year: function () {
return this.get('_splitID')[0]
}.property('_splitID'),
month: function () {
return this.get('_splitID')[1]
}.property('_splitID'),
day: function () {
return this.get('_splitID')[2]
}.property('_splitID'),
href: function () {
return this.get('id')
.split(this.get('year') + '-')
.join('')
.split(this.get('month') + '-')
.join('')
.split(this.get('day') + '-')
.join('')
}.property('id,year,month,day'),
preview: function () {
return this.get('body').split('\n\n')[0] + this.get('footnotes')
}.property('body'),
footnotes: function () {
return (
'\n\n' +
this.get('body')
.split('\n')
.map(function (line) {
if (line.match(/\[(\w+)\]\:\shttp/)) {
return line
}
})
.join('\n')
)
}.property('body'),
/**
* @private
*/
_splitID: function () {
return this.get('id').split('-')
}.property('id'),
})
As you can see, Article is a bit more complicated. I needed to be able to pick out the year, day, month and "id" (a parameterized version of the title) from the filename of the article, as well as the preview and "footnotes" for each article. This is so I could construct and deconstruct articles like /2000/01/01/happy-new-year into a date and ID for a specific Article record.
the front-end
I'm still using Zurb Foundation as my main CSS framework. It's easy to make apps that look nice and act responsively when browser sizes change over time. My web applications have never looked better than when I began using Foundation. It's quite configurable and great for Rails apps, which is how I got started on it, but it's also quite easy to add to an Ember.js app if you run the following command:
$ ember install ember-cli-foundation-sass
Ember CLI's large collection of addons is another benefit to the Ember.js ecosystem. You can find an addon for pretty much anything you wish to incorporate into your app. For example, here are the Ember CLI addon dependencies that are configured for psychedeli.ca:
{
"ember-cli": "0.2.7",
"ember-cli-app-version": "0.3.3",
"ember-cli-babel": "^5.0.0",
"ember-cli-content-security-policy": "0.4.0",
"ember-cli-dependency-checker": "^1.0.0",
"ember-cli-foundation-sass": "1.1.1",
"ember-cli-htmlbars": "0.7.6",
"ember-cli-ic-ajax": "0.1.1",
"ember-cli-inject-live-reload": "^1.3.0",
"ember-cli-qunit": "0.3.13",
"ember-cli-sass": "3.1.0",
"ember-cli-uglify": "^1.0.1"
}
Granted, a lot of these addons came directly from the base installation of Ember CLI, but I added ember-cli-foundation-sass and ember-cli-sass to the mix in order to support our frontend.
make
haste
In my quest to further automate the deployment of my app, I turned to one program that I knew would be available on any machine the app was installed to: Make. The Makefile is a great addition to any JavaScript developer's toolbelt. When you need to combine a bunch of commands together, why use a crazy shellscript when you can take advantage of an existing powerful build system?
Here's the Makefile I use to test and deploy the app:
EMBER_ENV ?= development
SRC = $(wildcard src/*.js)
LIB = $(SRC:src/%.js=lib/%.js)
all: clean lib dist
.PHONY: clean all test deps
clean:
@rm -rf $(LIB) tmp dist doc public/articles/*.json public/pages/*.json public/pages.json public/articles.json
node_modules:
npm install
bower_components:
bower install
bundle: node_modules bower_components
lib: bundle $(LIB)
lib/%.js: src/%.js
@mkdir -p $(@D)
babel $< -o $@
dist: bundle lib
ember build --environment=$(EMBER_ENV)
test: bundle lib
ember test
watch: bundle clean lib
ember server
doc:
esdoc -c config/docs.json
Travis automatically runs make test
as its "script" command and make
on "after_success". Due to the way we cache NPM packages and Bower
components on Travis, I still use the regular npm install
command to
install JavaScript dependencies. The Makefile doesn't deploy the actual
code, I keep my AWS keys encrypted via Travis so that the only way it
can be deployed is through the CI system. Travis provides a means of
safely deploying to Amazon S3, as well as encryption of secure
variables, so it's a fine choice as a "client" for deployment to S3.
a truly continuous workflow
After setting all this up, I can now freely write articles and not have
to worry about whether I can deploy the new changes at the time of
writing. Since everything is automated, the only work required to
publish an article to the web is a git push
to GitHub. There are still
a few things I'd like to add on some kind of after_deploy
callback on
Travis that would aid in CDN cache invalidation as well as promotion of
new article links to news aggregators and Twitter.
You can view the entire source code to this blog on GitHub.