A downloads manager for Jekyll
On my WordPress blog I ran a plugin called Download Monitor which allowed me to create download ids that could be inserted via short tags. When I updated a download version, any mention of it throughout the site would be updated to show the latest version and link to the most recent download package. I needed something similar on my Jekyll blog to keep things up to date. The following system is geared toward Jekyll but the concept could be adapted to any static blog.
I started by using a script that looped through all of the downloads listed in my WordPress database and generated a CSV file of all of my downloads and their associated metadata. For example:
id,title,file,version,description,info,icon,updated
1,Clippable to Evernote Service,/share/clippable-to-evernote.zip,1.0,"A Snow Leopard System Service [...]",http://brettterpstra.com/code/clippable-to-evernote-service/,/images/serviceicon.jpg,Tue Feb 23 21:55:00 -0600 2010
Copy
This file is editable in a plain text editor or in a spreadsheet app like Numbers, and doesn’t require an existing WordPress database to start, just a plain old CSV file with the appropriate data.
Next, I just needed a tag plugin that would let me create a tag like:
{% download 59 %}
Copy
The tag would insert a templated “download card” with the appropriate data:
The current tag plugin I’m using has the template hardcoded and needs to be updated to handle an actual external template file, but it should give you the idea:
# Title: Download tag for Jekyll | |
# Author: Brett Terpstra http://brettterpstra.com | |
# Description: Create updateable download cards for downloadable projects. Reads from a downloads.csv file in the root folder | |
# | |
# Example: {% download id %} to read download [id] from the CSV | |
module Jekyll | |
class DownloadTag < Liquid::Tag | |
require 'csv' | |
@title = nil | |
@file = '' | |
@info = '' | |
@icon = nil | |
@template = 'link' | |
def initialize(tag_name, markup, tokens) | |
if markup =~ /^(\d+)( .*)?$/ | |
req_id = $1.strip | |
@format = $2.strip.to_i unless $2.nil? | |
CSV.read("downloads.csv").each do |line| | |
if line[0] == req_id | |
id, title, file, version, description, info, icon, updated = line | |
@file = file | |
@version = version == '' ? '' : %Q{ v#{version}} | |
@title = %Q{#{title}#{@version}} | |
@icon = icon == '' ? '' : %Q{<img src="#{icon}" width="100" height="100">} | |
@updated = updated == '' ? '' : %Q{<p class="dl_updated">Updated #{updated.sub(/[\d:]+ -\d+ (\d+)/,"\\1").strip}.</p>} | |
@description = description == '' ? '' : %Q{<p class="dl_description">#{description}</p>#{@updated}} | |
@info = info == '' ? '' : %Q{<p class="dl_info"><a href="#{info}" title="More information on #{title}">More info…</a></p>} | |
break | |
end | |
end | |
end | |
super | |
end | |
# TODO: Make this read templates | |
# Example: | |
# Dir.chdir(includes_dir) do | |
# choices = Dir['**/*'].reject { |x| File.symlink?(x) } | |
# if choices.include?(file) | |
# source = File.read(file) | |
# partial = Liquid::Template.parse(source) | |
# context.stack do | |
# rtn = rtn + partial.render(context) | |
# end | |
# else | |
# rtn = rtn + "Included file '#{file}' not found in _includes directory" | |
# end | |
# end | |
def render(context) | |
output = super | |
# TODO: Enable template selection | |
if @title | |
download = %Q{<div class="download"><h4>#{@title}</h4><p class="dl_icon"><a href="#{@file}" title="Download #{@title}">#{@icon}</a></p><div class="dl_body"><p class="dl_link"><a href="#{@file}">Download #{@title}</a></p>#{@description}#{@info}</div></div>} | |
else | |
"Error processing input, expected syntax: {% download title filename [url/to/related/post] %}" | |
end | |
end | |
end | |
end | |
Liquid::Template.register_tag('download', Jekyll::DownloadTag) |
Updating a download’s row in the CSV file with a new download link, version number, update time and/or description change will recreate all of the references to it in the site next time I build it.
I also added a Rake task for searching my downloads and finding the ID for use in the tag:
desc "Find a download ID" | |
task :find_download, :term do |t, args| | |
raise "### You haven't created a download csv yet." unless File.exists?('downloads.csv') | |
results = CSV.read("downloads.csv").delete_if {|row| | |
row[0].strip =~ /^\d+$/ && row[1] + " " + row[4] =~ /.*#{args.term}.*/i ? false : true | |
} | |
results.sort! { |a,b| | |
a[0].to_i <=> b[0].to_i | |
}.map! { |res| | |
res[0] + ": " + res[1] + " v" + res[3] | |
} | |
results_menu(results,"download") | |
print ("Select download") | |
while line = Readline.readline(": ", true) | |
if !line || line =~ /^[a-z]/i | |
puts "## Canceled" | |
Process.exit 0 | |
end | |
line = line.to_i | |
if (line > 0 && line <= results.length) | |
id = results[line.to_i - 1].match(/^\d+/)[0] | |
download_tag = "{% download #{id} %}" | |
%x{echo "#{download_tag}\\c"| pbcopy} | |
puts %Q{Download tag in clipboard: "#{download_tag}"} | |
Process.exit 0 | |
else | |
puts "## Selection out of range" | |
Process.exit 0 | |
end | |
end | |
end | |
# creates a user menu from a hash or array | |
def results_menu(res, type = "file") | |
counter = 1 | |
puts | |
res.reverse! | |
res.each do |match| | |
match = match.class == String ? match : match[:path] | |
if type == "file" | |
display = match.sub(/^.*?\/([^\/]+)$/,"\\1") | |
display.gsub!(/^[\d-]+/,'') | |
display.gsub!(/\.(md|markdown)$/,'') | |
display.gsub!(/-/,' ') | |
elsif type == "download" | |
display = match.sub(/^\d+:\s*/,'') | |
else | |
display = match.strip | |
end | |
printf("%2d ) %s\n", counter, display) | |
counter += 1 | |
end | |
puts | |
end |
Now typing rake find_download[nvalt]
will show me all downloads matching the search term “nvalt,” offer a menu of matches and put the complete Liquid tag for the selected result in my clipboard:
$ rake find_download[nvalt]
1 ) OmniFocus Clipper Plugins for Chrome v1.0
2 ) nvALT 2.2 BETA v2.2b101
3 ) Marked Watcher Scripts v1.1
4 ) QuickQuestion v1.1
5 ) nvALT v2.1
Select download: 2
Download tag in clipboard: "{% download 59 %}"
Copy
I also plan to add a Rake task to make adding new downloads and updating existing versions from the command line as simple as possible.
With a little modification, this system could easily be used to generate a “Downloads” page for my site, though I’ve decided that’s really not necessary. I may change my mind in the future, though.
Todo
What I’ve shared here is a functioning system that is currently in use. Before it’s “complete” and ready to share in my plugins repo, there are a few things I’d like to polish:
- A template system
- reads a template name from the tag
- works with multiple templates
- Rake tasks for adding and editing downloads more easily
- script for adding downloads via Service or droplet
- (Possibly) a plugin for generating a Downloads page