I tend to favor building to buying when I want to learn something, so when I first started constructing the main site elevenseconds.com I thought of building myself a small Rails-like framework. The main difference would be that it would apply to a simple principle of just-enough abstraction layers, that is, I would introduce a new abstraction layer when I left that the cost of maintaining the current code was too high. I am currently pretty happy with the structure of the site, although updating the front page takes a bit of time so perhaps it’s time to introduce a new layer of abstraction.
At a high level, all requests go to an index page
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.cgi [QSA,L]
which grabs the desired page to be visited from the URL as well as the subpage option (I thought it would be cool to separate the option with a colon — I never liked the questions marks and ampersands, and overloading slashes for paths and parameter separations never agreed with me. And so http://elevenseconds.com/photography/italy:slides takes you to an index page which knows to display a page called photography/italy with an option of slides.
Most of the pages are very similar so I put in place a kind of Metacontent-Metaview paradigm: every page request will fetch content from the /content/ directory. The ruby files there are organized in the same hierarchy as the URLs, so the above link will fetch content from /content/photography/italy.rb.
The actual content file is very configuration-like; let’s take a look at a part of the main content file (root URL resolves to /content/main.rb):
append = []
append << rendering('text', {
'x'=>1,
'y'=>0,
'text'=>'<img src="/images/ui/whatis.png"/>11seconds: on exploration, introspection and creation'
})
append << rendering('verticalLine', {
'x'=>1,
'y'=>2,
'size'=>3
})
append << rendering('imageWithCaptionOnRight', {
'x'=>1,
'y'=>2,
'link'=>'http://blog.elevenseconds.com/trends/',
'src'=>'/images/thumbs/main/glogo.png',
'text'=>'trends'
})
append << rendering('imageOnRight', {
'x'=>1,
'y'=>11.5,
'link'=>'/pictures/glowdoodle',
'src'=>'/thumb.cgi?im=/images/pictures/glowdoodle/sadterminator.jpg&x1=197&y1=74&ss=344',
'id'=>'projects',
'text'=>'glow doodles'
})
render('main', {'append'=>append})
append is really a collection of items to append. Each item is a rendered view with some parameters. Each view is simply some HTML markup with Ruby inline code. For example, imageOnRight.rb contains
<div class="rightOfLine" style="left:#{x*35+2}px;top:#{y*35+69}px">
<a href="#{link}">
<img class="image" src="#{src}"
onMouseOver='caption("#{id}", "#{text}", "#{link}")'
onMouseOut='caption("#{id}", "", "")'/>
</a>
</div>
(To make the page more structured, I decided on absolute positioning — maybe that’s because I’m a bit of a control freak.)
Note that I’m not quite allowing hierarchical content — there is no need for it given the current design of the site. All I need to do is a two-level hierarchy: a main view (the last line in the main.rb file above may include a bunch of other views.
I needed to introduce another abstraction because a lot of the sites with photography looked the same and I saw myself writing the same code over and over again. So I wrote a function that displays a generic set of pictures. Take our italy.rb content page:
category = 'photography/italy'
caption = 'italy june 2010'
images = [
['italy-0', true, '115, 45, 305'],
['italy-1', true, ''],
['italy-2', true, '296, 20, 54'],
...
['italy-45', true, '117, 75, 85'],
]
append = []
append << generateSeries(images, category, caption, [534, 356, 2.42])
render('main', {'append'=>append})
The above specifies the absolute minimum that is needed to render the Italy photographs — a definition of each images (I don’t always just number them in sequence), with a specification of whether the image is in landscape (true) or portrait (false) mode, and the top-left coordinate and the size of a window which will be made into a thumbnail. If not specified, the function chooses a smart default.
$explore = [
['http://www.gagneint.com/Final%20site/Animation/Sensology/Sensology.html', '/images/thumbs/main/sensology.png', 'sensology'],
['http://js1k.com/demos', '/images/thumbs/main/js1k.png', 'demos'],
...
]
I do the same with the front page — since it’s mostly made up of a bunch of links, I define each (in the example above, all I need for a link is a URL and a thumbnail picture) and then just cycle over the array. I only look at the latest 10 images for each category (except for EXPLORE which gets 20) but when you click on the link you get them all (http://elevenseconds.com/detail:explore) — again, code reuse.
As the content gets more streamlined (and as I get more lazy), I’ll probably go a step further towards configuration and just define everything in some standard configuration format like JSON (XML may be human-readable, but it’s certainly not human-writeable!). My goal is not quite to minimize the number of characters that I type (because then the conventions at play will no doubt be too obscure, plus the amount of plumbing code I’ll have to write too much for me to remember what I wrote in a year), but I’ll want to get close to it.
The thumbnails are also pretty painless. The utility page, thumb.cgi, generates a thumbnail out of any picture. You just need to specify the top-left coordinate and the size of the window to made into a thumbnail, and the resulting size. Most thumbnails on my page are 32×32 (it’s a kind of hallmark of the site). Some are 67×67 (not 64×64 because I want the images to line up — two 32×32 in a row actually take up ((1+32+1)+1+(1+32+1)) = 69 pixels because of the border and the one-pixel gap so to replace it with a bigger picture means it needs to be 67: (1+67+1)=69.
thumb.cgi saves the autogenerated thumbnails in a cache directory so that the images don’t have to be scaled each time the page is loaded. That is, when thumb.cgi is called with the same arguments again, it will just bring up the saved thumbnail instead of resizing and cropping the picture:
# by default output png unless the target size is specified
format = cgi.has_key?('sc') ? 'jpeg' : 'png'
hash = Digest::SHA1.hexdigest(ENV['REQUEST_URI'].to_s())[0..7]
cacheFile = "cache/" + hash + "." + format
if(File.exist?(cacheFile))
puts cgi.header("image/#{format}")
file = new File(cacheFile, "r")
puts file.sysread(20)
exit
end
But (great example of just-enough abstraction layers), I go even further: instead of having thumb.cgi do the logic, I replace the source of the image when rendering the view with its cached image. That way I don’t even have to call thumb.cgi which significantly speeds up the page load:
def gen2cache(html)
source = html.clone
links = []
while(source =~ /"(\/thumb.cgi\?[^"]+)"/)
links.push($1)
source[$1] = ''
end
links.each { |link|
format = link.index('sc=') ? 'jpeg' : 'png'
hash = Digest::SHA1.hexdigest(link)[0..7]
cacheFile = "cache/" + hash + "." + format
if(File.exist?(cacheFile))
html[link] = "/"+cacheFile
end
}
return html
end
I guess the next step would be to actually autogenerate the entire HTML response.
Anyway, while Rails is certainly a much better solution to arrive at the final answer, I’ve learned a great deal about Ruby through this exercise. I doubt I’ll make the site much more complex than that.