Don't Forget to Plant It!

My Scoutmob Setup

When I got my Scoutmob laptop (a quad-core 15’ MBP) a couple of months ago, I wrote down everything I needed for making it ready for development. Here’s my minimum viable development setup.

Chrome (Dev Channel). At one point I was dabbling with writing Chrome Extensions, but there’s not really a good reason to continue using the dev channel - ‘cept that it’s been pretty stable for me.

iTerm2. I was a tmux user, and screen before that. Based on a recommendation from Amro, I’ve been using iTerm2 for a few weeks now. Not totally sold on it, but it’s a narrow win over tmux because of its ease of set up. I recommend downloading the Solarized theme for it.

Divvy. Great app for organizing windows. Use it all the time.

Dropbox. Easy filesharing. More importantly, I need it for my next app, which is…

1Password. I use this a lot already, but I need to use this more. You’re only as safe as your weakest password.

Homebrew. Best package manager for OSX. No reason to use anything else.

XCode 4. Requirement for Homebrew, but starting some serious iOS development as well.

TextMate. I vi occasionally, but TextMate is my editor of choice, especially for Ruby/Rails development. I recommend the Solarized theme for it as well.

Twitter. I don’t tweet enough to need anything more.

Notational Velocity. Synchronized to Simplenote, I’ve built up enough notes over time to where this is tool is now an essential part of how I work. I’ve tried Evernote, but it wants to be more than I really need.

iStat Menu. I tried to go without installing this initially - until my machine started getting sluggish. Essential for finding misbehaving processes.

JumpCut. Clipboard history. Nice not having to worry about overwriting your current clipboard contents.

Git (via Homebrew). I would use this even if I didn’t need to push to a remote repository. It’s the safety net for software developers.

RVM. Nice that it makes upgrading Ruby a breeze, but Gemsets is the killer feature.

MySQL (via Homebrew). It’s what we use at Scoutmob.

POW. Always available Rails apps. Install the powder gem to make managing applications a snap.

Ack (via Homebrew). Super fast searches on the command line. Also, I recommend install the Ackmate plugin for TextMate.

My dotfiles and dotvim Projects. Gets my terminal environment how I like it quickly.

In addition, there are two settings I do every time I setup a new machine:

  • Turn off Dashboard. Never use it. and it hijacks a very useful function key. Speaking of function keys…
  • Use F1, F2, … Keys. Instead of the feature keys. Developers pretty much live in those function keys.

Building an HTML5 Application, Part 3: Let’s Take This Offline

Here’s Part 1 and Part 2 of this series.

So after taking a break to spend time with family (yay!) and doing taxes (bleh!), I was able to spend some time on Thymer again. It’s time to get it working offline.

Since it doesn’t require a server, Thymer is a perfect candidate to be an offline application. In HTML5 you do this by telling the browser what files it should store locally so they are available offline. You do this in a file called the Cache Manifest file. The one for Thymer looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
CACHE MANIFEST
# Revision: 10
alarm.mp3
alarm.ogg
alarm.wav
clock_32x32.png
index.html
jquery-1.5.2.min.js
minus_12x3.png
reload_12x14.png
thymer.css
thymer.js

In this file, I list all the files I’m going to need when offline. It’s important to note that files listed here must follow the same origin policy, meaning the files must be served from the same domain. I ran into this since I was using a Google hosted version of jQuery, and had to switch to self-hosting instead.

Another important thing to note is the second line of the manifest, where I specify the revision of the manifest in a comment. Browsers will only update their offline cache when the manifest changes, and not when the files listed in the manifest change. Common practice then is to update the revision number in the manifest to notify browsers to update their cache.

I ended up creating a rake task to generate the cache manifest, updating the file list when new files are added and incrementing the revision number on any changes. To update the manifest, I just run the manifest task:

1
rake thymer.appcache

Once the manifest file is created, I need to reference that manifest from the page. You do this by setting the manifest attribute on the html element:

1
2
<!doctype html>
<html manifest="thymer.appcache">

Handling Updates in the Browser

Some browsers must be explicitly told to update the application cache when it sees an update. Here’s how that is done:

1
2
3
4
5
6
7
8
9
10
11
12
13
// check for updates
if ('applicationCache' in window) {
$('#update-button').click(function() {
// Ensure the browser uses the latest version of the code
window.applicationCache.swapCache();
// Reload the application
window.location.reload();
});
window.applicationCache.addEventListener('updateready', function() {
$("#update").show();
}, false);
}

In the above code, I just show a short message with a link to swapCache() and reload when a new update is available. Note that in some browsers, the update doesn’t happen until swapCache() is called, while others it’s automatic and all you really have to do is reload.

In JavaScript, you can detect your current offline/online status with navigator.onLine. You can also listen to offline and online events in the document body. For Thymer, I decided to check the manifest for updates when the browser comes back online:

1
2
3
4
// Check for updates when the browser comes back online
$(document.body).bind('online', function(){
window.applicationCache.update();
});

In testing, it looks like listening for online event doesn’t yet work in Chrome or Safari.

Other Notes

  • When serving the manifest file, make sure it is served with a Content-Type header of ‘text/cache-manifest’.

  • A lot of references and tutorials I found often named the manifest file cache.manifest, but it looks like .appcache will ultimately be the standard extension. This make sense, since the latter will probably have a lesser chance of name collisions with other file types.

  • I ended up declaring all versions of my audio file in the manifest, which is less that optimal since browsers would only use one of the three sources. I could dynamically generate the manifest based on User Agent, but that’s less than ideal.

  • You can browse the contents of the offline cache in FF by going to about:cache from the address bar. In Chrome, you can see it for the current page under the Resources tab from within the Web Inspector. Didn’t see where this information was available in Safari.

That’s a Wrap

That’s it for Offline mode. In the next and most likely the final post in this series, I’m going to get Thymer onto the Chrome web store. If you want to learn more about Offline mode, HTML5Rocks has curated a bunch of articles related to offline. And here is the source code referenced by this post.

Building an HTML5 Application, Part 2: Web Notifications & <Audio>

This is part 2 in a series of posts walking through my experiences building Thymer, a single-page web application using HTML5 and related technologies. In my first post, I got the basics of the application working and used the Local Storage API to store timers between browser refreshes. In this article, I’m going to talk about the Notification API and the <audio> tag.

The Notification API

Since my last post, I’ve made some minor fixes and UI tweaks to get Thymer feeling more like a usable application, but Thymer really wasn’t going to be useful unless it can grab a user’s attention when a timer ends. Recently Gmail introduced a new feature in Chrome where I get a nice little Growl-like notification whenever I received a Google Talk message. This was done using something called the Notification API. I wanted to see how easy it would be use this API to show a notification when a timer completes.

The Notification API is currently just a draft spec, and it’s support is currently only limited to Chrome. Once the API gets wider adoption, there’s a very good chance the API will change (at the minimum to rename the window.webkitNotifications object). Getting it working was super easy, basically by following this tutorial here.

First, I request permission to use the Notifications API when the first timer is created. It’s important to note that you can only request permission on a user triggered event like a mouse click or key event. In Thymer’s case, I request permission when the Enter key is pressed to submit a new timer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$('#add-timer-form input').keypress(function(event) {
if (event.which == '13') { // Enter key
event.preventDefault();
var seconds = parseInt($('#secs').val()) +
(parseInt($('#mins').val()) * 60) +
(parseInt($('#hours').val()) * 60 * 60);
Thymer.addTimer(new Timer($('#timer-name').val(), seconds));
$('#timer-name').val('');
// request notifications permission if API is supported. We do
// this here because we can only do this on user triggered events.
if (window.webkitNotifications &&
window.webkitNotifications.checkPermission() != 0) {
window.webkitNotifications.requestPermission();
}
}
})

Then I create a notification when the timer ends:

1
2
3
4
5
6
7
8
9
_alarm:function() {
// show a notification if the browser supports it.
if (window.webkitNotifications &&
window.webkitNotifications.checkPermission() == 0) {
window.webkitNotifications.createNotification('clock_32x32.png',
'Thymer', '"' + this.name + '" timer has completed').show();
}
}

Here’s what the notification looks like:

Thymer Notification in Chrome

Some notes about the Notifications API:

  1. I had issues with requestPermission() when using the file:// protocol where my permission got set as if I denied permission and didn’t give me a way to reset it. If you’re testing locally I recommend serving your page via localhost.
  2. Once you approve or deny the notification permission, users will not receive the permission infobar again when you call requestPermission(). You can reset the permission in Chrome from Preferences > Under the Hood > Content Settings (Under Privacy).
  3. The image parameter in createNotification() is not optional. Passing null will show a broken image.

The Audio Tag

So now I have a way to grab user’s attention when a timer completes in Chrome, but what about other browsers? How about playing an alarm sound when the timer completes? This turned out to be pretty easy with the <audio> tag:

1
2
3
4
5
<audio id=alarm-sound>
<source src=alarm.mp3 type=audio/mpeg />
<source src=alarm.ogg type=audio/ogg />
<source src=alarm.wav type=audio/wav />
</audio>

This sets up a audio clip that I can access from JavaScript via the element id. Since I don’t specify a controls or autoplay attribute, the audio won’t play until I want it to. I’m also specifying the three different sources for audio (gotta love proprietary sound formats). Between MP3 & Ogg Vorbis, all modern browsers should be covered, but I’m going to throw a WAV in there for completeness anyway. I am concerned about how this will affect my footprint when I setup offline mode for Thymer, but that’s an issue for a different post.

Playing the audio from JavaScript is cake:

1
2
3
4
5
6
7
8
9
_alarm:function() {
// show a notification if the browser supports it.
if (window.webkitNotifications && window.webkitNotifications.checkPermission() == 0) {
window.webkitNotifications.createNotification('clock_32x32.png', 'Thymer', '"' + this.name + '" timer has completed').show();
}
// play the alarm sound
document.getElementById('alarm-sound').play();
}

For more information about the Audio tag, I recommend this article at <html>5doctor. In that article they have a helpful table showing the browser support for the different audio formats.

This concludes Part 2 of this series. You can see the final code for this part here. You can also see the final application on the master branch as well as play with a running version of Thymer here. In part 3, I’m going to configure Thymer to run in offline mode by creating a cache manifest.

Building an HTML5 Application, Part 1: Local Storage

So far, I’ve dabble with pieces of HTML5, having build the rite-of-passage Todo application - now it’s time to jump in and build a more complete application. In a series of posts, I’m going to build an HTML5 application from scratch and document my experiences along the way. Let’s start off by exploring HTML5’s Web Storage feature.

The application I’ve decided to build is a timer application - basically, you enter a time interval (in hours, minutes, seconds), and it’ll notify you when that time has been reached. I start things off by creating the basics of what the application would look like via HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!doctype html>
<html>
<head>
<title>Thymer</title>
</head>
<body>
<header>
<h1>Thymer</h1>
</header>
<div id=content>
<div id=add-timer-form>
<input class=text name=hours value=00 size=2> :
<input class=text name=mins value=05 size=2> :
<input class=text name=secs value=00 size=2>
<input name=name placeholder='Timer Name'>
<input type=submit value='Create Timer'>
</div>
<ul id=timer-list></ul>
</div>
<script src=thymer.js></script>
</body>
</html>

Couple of notes about the above snippet:

  1. The doctype and lack of attribute quoting are standard to the spec. One of the key points of HTML5 was identify the lowest common denominator between the popular browsers and based the spec around them. In this case, the doctype of html was the minimum needed to be a valid doctype and attribute values do not need to be quoted unless there are whitespace in the value.

  2. Line #21: I’m using the new placeholder attribute which puts a placeholder text in the text input when the input is empty and doesn’t have value. Once you put focus on that text input and enter a value, the place holder text goes away.

Next comes the thymer.js script. Nothing HTML5 specific here, I’m just putting the code here for completeness.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
$().ready(function(){
var timerList = $('#timer-list');
var timers = [];
var UpdateLoop = {
start:function() {
if (!this.started) {
this.started = true;
this.interval = setInterval(function(){
UpdateLoop.update();
},1000);
}
},
update:function() {
if (timers.length > 0) {
for (var i in timers) {
timers[i].update();
}
} else {
clearInterval(this.interval);
this.interval = null;
this.started = false;
}
}
};
var Timer = function(name, seconds) {
this.name = name;
this.seconds = seconds;
this.started = new Date().getTime();
this.finished = false;
this.start();
};
Timer.prototype = {
start:function() {
this.el = $("<li>" + this.buildDisplayString() + "</li>");
timerList.append(this.el);
},
update:function() {
if (!this.finished) {
if (this.check()) {
var timer = this;
var removeLink = $('<a href="#">remove</a>').click(function(){
timer.remove();
});
this.el.text('ALARM!!! - ' + this.name + ' ');
this.el.append(removeLink);
} else {
this.el.text(this.buildDisplayString());
}
}
},
remove:function() {
this.el.remove();
for (var i in timers) {
if (timers[i] == this) {
timers.splice(i,1);
break;
}
}
},
check:function() {
var remaining = this.calculateRemaining();
if (remaining <= 0) {
this.finished = true;
return true;
}
return false;
},
calculateRemaining:function() {
return this.seconds - Math.floor((new Date().getTime() - this.started) / 1000);
},
buildDisplayString:function() {
var s = [];
var remaining = this.calculateRemaining();
remaining = this.buildTimeSegment('h', 60*60, remaining, s);
remaining = this.buildTimeSegment('m', 60, remaining, s);
s.push(remaining + 's');
return s.join(' ') + ' - ' + this.name;
},
buildTimeSegment:function(suffix, divisor, secondsRemaining, segments) {
var units = Math.floor(secondsRemaining / divisor);
if (units > 0) {
segments.push(units + suffix);
return secondsRemaining % divisor;
}
return secondsRemaining;
}
};
$('#add-timer-form form').submit(function(){
var seconds = parseInt($('#secs').val()) +
(parseInt($('#mins').val()) * 60) +
(parseInt($('#hours').val()) * 60 * 60);
var timer = new Timer($('#timer-name').val(), seconds)
timers.push(timer);
UpdateLoop.start();
return false;
});
});

I now have a basic timer application. Now for the HTML5 goodness.

For any desktop to be truly useful (desktop or web), it needs to be able to remember state from the last time you ran the application. For desktop applications, there are many different ways to do this, but web applications have always been limited to cookie or server-based storage. In HTML5, there are actually a few options for storing data, with the most commonly supported one being the Local Storage API. Local Storage API is a simple API that allows you to store data as key/value pairs. For my Thymer application, I’m going to store the timer array so any created timers will be persisted across browser reloads.

First, I update the Timer object so it can be converted to/from a save state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Timer = function(name, seconds, started, finished) {
this.name = name;
this.seconds = seconds;
this.started = started ? started : new Date().getTime();
this.finished = finished || false;
this.start();
};
Timer.prototype = {
// the rest of the Timer prototype...
toObject:function() {
return {
name: this.name,
seconds: this.seconds,
started: this.started,
finished: this.finished
};
}
}

Then I save the timer array every time a new timer is created, and load the stored timers when the application starts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$('#add-timer-form form').submit(function(){
var seconds = parseInt($('#secs').val()) +
(parseInt($('#mins').val()) * 60) +
(parseInt($('#hours').val()) * 60 * 60);
var timer = new Timer($('#timer-name').val(), seconds)
timers.push(timer);
// store timers
if ('localStorage' in window) {
var arr = [];
for (var i in timers) {
arr.push(timers[i].toObject());
}
window.localStorage.setItem('timers', JSON.stringify(arr));
}
UpdateLoop.start();
return false;
});
// load stored timers
if ('localStorage' in window) {
var timersData = window.localStorage.getItem('timers');
if (timersData) {
var timersData = JSON.parse(timersData);
for (var i in timersData) {
var t = timersData[i];
timers.push(new Timer(t.name, t.seconds, t.started, t.finished));
}
UpdateLoop.start();
}
}

Some notes about the Local Storage API:

  1. localStorage only stores string values, so use JSON.stringify() and JSON.parse() to convert your data to/from JSON.

  2. In addition to setItem and getItem, there are also clear and removeItem functions available.

  3. You can also access stored values directly as properties on the localStorage object (e.g. window.localStorage.timers).

  4. Values are stored to an origin, which is the combination of scheme (http/https) + host + port. This means other pages on that server could access those stored values, so attention should be paid to avoid naming collisions.

  5. Origin pinning also means that if a page can be viewed via multiple domains (www.example.com and example.com for example), you won’t be able to access stored values between those two domains. Consider using permanent redirects to get users under one domain.

  6. Local Storage doesn’t seem to work in Firefox with pages served via the file:// protocol - you’ll need to serve your page from a web server when testing locally.

So that’s it for the part 1. A lot of this is common knowledge for those who have already read up on HTML5, but now that we’ve setup the basic of the application, we can move on to more interesting aspects of HTML5. In part 2, I’ll cover playing audio natively and using Chrome’s notification API.

You can get the final code for Part I here. If you’re interested in going deeper into HTML5, I highly recommend visiting Mark Pilgrim’s Dive Into HTML5 and getting his HTML5: Up and Running book.

Stor.IO and Backbone.js

Backbone.js is a nice JavaScript framework that provides some basic structure to your JavaScript application. What’s nice about it is that it does this while at the same time being flexible and letting you make some design decisions on your own. One of those is how you choose store you data.

While looking at Backbone’s extensive documentation, I couldn’t help but notice that they had an example todo application that used localStorage as a backing store. And since the Stor.IO API so closely resembles the localStorage API, it was easy to adapt the example application to use Stor.IO as a backend. Here’s the Github Project. And you can play around with the application here.

In other news, I’ve setup a group on Convore to discuss Stor.IO. I’m on there pretty regularly, so there’s a good chance you’ll find me there if you have questions.

Easy Storage for HTML5 Applications

I’m a big believer that HTML5 will be a game changer. It reminds me of the introduction of DHTML over a decade ago, and although DHTML the term never really caught on, it definitely paved the way to the eventual discovery of Ajax and an integral part of what is considered standard web application practice today.

A core component of HTML5 is the localStorage API. With it, you can write a fairly useful application — all in JavaScript. There are limitations however: without server-side storage, your data is trapped to that browser, and with the diversity of devices we use today (laptop, smartphones and tablets), applications need to be available wherever you have a browser.

So what if you can write an application, still all in client-side JavaScript, but store data centrally so it can be accessible on all devices and browsers? This is what Stor.IO does. Here’s some example code:

1
2
3
4
5
6
7
8
<script src="http://stor.io/app_storage.js"></script>
<script>
window.appStorage.$.connect({email: 'my@emailaddress.com'});
//window.appStorage.$.connect({twitter:true}); // use Twitter OAuth
window.appStorage.setItem('foo', 'bar');
window.appStorage.getItem('foo'); // "bar"
window.appStorage.foo; // "bar"
</script>

Including the app_storage.js script creates the appStorage object on the browser window. Next, you call the connect method on the server object ($), passing in the identity of the user. Currently, an email-only method of authenticating is available, as well as Twitter OAuth. Once connected, the appStorage API behaves essentially like the localStorage API. And it doesn’t force you to store strings like localStorage does, meaning you don’t need to serialize/deserialize objects to/from JSON.

For reading data from Stor.IO, you’ll need to wait on the server to identify the user and retrieve their data, and there’s a ready() handler available for that:

1
2
3
4
5
6
7
8
<script>
window.appStorage.$.ready(function(){
var tasks = window.appStorage.tasks;
for (var i=0; i<tasks.length; i++) {
console.log(tasks[i].text);
}
});
</script>

Underneath, how Stor.IO works is that it creates an iframe and uses the postMessage API to work around the same origin policy that handicaps Ajax calls. The server is Sinatra app with the data being stored in MongoDB. The data is isolated by the user identity and the application path, which is a substring the current URL up to the first directory path, so http://localhost/app1 and http://localhost/app2 will actually hold different data sets.

To flush out the initial implementation, I wrote a very rudimentary todo application. It’s actually hosted on Github pages, so you can see how everything works from the project page. Even better is that you can fork the project and immediately have groundwork on your own todo application, with hosting courtesy of Github.

Please give it a shot and join the Google Group if you have questions or are interested in further development.

Leaving WordPress for Jekyll

Taking inspiration from Paul, I’ve switched my blog from a self-hosted WordPress one to a static HTML site using Jekyll. I didn’t initially look at Jekyll because it seemed like the migration process was going to take a lot of work, but eventually settled on it because:

  1. I couldn’t easily migrate my blog data to Tumblr, and
  2. Although I was able to migrate to Posterous, the customization options was limited and buggy at times. Also, some of the HTML it generated (i.e. comments) looked pretty gnarly to style.

So I gave Jekyll a shot, and came out quite happy with the results. The migration script to convert my WP data worked as well as expected, and since the results were plain text it was easy to make the necessary tweaks to complete the migration.

In the switch over, I elected not to migrate all the sidebar widgets, instead putting the focus primarily on the articles. At the end of the day, that’s really want I want people to get out of this site.

Apologies to my RSS subscribers (all 70 of you) who will likely see all my posts as new posts.

Loading CouchDB Views From Source Files

I’ve been doing a lot work with CouchDB lately for Socialytics, and which means writing map/reduce functions in JavaScript for building views.  I was beginning to have a healthy set of views, it made sense to have this code in version control and be able to load these views to a CouchDB instance via the command line.  Not finding anything like this on the interwebs, I decided to come with something on my own.

My first pass at it was a Ruby implementation, but was fragile since it found the map and reduce functions via regular expressions.  This past weekend I decided to give a go with a new implementation using Node.js.  Since I’m now using JavaScript, I no longer had to use regular expressions to sniff out the map/reduce functions - I can just load the scripts up as code.  Another additional benefit is that now the view code got parsed, so I find syntax errors before the are loaded to Couch.

Here’s the code.  The script expects a designs/ folder at the same level as the loader script, with a subfolder for each design document underneath.  A design folder should have a .js file for each view.  A view file looks like this:

1
2
3
4
5
6
design.view = {
map: function(doc) {
emit(doc.created_at, null);
},
reduce: '_count'
};

CouchDB document update handlers are supported as well by assigning a function to design.update.  Once your view files are ready, just run loader.js with the CouchDB database URL as the parameter:

node db/couchdb/loader.js http://localhost:5984/myDatabase

So what do you think?  Does something like this exist already?  Is there a better way to do this?

Introducing Rack::CORS

Recently, I’ve been working on an HTML5 project that needed to need to retrieve data from a different origin, and decided to look at using CORS.

CORS, or Cross-Origin Resource Sharing is a specification that allows web applications to make AJAX calls cross-origin without resorting to workarounds such as JSONP.

Searching around, I found an CORS extension for Sinatra, which happened to be the framework I was using. However, the extension didn’t properly implement the spec, nor did it support CORS preflighting (required for more complex AJAX requests). So I rolled my own, but as a Rack Middleware. Here’s an example of a Rackup that shows it in action (this example uses Rack::CORS in Sinatra app, but should be able to use it in any Rack compatible framework):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require 'sinatra'
require 'rack/cors'
use Rack::Cors do |config|
config.allow do |allow|
allow.origins '*'
allow.resource '/file/list_all/', :headers =&gt; :any
allow.resource '/file/at/*',
:methods =&gt; [:get, :post, :put, :delete],
:headers =&gt; :any,
:max_age =&gt; 0
end
end
get '/file/list_all/' do
#...
end
get '/file/at/*' do
#...
end

To get going with Rack::CORS, just install the rack-cors Gem. To check out the source, see the project on Github.

If you want to learn more about CORS, here are some good links I found along the way:

Setting Up Virtual Hosts on Mac OSX

I’ve been juggling a few different web projects lately, and decided to setup different virtual hosts on my Mac so that I can easily work with them. Googling around gave me a lot of different answers, none of which seem to work completely. This is what finally worked for me (on Snow Leopard).

First, add a new local domain to your /etc/hosts file:

127.0.0.1       localhost devsite.local

Next, you’ll need to configure Apache with this new virtual host. Fortunately, the default Apache config has this partially setup. Open up /etc/apache2/httpd.conf and uncomment the following Include:

1
2
# Virtual hosts
#Include /private/etc/apache2/extra/httpd-vhosts.conf

Now, we need to add our virtual host to the httpd-vhosts.conf file referenced above. The file already had a couple of sample configuration in it, but I commented out those and added the following:

1
2
3
4
5
6
7
8
9
<VirtualHost *:80>
DocumentRoot "/Library/WebServer/Documents"
ServerName localhost
</VirtualHost>
<VirtualHost *:80>
DocumentRoot "/usr/docs/devsite.local"
ServerName devsite.local
</VirtualHost>

This first entry will map localhost to its default document location (without it http://localhost won’t work correctly). The second entry maps my new domain.  Additionally, you’ll want to make sure files in your new docs directory have adequate access permissions.  I ended adding a new Directory section to httpd-vhosts.conf file:

1
2
3
4
5
6
<Directory "/usr/docs/devsite.local">
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
Allow from all
</Directory>

Now all you have to do is put your web files in /usr/docs/devsite.local. I originally had my new local domain map to <user dir>/Sites/devsite.local, but changed it because I would have to make sure Apache could access to all the directories leading up to those docs. So instead I just symlinked my http docs from my user directory into /usr/docs.