If you're not a computer person, you may not be terribly interested in this particular bit. But back in the 90s when I first started developing my own websites, I hand-coded my own static HTML pages and uploaded them to my server. When I started developing software and websites for a living, I used the Microsoft .NET platform and a SQL Server database to create dynamic websites where the contents were stored in database tables rather than in separate files. Any time a user arrived at a web page, the server would have to do the work of determining which page it was, go ask the database for the necessary content sections, interweave those sections into the site's HTML template and send that out to the user's web browser. For my own personal sites, I opted to use PHP and MySQL – essentially doing the same things with slightly different technologies.
At some point in or around 2012, maybe even 2011, I sort of got tired of the way I was having to manage my own blog with hand-coded PHP and a custom MySQL schema. By switching to WordPress – an increasingly ubiquitous, very customizable PHP- and MySQL-based website platform – I felt like I caved in a bit. But it had a pretty good content editor, it was very easy to add pages, it provided good controls for presenting content along with controls for getting to content, and it seemed to present well on mobile devices. If someone who would not consider himself or herself to be a "computer person" were to ask me about putting a blog on the web, WordPress is still the first place to which I'd direct them. It's just easy as far as technology things go. But I wouldn't necessarily say that to a fellow tech-savvy developer type.
One day, I got an email from a programming site offering a free book about "static web generators". After having spent more than the past decade building dynamic database-driven web sites, I immediately cringed at the thought. But I was curious as to why "the old way of doing things" was suddenly the topic of a new discussion. I took advantage of the offer, started reading about the concept, and very quickly started investigating options.
The essential idea behind the new static movement is that static web pages are faster to serve than dynamic ones. A server doesn't have to interact with any server-side scripting language or any database to retrieve content. A server simply has to grab the HTML file and send it on its way. But the original way of handling static web pages could get tedious quickly. If you had to change a menu, or an entire site's layout or theme, you had to edit every file on the site. So the latest idea was to essentially move the server-side processing to a local computer, use individual text files as the content database, then have a program compose or compile an entire site from all of that information.
Being experimental by nature, and thinking this new approach to static web site was rather intriguing, I decided to give it a go. I'm running a Mac, so I already had Ruby installed on my machine. Initially, I was convinced that I would rebuild my personal blog using Jekyll, since that seemed to be the most blog-centric state site generator. So I installed it and built a basic site with a couple pages of content just to see how things worked.
But in my searching, I'd also come across Middleman, which seemed to be a static site generator geared more toward non-blog sites – which is what most of my customers have. That got me thinking about possibly transitioning some customer sites over to a Ruby/Middleman static platform from a .NET/SQL Server server-side platform. So I changed course a bit, installed Middleman and build a new site for a customer with that. Then I did a couple other smaller sites for existing customers.
I know that in this day and age, server disk capacity isn't really an issue. There is no need for me to be concerned about a site taking up too much space on a server because really, I don't have too many customers with sites larger than 75 MB or so. Many are down around the 35 MB level. But most of that space is taken up with actual proprietary code, some related to the website and some related to back-end business software, which may or may not be in use.
The first site I built using Middleman takes up a mere 5.5 MB of server space. All of the pages are static. Because the customer has changed phone numbers a couple of times over the past few years, I added the phone number to a configuration file. So any page that needs that phone number gets it from the config file during page generation. (And the fact is that every page references that because the phone number is in the site's header.) If the customer changes the phone number again after he gets a new sales rep or sells the company, I can change the value in a single file, rebuild the site and upload everything.
The static web site is not the solution for every customer or person who wants a website. If a non-technical person wants to get on the web, this is probably not the ideal solution for them because there is some mucking about with code and what not. But for someone managing a site that doesn't need information from a database, and where the content for most of the pages really doesn't change that often, then I think Middleman is probably a decent contender for your development time.
On my Mac, the ~/Sites/
directory is where I develop all my
websites. For personal sites, I have folder names directly beneath that
match the full domain name of any given site. Also directly under
~/Sites/
, I have a directory called clients/
.
Under that, I have directories for the name of each client. And then under
those directories, I have a folder for the full domain name of the
respective customer's sites (some customers have more than one
site). So basically, it looks like this:
~/Sites/ clients/ amazingproducts/ amazingproducts.com/ buythisnow.net/ widgetmakers/ widgetmakers.com/ fishburn.me/ someothersite.net/
A typical site folder will have the following structure:
projectsite.com/ build/ config.rb data/ pages.yml Gemfile Gemfile.lock images-source/ company-logo-source.ai source/ .htaccess 404.html.haml css/ _variables.scss site.css.scss images/ company-logo.svg index.html.haml js/ all.js layouts/ _footer.haml _head.haml _header.haml layout.haml
Typically, when you're starting a new Middleman project, you can simply type:
middleman init projectname
and it will run through some paces to
put the necessarily files in place. But I had some things that I wanted to
keep consistent across several sites, like some particular configuration
details. For instance, by default, Middleman creates a
stylesheets/
folder and a javascripts/
folder for
stylesheets and javascript files, respectively (and rather
obviously). But personally, I like those folders to be named
css/
and js/
, respectively. So essentially, I have a
"base" version of the what Middleman creates, modified with my own
preferences.
In the config.rb
file, I have the following lines:
page 'google################.html', :directory_index => false
— don't change my Google site verification file into a directory (of
course, I have my own number in place of the ################
)
activate :directory_indexes
— use directories like /about/
rather than files like
/about.html
config[:css_dir] = 'css' config[:js_dir] = 'js'
— override the directory names for the stylesheet and javascript locations
Then in the configure :build do
section, I have uncommented two
"minify" lines which are commented by default, and I added the gzip line
which is not present by default:
activate :minify_css activate :minify_javascript activate :gzip
I like shortcuts for certain things, especially if I'm working at the
command line in the Mac’s Terminal application. So I have created some
shortcuts for my Middleman workflow. In my ~/.profile
file, I
have created two aliases:
alias mmserve='bundle exec middleman serve' alias mmbuild='bundle exec middleman build'
So from the base directory of the site I'm editing, let's say
~/Sites/middleman/
, I can type mmserve (actually
just mms[TAB] and let the shell autocomplete the command for me
since this is a real shortcut) to start the local middleman server
which lets me view my work real-time at http://localhost:4567/
.
When I think I'm done editing things, I can hit Ctrl+C, then type
mmbuild and the scripts will build my site.
Since I have my own already-slightly customized base for what I want in my middleman sites, and I have a straightforward folder structure for client sites, I created a Perl script that lets me specify a customer name and a site name, then it just copies my base site to a new location. I didn't really need to go this far out of my way. But I never had gotten too deep into Perl, so I took a little time to get my feet wet there as well. So if you're a Perl guru, don't furrow your brow too deeply at this script:
#!/usr/bin/perl -w # mminit # - Script to initialize a Middleman static site by copying the codebase # from an existing directory (~/Sites/middlemanbase/) to a directory # based on the site name specified by the user. use 5.18.2; # set the path for our Middleman codebase use constant MIDDLEMAN_PATH => '/Users/fishburn/Sites/middlemanbase/'; # set up our main variables my $argc = $#ARGV + 1; my $client = ''; my $site = ''; # say_usage # - We will use this to display the usage of this in case we don't get the # the number of parameters we're expecting sub say_usage { say 'usage: mminit -c client_name -s domain_name'; say ' mminit client_name domain_name'; say ' mminit domain_name'; } # assign_site # - If the parameter passed matches a basic regular expression for a proper # domain name (including top-level domain), e.g. awesomesite.com, then we # assign our $site variable name accordingly. Otherwise, we send a message # to the user. sub assign_site { if ($_[0] =~ /^[a-z0-9\.0]+\.\w{2,4}$/) { $site = $_[0]; } else { say 'Invalid domain name. Expecting'; &say_usage; exit; } } # makedir # - Attempt to create the directory specified by the parameter sub makedir { mkdir $_[0], 0755 or warn "Cannot make $_[0] directory: $!"; } # We check for parameters in reverse order of the syntax specified in say_usage # (only because that order goes from simple and most likely used to longer and # less likely used. But the parameters expected line up, respectively: # 1 = domain_name # 2 = client_name domain_name # 4 = -c client_name -s domain_name if ($argc == 1 || $argc == 2 || $argc == 4) { my $next = ''; my $i = 0; if ($argc == 1) { assign_site($ARGV[0]); } elsif ($argc == 2) { $client = $ARGV[0]; &assign_site($ARGV[1]); } elsif ($argc == 4) { for ($i = 0; $i < $argc; $i++) { $_ = $ARGV[$i]; $next = $ARGV[$i + 1]; if (/^-c$/ && $next !~ m/^-/) { $client = $next; $i++; } elsif (/^-s$/ && $next !~ m/^-/) { &assign_site($next); $i++; } } } if ($site) { if ($client) { print "Initialize site at $client/$site?"; } else { print "Initialize site at ./$site?"; } print ' [yn] '; chomp($_ = ); if ($_ eq 'y') { if ($client) { if (!-d $client) { makedir($client); } chdir $client or die "cannot chdir to $client: $!"; } if (!-d $site) { makedir($site); system('cp', '-a', MIDDLEMAN_PATH, $site); } } else { say 'Exiting...'; } } else { &say_usage; } } else { &say_usage; }
Then, I wanted a way to deploy a site to the proper server location with as
few steps as possible. The typical method of moving files to a server is to
use FTP. But I decided to try a different method and use rsync
and another custom Perl script.
One quick note: on my web server, I have a folder specifically for uploading site previews so a client can see site changes without affecting the live site. For this folder, I forego a structure that includes client names, and have only directories for each specific site.
#!/usr/bin/perl -w # mmdeploy # - Script to deploy a Middleman static site to either a live or a preview # location on a remote server with an established folder structure convention. use Cwd; # set the base part of the server path use constant SERVER_ROOT => "/my/server/root/folder"; # get an array of our current working directory @paths = split /\//, cwd(); # set the site as the last part of the cwd (and pop it off the array) $site = pop(@paths); # set the client variable as the (new) last part of the cwd (and pop it off the array) $client = pop(@paths); print `clear`; print "Push site to server\n"; print "===================\n"; # We should only proceed if our current folder is a direct child of "Sites", # or if our current directory ends with a standard URL extension like ".com". if ($site =~ /\.\w{3}$/ || $client eq 'Sites'){ # this will be a user input option $o = ''; # if we're not directly under "Sites", then show the $client if ($client ne 'Sites') { print "Client: " . $client . "\n"; } # show the $site print " Site: " . $site . "\n"; # this just adds a line of "-" as long as the line above it print "-" x (8 + length($site)) . "\n"; # only proceed if we have a "build" directory. if (-d 'build') { # loop until we get good input while ($o eq ''){ print "Push to preview or live, or cancel? [plc] "; chomp($o =); # if we don't get 'p' or 'l', check for 'c'. If we didn't get 'c', # loop around and ask again. If we get 'c', exit. if ($o ne 'p' && $o ne 'l') { if ($o ne 'c') { $o = ''; } else { exit; } } # if we get 'l', get confirmation because this is a big deal, # overwriting the live site with what is on the development side elsif ($o eq 'l') { print "Type 'live' to confirm push to live site: "; chomp($confirm = ); # if the user doesn't type 'live', just exit. if ($confirm ne 'live') { exit; } } } # if our current directory is not directly under "Sites", then we must # be in a client folder. When we build the server path, we'll add # either $client or 'preview' depending on whether the user specified # live or preview, respectively if ($client ne 'Sites') { $rsync_path = join('/', "clients", ($o eq 'l' ? $client : 'preview'), $site); } # if we're directly under "Sites", then we're on one of our own sites, # and don't need to add $client. But we do need to still add "preview" # if necessary. If we're not on preview, the join() could potentially # double our "/" before the site name. So we'll keep else { $rsync_path = join('/', ($o eq 'p' ? 'preview' : ''), $site); # if we didn't add 'preview', we'll get '//' before $site. So we'll # us a regular expression to replace '//' with '/'. $rsync_path =~ s/\/\//\//; } $rsync_path = $rsync_path; print "deploy to /$rsync_path [yn]? "; chomp($o = ); if ($o eq 'y') { system('rsync', '-a', '--exclude=.DS_Store', './build/', 'myserver:' . SERVER_ROOT . '/' . $rsync_path); if ($? == 0) { print "Success!\n"; } else { printf "Failed! Exited with value %d\n", $? >> 8; } } else { print "--> exiting.\n"; exit; } } else { print "** You must build the site first.\n"; } } else { print "We don't seem to be in the proper subdirectory.\n"; print "--> " . cwd() . "\n"; }