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

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 %}

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:

download.rbraw
"
# 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&hellip;</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:

Rakefile.rbraw
"
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 %}"

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