SEO optimization these days requires that you have nicely-dashed-urls instead of ids. While there's a few examples of how to generate these on the web, I found all of them lacking in flexibility and reuse. So here's my solution to the problem. The approach decomposes the problem into three subproblems:
- Eliminate unsafe characters from a string.
- Identify the uniqueness of a string.
- Generation of endings to construct uniqueness candidates.
You can then put them all together according to your needs. For example:
#Generate a unique name by adding integers to the end
uniquify(url_safe(name.downcase)) do |candidate|
Article.find_by_url_name(candidate).blank?
end
The code is formatted as a Ruby module that can be mixed in to your classes as needed. I hope you find it useful!
module UrlUtils
# Makes a string safe for urls
# Options:
# :replacements - a Hash of a replacement string to a regex that should match for replacement
# :char - when :replacements is not provided, this is the string that will be used to replace unsafe characters. defaults to '-'
# :collapse - set to false if multiple, consecutive unsafe characters should not be replaced with only a single instance of :char. defaults to true.
def url_safe(s, options = {})
default_regex = options.fetch(:collapse, true) ? /[^a-zA-Z0-9-]+/ : /[^a-zA-Z0-9-]/
replacements = options.fetch(:replacements, { options.fetch(:char,"-") => default_regex })
replacements.each do |replacement, regex|
s = s.gsub(regex,replacement)
end
return s
end
# Generate integers
# Options:
# :start => 1, The integer to start with
# :end => nil, the last integer to generate, when nil this becomes an infinite sequence
# :increment => 1, the amount to add for each iteration
def int_generator(options = {})
start = options.fetch(:start,1)
last = options[:end]
increment = options.fetch(:increment, 1)
raise ArgumentError if increment == 0
raise ArgumentError if last && (increment > 0 && start > last) || (increment < 0 && start < last)
Generator.new do |g|
i = start
loop do
g.yield i
return if !last.nil? && (increment > 0 && i >= last) || (increment < 0 && i <= last)
i = i + increment
end
end
end
# accepts a block that will be passed a candidate string and should return true if it is unique.
# Options:
# => :separator => "-", a string that will be injected between the base string and the uniqifier
# => :endings => generator, a Generator that provides endings to be placed at the end of the base.
# defaults to the set of positive integers.
def uniquify(base, options = {})
sep = options.fetch(:separator, "-")
endings = options[:endings] || int_generator
return base if yield base
while endings.next? do
candidate = base+sep+endings.next.to_s
return candidate if yield candidate
end
raise ArgumentError.new("No unique construction found for \"#{base}\"")
end
end
If you prefer you can get the code as a
pastie.