Skip to content
100644 208 lines (159 sloc) 7.69 KB
90595c8 @RISCfuture Version 1.0
authored Oct 30, 2010
1 h1. Slugalicious -- Easy and powerful URL slugging for Rails 3
3 _*(no monkey-patching required)*_
5 | *Author* | Tim Morgan |
39bf93e @RISCfuture Version bump to 1.2.1
authored Mar 2, 2012
6 | *Version* | 1.2.1 (Mar 2, 2012) |
90595c8 @RISCfuture Version 1.0
authored Oct 30, 2010
7 | *License* | Released under the MIT license. |
9 h2. About
11 Slugalicious is an easy-to-use slugging library that helps you generate pretty
12 URLs for your ActiveRecord objects. It's built for Rails 3 and is cordoned off
13 in a monkey patching-free zone.
15 Slugalicious is easy to use and powerful enough to cover all of the most common
16 use-cases for slugging. Slugs are stored in a separate table, meaning you don't
17 have to make schema changes to your models, and you can change slugs while still
18 keeping the old URLs around for redirecting purposes.
20 Slugalicious is an intelligent slug generator: You can specify multiple ways to
21 generate slugs, and Slugalicious will try them all until it finds one that
22 generates a unique slug. If all else fails, Slugalicious will fall back on a
23 less pretty but guaranteed-unique backup slug generation strategy.
25 Slugalicious works with the Stringex Ruby library, meaning you get meaningful
26 slugs via the @String#to_url@ method. Below are two examples of how powerful
27 Stringex is:
29 <pre><code>
30 "$6 Dollar Burger".to_url #=> "six-dollar-burger"
31 "新年好".to_url #=> "xin-nian-hao"
32 </code></pre>
34 h2. Installation
36 *Important Note:* Slugalicious is written for Rails 3.0 and Ruby 1.9 only.
38 Firstly, add the gem to your Rails project's @Gemfile@:
40 <pre><code>
41 gem 'slugalicious'
42 </code></pre>
44 Next, use the generator to add the @Slug@ model and its migration to your
45 project:
47 <pre><code>
48 rails generate slugalicious
49 </code></pre>
51 Then run the migration to set up your database.
53 h2. Usage
55 For any model you want to slug, include the @Slugalicious@ module and call
56 @slugged@:
58 <pre><code>
59 class User < ActiveRecord::Base
60 include Slugalicious
61 slugged ->(user) { "#{user.first_name} #{user.last_name}" }
62 end
63 </code></pre>
65 Doing this sets the @to_param@ method, so you can go ahead and start generating
66 URLs using your models. You can use the @find_from_slug@ method to load a record
67 from a slug:
69 <pre><code>
70 user = User.find_from_slug(params[:id])
71 </code></pre>
73 h3. Multiple slug generators
75 The @slugged@ method takes a list of method names (as symbols) or @Procs@ that
76 each attempt to generate a slug. Each of these generators is tried in order
77 until a unique slug is generated. (The output of each of these generators is run
78 through the slugifier to convert it to a URL-safe string. The slugifier is by
79 default @String#to_url@, provided by the Stringex gem.)
81 So, if we had our @User@ class, and we first wanted to slug by last name only,
82 but then add in the first name if two people share a last name, we'd call
83 @slugged@ like so:
85 <pre><code>
86 slugged :last_name, ->(user) { "#{user.first_name} #{user.last_name}" }
87 </code></pre>
89 In the event that none of these generators manages to make a unique slug, a
90 fallback generator is used. This generator prepends the ID of the record, making
91 it guaranteed unique. Let's use the example generators shown above. If we create
92 a user with the name "Sancho Sample", he will get the slug "sample". Create
93 another user with the same name, and that user will get the slug
94 "sancho-sample;2". The semicolon is the default ID separator (and it can be
95 overridden).
97 h3. Scoped slugs
99 Slugs must normally be unique for a single model type. Thus, if you have a
100 @User@ named Hammer and a @Product@ named hammer, they can both share the
101 "hammer" slug.
103 If you want to decrease the uniqueness scope of a slug, you can do so with the
104 @:scope@ option on the @slugged@ method. Let's say you wanted to limit the scope
105 of a @Product@'s slug to its associated @Department@; that way you could have a
106 product named "keyboard" in both the Computer Supplies and the Music Supplies
107 departments. To do so, override the @:scope@ option with a method name (as
108 symbol) or a @Proc@ that limits the scope of the uniqueness requirement:
110 <pre><code>
111 class Product < ActiveRecord::Base
112 include Slugalicious
113 belongs_to :department
114 slugged :name, scope: :department_url_component
116 private
118 def department_url_component
119 + "/"
120 end
121 end
122 </code></pre>
124 Now, your computer keyboard's slug will be "computer-supplies/keyboard" and your
125 piano keyboard's slug will be "music-supplies/keyboard". There's an important
126 thing to notice here: The method or proc you use to scope the slug must return a
127 proper URL substring. That typically means you need to URL-escape it and add a
128 slash at the end, as shown in the example above.
130 When you call @to_param@ on your piano keyboard, instead of just "keyboard", you
131 will get "music-supplies/keyboard". Likewise, you can use the
132 @find_from_slug_path@ method to find a record from its full path, slug and scope
133 included. You would usually use this method in conjunction with route globbing.
134 For example, we could set up our @routes.rb@ file like so:
136 <pre><code>
137 get '/products/*path', 'products#show', as: :products
138 </code></pre>
140 Then, in our @ProductsController@, we load the product from the path slug like
141 so:
143 <pre><code>
144 def find_product
145 @product = Product.find_from_slug_path(params[:path])
146 end
147 </code></pre>
149 This is why it's very convenient to have your @:scope@ method/proc not only
150 return the uniqueness constraint, but also the scoped portion of the URL
151 preceding the slug.
153 h3. Altering and expiring slugs
155 When a model is created, it gets one slug, marked as the active slug (by
156 default). This slug is the first generator that produces a unique slug string.
158 If a model is updated, its slug is regenerated. Each of the slug generators is
159 invoked, and if any of them produces an existing slug assigned to the object,
160 that slug is made the active slug. (Priority goes to the first slug generator
161 that produces an existing slug [active or inactive]).
163 If none of the slug generators generates a known, existing slug belonging to the
164 object, then the first unique slug is used. A new @Slug@ instance is created and
165 marked as active, and any other slugs belonging to the object are marked as
166 inactive.
168 Inactive slugs do not act any differently from active slugs. An object can be
169 found by its inactive slug just as well as its active slug. The flag is there so
170 you can alter the behavior of your application depending on whether the slug is
171 current.
173 A common application of this is to have inactive slugs 301-redirect to the
174 active slug, as a way of both updating search engines' indexes and ensuring that
175 people know the URL has changed. As an example of how do this, we alter the
176 @find_product@ method shown above to be like so:
178 <pre><code>
179 def find_product
180 @product = Product.find_from_slug_path(params[:path])
181 unless @product.active_slug?(params[:path].split('/').last)
182 redirect_to product_url(@product), status: :moved_permanently
183 return false
184 end
185 return true
186 end
187 </code></pre>
f6bbf46 @RISCfuture Doc changes
authored Nov 6, 2010
189 The old URL will remain indefinitely, but users who hit it will be redirected to
190 the new URL. Ideally, links to the old URL will be replaced over time with links
191 to the new URL.
193 The problem is that even though the old slug is inactive, it's still "taken." If
194 you create a product called "Keyboard", but then rename it to "Piano", the
195 product will claim both the "keyboard" and "piano" slugs. If you had renamed it
196 to make room for a different product called "Keyboard" (like a computer
197 keyboard), you'd find its slug is "keyboard;2" or similar.
199 To prevent the slug namespace from becoming more and more polluted over time,
200 websites generally expire inactive slugs after a period of time. To do this in
201 Slugalicious, write a task that periodically checks for and deletes old,
202 inactive @Slug@ records. Such a task could be invoked through a cron job, for
203 instance. An example:
90595c8 @RISCfuture Version 1.0
authored Oct 30, 2010
205 <pre><code>
206 Slug.inactive.where([ "created_at < ?", 30.days.ago ]).delete_all
207 </code></pre>
Something went wrong with that request. Please try again.