Building Static Websites with Webby
Let's suppose that last week your long-lost cousin Bubba called to see if you'd design the website for his new bait and tackle shop. (Hey, he's a progressive redneck.) You figured it would be a quick and easy job—he just needed a few "purty pages to git my new bidness on the World Wide Web." So you hacked together a couple static HTML pages with pictures of Bubba and his charming bait, and uploaded them to his GoDaddy account. Bubba was pleased as punch. You reckoned (hoped) you'd never hear from him again.
But it turns out Bubba was just getting started. Once he saw those first pages light up, the ideas started flowing like tobacco to a spittoon. Then he asked for new pages for all sorts of stuff: a mission statement, a series of tips on how to fish like a pro, the GPS coordinates of his favorite fishing holes, and so on.
Now you're in a pickle. The site was only supposed to have a couple pages, and so you played the ol' copy/paste trick on the header, footer, and sidebar. With all these new pages coming down the pike, you need to DRY things up in a hurry.
Being a Rails programmer, the first thing that comes to mind is layouts, helpers, and partials. But let's face it, managing a full Rails application is overkill for this job. (Bubba's not that progressive.) Instead, you'd prefer to simply use a familiar layout, helper, and partial style for now with the option of easily migrating to Rails later, if necessary.
Hello, Webby!
Webby (created by Tim Pease) takes text files written in your favorite markup language, combines them with layouts, helpers, and partials, and generates HTML files. You upload these HTML files to your web server and away you go. Simply put, Webby makes it easy to build static websites without duplication.
1. Install It
Webby is a Ruby gem, which means even Bubba could handle the install.
$ gem install webby
Don't be too alarmed by all the other gems Webby relies on to get its job done. All the dependent gems are mostly common stuff.
You can also check out Webby on GitHub.
2. Build the Default Website
To build our website with Webby, the files need to live in a directory structure that Webby understands. (Hey, Webby has opinions, too.) There's not much to it, and the webby generator creates everything for us.
$ webby bubbas_bait
That creates a bubbas_bait directory with a Rakefile and several directories:
Rakefile content/ layouts/ lib/ output/ tasks/ templates/
We'll look at each of these directories as we go. For now, let's just pull the trigger and see what happens.
$ cd bubbas_bait $ rake
Running the default Rake task creates a bunch of files in the output directory, including an index.html file. Where did that file come from? Well, it's pretty simple.
When we run rake to "build" the site, Webby copies the files in the content directory to the output directory. Some files, such as images, are copied verbatim into the output directory. Nothing too tricky there. However, the content directory can also contain special files (called pages) that include a snippet of meta-data at the top. These pages are first "compiled" into HTML files (using our favorite markup language) and then copied into the output directory. In this case, Webby transformed the default content/index.txt file into the output/index.html file. (It also copied the Blueprint CSS framework and the S5 presentation CSS into the output directory.)
If you want to clear out the output directory and build the site from scratch, use
$ rake rebuild
If we now open the output/index.html file in our browser, we see the default page for our website. So far, so good.
3. Make a Page, Any Page
Before we dive into converting Bubba's existing HTML files into Webby-managed files, let's start by creating a new page for his mission statement.
$ rake create:page mission_statement
That creates a mission_statement.txt file in the content directory, and opens the file in our default text editor. After changing the default text to include Bubba's mission statement, the file looks like this:
--- title: Mission Statement filter: - erb - textile --- h1(title). <%= h(@page.title) %> To sell world-class bait by the pound, and have fun doing it!
The YAML-formatted text at the top between the dashed lines is the meta-data for the page. All the text after the meta-data section is content that gets rendered into the resulting HTML file.
The meta-data section is simple, yet powerful. First, it contains page attributes such as title in this example. We can add arbitrary page attributes to the meta-data section and reference their values in the page using the @page.attribute notation within an ERB expression. In this case, we use @page.title to add the page title to the top of the page.
The meta-data section also contains instructions that tell Webby how to process the page. For example, the way a page is generated hinges on the filter attribute. It lists the filters that need to be applied to the page's contents in order to transform it into an HTML file in the output directory. In this case, the ERB and Textile filters will be run, in that order. We need the erb filter so that the stuff between the <% and %> (the ERB expression) will get evaluated. And since our page content is in the Textile format, we need the textile filter to transform the page into HTML. If instead we wanted to use raw HTML, which we'll use from here on, we'd simply remove the textile filter. Other filter options include markdown, haml, outline, basepath, and tidy. (See, Webby isn't that opinionated.)
Having changed a page, if we rebuild the site, only those pages that have been modified are copied to the output directory.
$ rake [13:43:04] INFO: creating output/mission_statement.html
4. Autobuild and Serve It Up
Now let's say we make another change to the content/mission_statement.txt file. To have a look at the final HTML file, we'd need to run the rake command to rebuild the site and open the resulting HTML file in our browser. That's sorta tedious. Thankfully, we can tighten up the feedback loop with Webby's autobuild feature.
$ rake autobuild
The autobuild task starts a build loop that keeps the output directory up to date as files change. Whenever a page is created or updated, its corresponding HTML file is automatically generated in the output directory. Better yet, the autobuild task serves up the files in the output directory on port 4331 using Heel , a small static web server built using Rack and Thin. That way we can immediately see changes reflected in our browser.
5. Frame It With a Layout
Next we need to put everything that's common to all the pages—the header, footer, sidebar, etc.—in one file and let Webby combine it with the contents of each page to produce the HTML file. That is, each page file should contain only the information for that page and no more.
To do that, we update the layouts/default.rhtml file to include our site's layout. Here's an example layout file:
---
extension: html
filter: erb
---
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-us">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title><%= @page.title %></title>
<link rel="stylesheet" type="text/css" href="/css/site.css" />
</head>
<body>
<div id="header">
Bubba's Bait and Tackle Shop
</div>
<div id="content">
<%= @content %>
</div>
<div id="sidebar">
Side Dishes
</div>
<div id="footer">
Copyright <%= Time.now.strftime('%Y') %> Bubba's Bait Shop.
Steal it, and we'll hunt you down!
</div>
</body>
</html>
Notice that the layout file contains meta-data just like regular page files. In this case, we're using raw HTML rather than Textile, so we only need the erb filter. The extension attribute specifies the filename extension that will be appended to every file this layout generates.
The layout file's job is to wrap the page-specific content in a consistent layout. The content of each page is accessible in the layout via the @content variable. So, we simply add the <%= @content %> line where we want the page content to show up in the layout. In addition to the @content variable, the layout also has access to all the attributes of the current page being rendered via the @page variable. For example, we're using @page.title to set the page title in the HTML header.
By default, the layouts/default.rhtml will be used as the layout file. However, if we need to generate a certain page with a specific layout, we can explicitly set the layout file for any page using the layout page attribute. For example, perhaps the mission statement page should have a more corporate feel to it. To change the layout, the mission_statement.txt page's meta-data would look something like this:
--- title: Mission Statement layout: corporate filter: - erb - textile ---
This assumes we have a layout file called _corporate.rhtml in the layouts directory.
6. Summons a Helper
Bubba likes to show off by quoting the weight of fish landed by his aromatic bait. These weights are listed on various pages around the site. We need the weights to be formatted consistently. Specifically, the weights get reported in ounces, but should be displayed in pounds and ounces.
Rather than inline the code for the weight conversion in several pages, we can write a helper method and call it from any page. To do that, we create a weight_helper.rb file, for example, in the lib directory. Just like in Rails, a helper in Webby is a Ruby method in a module.
module WeightHelper
def format_weight(weight_in_ounces)
lbs, ounces = weight_in_ounces.divmod(16)
sprintf("%d lbs, %d ounces", lbs, ounces)
end
end
Webby::Helpers.register(WeightHelper)
Unlike Rails, helpers must be explicitly registered as we did on the last line.
Then, wherever we need to display a weight on a page, we call the helper method in an ERB expression like this:
<%= format_weight(70) %>
Pretty cool.
7. Paginate A Gogo
Remember that series of pro fishing tips that Bubba wrote? To let the drama build, we can use pagination to spread them across multiple pages. Let's assume that the individual tip pages are in a directory called tips in the content directory. First we create a top-level tips page:
$ rake create:page tips
We want the contents of the tips.txt page to display the individual tip pages ten at a time in reverse chronological order. To do that, we can use the @pages.find method to fetch all the pages inside the tips directory and sort them. Then we pass the collection of pages, and the number of tips per page, to the paginate method. Here's the full tips.txt file:
---
title: Pro Fishing Tips
filter:
- erb
- textile
---
<h1><%= h(@page.title) %></h1>
<%
tips = @pages.find(:all,
:in_directory => "tips",
:recursive => true,
:sort_by => "mtime",
:reverse => true)
paginate(tips, 10) do |page|
%>
<div class="tips">
<%= render(page) %>
</div>
<% end %>
<%= link_to("Prev", @pager.prev) if @pager.prev? %>
<%= link_to("Next", @pager.next) if @pager.next? %>
The paginate method hands each tip page to the block where we simply render the page within a styled div. Finally, at the bottom of the page, we use the link_to method to generate the pagination links. The @pager object was created when we called the paginate method. It has methods including prev and next to navigate the pages.
8. Reuse Snippets with Partials
Let's say we end up including pagination on a couple more pages. Along the way we add page numbers and tart up the style of the pagination links. Instead of duplicating the pagination links on each page, we'd like to encapsulate the pagination links in one file and reuse it on all the pages that have pagination. That's where partials come in handy.
First we create the partial file:
$ rake create:partial pagination
That generates a content/_pagination.txt file. (Note that partial filenames begin with an underscore and they aren't copied to the output directory.) Inside the partial file, we add the HTML that generates the pagination links:
---
filter: erb
---
<div id="pagination" class="rounded">
<strong><%= name %> Pages</strong>:
<%= link_to("Prev", @pager.prev) if @pager.prev? %>
<% 1.upto(@pager.number_of_pages) do |page_num| %>
<% if @pager.number == page_num %>
<%= page_num %>
<% else %>
<%= link_to(page_num, @pager.page(page_num).url) %>
<% end %>
<% end %>
<%= link_to("Next", @pager.next) if @pager.next? %>
</div>
Then we use the render method at the bottom of our content/tips.txt page, for example, to include the partial into the page.
<%= render(:partial => "pagination", :locals => {:name => 'Tip'}) %>
Looking back at the _pagination.txt file, we see two variables: @pager and name. Remember that the @pager object was accessible in the original content/tips.txt page, so it's accessible in this partial as well. We use the @pager object here to generate navigation links and display page numbers. The name variable is accessible because we added it to the locals hash when calling the partial. We use it to change the text that appears next to the pagination links.
If you're a Rails programmer, you'll notice that the partial syntax is exactly the same.
9. Generate Stylesheets, Too!
So far we've only used Webby to generate HTML files, but in fact Webby can transform any kind of text into something else. For example, let's say we have our site's stylesheet in the content/css/site.css file. When we build the site, Webby will simply copy it over to the output/css/site.css file. However, if we add meta-data to the top of the stylesheet file, Webby will transform it for us.
What's this good for? Well, do you ever get tired of typing (and remembering) those hex color codes in your CSS files? If so, you're not alone. Here's an example content/css/site.css file that uses page attributes to name the colors:
---
extension: css
filter: erb
layout: nil # no layout
color:
bubba_blue: "#3366FF"
bubba_brown: "#996600"
---
body {
font-family: Verdana, "Bitstream Vera Sans", sans-serif;
background: <%= @page.color['bubba_blue'] %>;
}
table {
border: 4px solid <%= @page.color['bubba_brown'] %>;
}
th,td {
border: 2px solid <%= @page.color['bubba_brown'] %>;
}
The color attribute is a hash of color names and hex codes that we can reference in the page using the @page.color[] syntax. Notice that we also set the layout attribute to nil because the resulting output/css/site.css doesn't need a layout.
10. Deploy It
Deploying our website is as easy as building the site and uploading the contents of the output directory to our web server. Remember, everything except partial files are copied from the content directory to the output directory when we build the site. Your content directory can include an arbitrary number of subdirectories to organize your content any way you like. (Consider removing the s5 and css/blueprint directories if you aren't using them.)
If we don't already have an easy way to upload our files, Webby gives us a few Rake tasks to automate that process using rsync or ssh.
In the top-level Rakefile, we need to add three variables:
SITE.user = 'bubba' SITE.host = 'bubbas-bait.com' SITE.remote_dir = '/path/to/webserver/root/'
Then to upload the contents of the output directory using rsync, use
$ rake deploy
By default, the deploy task will use rsync to copy the files to the directory specified in the SITE.remote_dir variable. (Look in the tasks/deploy.rake file for details.) If you'd prefer uploading via ssh, use the deploy:ssh task.
And that's all there is to it! We have a simple, yet flexible, way to remove duplication from our content and upload it to our web server.
Importing Existing Files
Now that we've created pages, layouts, helpers, and partials, importing our existing static HTML files into Webby is a breeze. Each existing file currently includes all the layout and page-specific HTML. To DRY it up, we just repeat these steps for each file:
-
Copy the layout code into the layouts/default.rhtml file. (Most of the files will likely have the same default layout, and you can create custom layout files for those that are different.)
-
Copy the page-specific content into a corresponding .txt file under the content directory. You can mirror the directory structure of your existing site in the content directory.
-
Write helper methods to encapsulate view logic and keep the pages DRY.
-
Extract reusable snippets into partials so you can render them from multiple pages.
Finally, copy any asset directories into the content directory. Webby will copy it over wholesale. For example, if you have an images directory in your existing site, just copy it into the content directory.
Writing Your Own Tasks
Webby includes a number of Rake tasks for common chores when building a website. We've used several of them:
$ rake -T rake autobuild # Continuously build the website rake blog:post # Create a new blog post rake build # Build the website rake clobber # Delete the website rake create:atom_feed # Create a new atom_feed rake create:page # Create a new page rake create:partial # Create a new partial rake create:presentation # Create a new presentation rake deploy # Deploy the site to the webserver rake deploy:rsync # Deploy to the server using rsync rake deploy:ssh # Deploy to the server using ssh rake growl # Send log events to Growl (Mac OS X only) rake heel:start # Start the heel server to view website (not for ... rake heel:stop # Stop the heel server rake rebuild # Rebuild the website rake validate # Alias to validate:internal rake validate:external # Validate hyperlinks (include external sites) rake validate:internal # Validate hyperlinks (exclude external sites)
As you continue to work on your site, you may find opportunities to automate other chores. Just create a file with a .rake extension in the tasks directory and add your Rake tasks to it. It's worth looking at the built-in task files in the tasks directory for examples.
The Bubba Blog
Ok, we're going a little overboard here, but creating a blog illustrates the power of Webby. We said that Webby can be used to generate any kind of text, and that includes generating XML files for RSS or Atom news feeds. So if Bubba decides one day that he'd like to do some blogging, we can generate a blog post page:
$ rake blog:post i_got_me_a_blog
That creates a content/blog/2008/08/05/i_got_me_a_blog.txt file, for example, where we can add the text for a blog post. Then we generate an Atom feed file using
$ rake create:atom_feed bubba_blog
If we then build the site, we'll end up with an output/bubba_blog.xml file that includes all the blog entries found in the output/blog directory.
Transitioning to Rails
If at some point we need the full power of Rails, it's fairly easy to make the transition. Webby and Rails both use ERB to render pages, so converting the page files into Rails view files is straightforward. As well, the layout, helper, and partial syntax match very closely to the same facilities in Rails. It's great to be able to take incremental steps like this rather than jumping straight to Rails before you need it.
So the next time you're faced with building a static website, give Webby a whirl! I've happily used it to build several static websites now. Frankly, I'm a bit surprised it hasn't received more press. Perhaps this tutorial will help Webby get the attention it deserves.
If you're transitioning to Rails and you're looking for high-quality training, consider attending an upcoming Rails Studio or Advanced Rails Studio. We also have screencast video tutorials on a variety of timely topics, including the ExpressionEngine content management system for building dynamic websites.