- Published
- 4 July 2014
- Tagged
If you're maintaining a blog, you will probably want to put images in your posts at some point. If you have some fancy server-side blogging software to do this for you, you're set. If, however, you're using a set of files on your local drive, which somehow get turned into a website, you have to maintain your post images yourself.
This is the current setup, assuming I wrote a blog post in May 2014 which contained two images, "image.jpg" and "image2.jpg":
Note that all posts from the same month share one folder for images. They have to double up on the namespace (i.e. array of possible image names), which means if I have two posts where I use an image called image.jpg
I'll need to rename one of these files to image2.jpg
or something similarly creative.
There's one problem with such a setup: when writing my blog post, I can't just assume that the image "foo" that I want will be located at /assets/images/posts/foo.png
: I need to work out the year and month of the blog entry, then enter all that manually, e.g.:
![Sample caption](/assets/images/posts/2014-05/my-files.png)
This gets boring and repetitive quickly. I'd much prefer that my blogging engine (which is, after all, generating the whole site) work out where the image should be stored, so I only have to enter, say:
!![Sample caption](my-files.png)
Would you believe me if I said this were true?
Bastardising markdown
All my blogging is done in Markdown. Markdown has seen a number of different variations as people get it to do different things that John Gruber never intended when he made it. There's a number of very good ruby markdown libraries for ruby too: my favourite is currently redcarpet, because of the vast array of different bits of markdown it can include. Things like my sample image syntax above can be included by adding new filters, but it's easier to do this by adding a filter to nanoc itself.
Nanoc's filters are surprisingly easy to create and maintain. All they need is an identifier and some code to run on pages. Here's an "identity" filter (i.e. a filter that just returns the post as-is):
class IdentityFilter < Nanoc::Filter
identifier :identity
def run(content)
content
end
end
If you put this in your site's lib
folder (or some subfolder thereof), it'll get loaded at runtime. Then all you need to do is include it in your rules like you would normally:
compile "/stuff/*" do
filter :identity
end
Of course, this doesn't do that much. But the nice thing about these filters is that they can be chained:
compile "/stuff/*" di
filter :identity
filter :redcarpet
end
Since our !![]()
syntax is really a very specific subset of the usual markdown ![]()
image syntax, we can probably just turn it into markdown, to be properly parsed by our markdown engine of choice:
Here's the skeleton of our plugin:
class ImageFilter < Nanoc::Filter
identifier :images
def run(content)
content
end
end
But what now? How do we get our !![]()
-style images to autofill year and month? The trick here is that the item we're processing right now is available inside the filter as @item
. That item's identifier contains date information (and, indeed, you might arbitrarily include date information in the header of your item, which means it's sitting in the hash ready for you to use). Given my post entry has an identifier like /blog/2014-05/2014-05-02-blog-entry/
, we can perform a quick regex to retrieve the relevant data[1]:
if @item.identifier =~ %r<\A/blog/(\d{4}-\d{2})>
yearmonth = $1
end
Here's how we can modify our articles, removing the "custom" image tags and replacing them with standard markdown-style image tags:
class ImageFilter < Nanoc::Filter
identifier :images
def run(content)
if @item.identifier =~ %r<\A/blog/(\d{4}-\d{2})>
yearmonth = $1
content.gsub(/^!!\[(.*)]\((.*)\)$/) do
alt = $1
href = File.join("/assets/images/posts", yearmonth, $2)
"![#{alt}](#{href})"
end
else
content
end
end
end
As a bonus, this filter won't modify the item if it doesn't match the identifier string.
Expanding further
Obviously this kind of setup is perfectly designed for many small filters, each doing one focussed task. This means that you've got plenty of space to expand your own custom filters as you see fit. Want to use a <figure>
tag around your images, or maybe implement some sort of custom syntax for maximum width or captions? It's all doable with a dedicated filter. Don't forget that each filter can be enabled or disabled on a specific subset of files, giving you ultimate control over which pages use your custom syntax.
Incidentally, this is what my custom filter looks like. Decoding everything is left as an exercise to the reader:
class ImageFilter < Nanoc::Filter
identifier :images
@images = {}
class << self
attr_reader :images
def register_image(str, &blck)
regexp = Regexp.compile("^" + str + '\[([^\]]*)\]\(([^)]*)\)$')
@images[regexp] = blck
end
end
def run(content, params={})
ImageFilter.images.each do |regexp, blck|
content = content.gsub(regexp) do
caption = $1
href = $2
# Allow optional syntax: "href|width"
if href.include?("|")
href,width = href.split("|",2)
end
href = File.join(blck[@item], href)
image_tag = %|<img src="#{href}" title="#{caption}"| +
(width ? %| width="#{width}px"| : "") +
"/>"
figure = "<figure>#{image_tag}"
unless caption.empty?
caption = Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(caption)
figure << %|<div class="caption">#{caption}</div>|
end
figure << "</figure>"
figure
end
end
content
end
end
ImageFilter.register_image("!!"){ |item| item[:date].strftime("/assets/images/posts/%Y-%m") }
ImageFilter.register_image("!!p"){ "/assets/images/projects" }
ImageFilter.register_image("!!s"){ "/assets/images/site" }
Bonus step: quick image filing with Automator
You still have to get your images to your image directory, and if you're like me you can't be bothered shifting the images into the right place. Thankfully, you can automate this using Automator's suite of services and similar tricks.
I'm shifting my images using a little ruby script, which basically works out the right month-year combination and slots images into the right place. It looks something like this:
require "fileutils"
#This is where you store your images
ROOT_LOCATION = File.join(ENV['HOME'], "blog/content/assets/image/posts")
#This is the subfolder to store them in
yearmonth = Time.now.strftime("%Y-%m")
subfolder = File.join(ROOT_LOCATION, yearmonth)
FileUtils::mkdir(subfolder) unless File.exists?(subfolder)
ARGV.each do |file|
FileUtils::mv file, subfolder
end
While you could run this from the terminal, it makes much more sense to create a service in Automator that deals with the whole running-the-script thing for you:
Save this as a service, and give it an appropriate name (mine is named "â1klb"). Now all you need to do is select your image, right click and hit "â1klb" (or equivalent for your blog), and boom, the image is placed in the right place in your file system. Between this and the !![]()
syntax I introduced earlier, you'll be dropping images into your blog posts like nobody's business.
Note: this code will break after the year 9999 âŠī¸