User-friendly URL với gem FriendlyId

Giới thiệu

Nếu bạn là lập trình viên web, hẳn bạn đã quen với những URL có dạng như http://example.com/articles/15 với số 15 ở cuối là id của videos trong Database. Bạn thấy đường dẫn kiểu này thật nhàm chán và muốn thay thế các các con số vô nghĩa bằng những nội dung dễ hiểu hơn. Ví dụ như http://example.com/articles/noi-dung-hom-nay. FriendlyId chính là thư viện mà bạn cần tìm.

FriendlyId sẽ giúp bạn thay thế các id số truyền thống bằn những keyword dễ đọc, dễ hiểu hơn với con người.

Khái niệm cơ bản

Slugs

Khái niệm Slug là phần quan trọng nhất của FriendlyId.

Một slug là một đoạn của URL, nó là một định danh của trang sử dụng keyword dễ đọc đối với con người thay vì dùng những định danh khó hiểu như id số. Điều này giúp application của bạn thân thiện hơn với con người và máy tìm kiếm.

Finders

Để mở rộng khả năng sử dụng, FriendlyId cho phép bạn xử lý những định danh bằng chuỗi ký tự giống như cách bạn xử lý với định danh số. Điều này có nghĩa, bạn có thể thực hiện các phép tìm kiếm với slugs giống như bạn tìm kiếm với id số thông thường.

Article.find(15)
Article.friendly.find("noi-dung-hom-nay")

Cả hai câu lệnh trên đều trả về kết quả như nhau.

Thiết lập FriendlyId cho model của bạn

Để sử dụng FriendlyId trong các model của Application, trước hết bạn cần extend hoặc include module FriendlyId (cả 2 cách này đều cho kết quả giống nhau), sau đó gọi method friendly_id để thiết lập tuỳ chọn mà bạn mong muốn.

class Foo < ActiveRecord::Base
  include FriendlyId
  friendly_id :bar, :use => [:slugged, :simple_i18n]
end

Tuỳ chọn quan trọng nhất là :use, nó sẽ cho bạn biết bạn sử dụng addon nào của FriendlyId. FriendlyId có các addon sau: :slugged, :history, :reserved, :scoped, :simple_i18n

Chú ý: Khi bạn sử dụng Single Table Inheritance (STI), bạn cần extend FriendlyId ở tất cả các class tham gia vào STI, bao gồm cả class cha và class con.

Sử dụng lệnh Find với FriendlyID

FriendlyId cho phép bạn sử dụng finder để tìm kiếm record bằng friendly id, và cho phép tìm kiếm bằng cả id số nếu cần thiết. Điều này giúp cho bạn dễ dàng thêm FriendlyId vào một application có sẵn mà chỉ phải viết thêm một số lượng ít code.

Mặc đinh, các method này chỉ có thể được sử dụng trong scope friendly.

Article.friendly.find('noi-dung-hom-nay') #=> Có hoạt động
Article.friendly.find(15)                 #=> Vẫn hoạt động
Article.find(15)                          #=> Vẫn hoạt động
Article.find('noi-dung-hom-nay')          #=> Không hoạt động

Như vậy trong controller của application, bạn cần thay đổi method set_resource bằng cách thêm scope friendly vào.

# Trước
def set_article
  @article = Article.find params[:id]
end

# Sau
def set_article
  @article = Article.friendly.find params[:id]
end

Để mặc định sử dụng scope friendly, bạn có thể thêm addon finders vào trong config của friendly_id.

friendly_id :title, use: [:slugged, :finder]

Article.friendly.find('noi-dung-hom-nay')  #=> Có hoạt động
Article.find('noi-dung-hom-nay')           #=> Vẫn hoạt động

Slugged Models

FriendlyId có thể sử dụng một column riêng biệt để lưu trữ slugs cho các model.

Ví dụ, một blog application thường sử dụng title của bài viết để tạo ra một URL phù hợp với máy tìm kiếm. Những định danh như vậy thường thiếu các ký tự inhoa, sử dụng dụng ký tự ASCII tương ứng với UTF-8 vào bỏ đi các ký tự không phù hợp.

class Article < ActiveRecord::Base
  extend FriendlyId
  friendly_id :title, use: :slugged
end

@article = Article.create title: "Nội dung hôm nay"
@article.friendly_id #=> trả về "noi-dung-hom-nay"
redirect_to @article #=> mở ra trang có URL là /articles/noi-dung-hom-nay

FriendlyID sẽ generate slugs từ một column hoặc một method được chỉ định của model và lưu nó ở một column trong model. Mặc định thì column này là :slug. Bạn nên thêm index cho column này và trong hầu hết các trường hợp, cần thiết lập unique và NOT NULL. Tuy nhiên điều này đôi khi cũng có thể thay đổi tùy thuộc vào yêu cầu và hoạt động của app của bạn.

Làm việc với Slugs

Định dạng

Mặc đinh, FriendlyId sử dụng method paramaterize của ActiveRecord để tạo ra slug. Method này sẽ thay thế các dấu cách bởi dấu gạch ngang, và thay thế các ký tự đặc biệt bởi ký tự ASCII xấp xỉ nó.

Column hay Method?

FriendlyId luôn sủ dụng một method để tạo ra slug text chứ không phải là một column. Điều này có vẻ dễ gây nhầm lẫn, nhưng bạn hãy nhớ rằng ActiveRecord cung cấp các method cho mỗi column trong một model, và đó là thứ mà FriendlyId sử dụng.

Đây là ví dụ của một class sử dụng một customer method để tạo ra slug.

class Person < ActiveRecord::Base
  friendly_id: full_name

  def full_name
    "#{first_name} #{last_name}"
  end
end

ronaldo = Person.create! first_name: "Ronaldo", last_name: "Cristiano"
ronaldo.friendly_id #=> "cristiano-ronaldo"

Tính duy nhất

Khi bạn insert một record mà sẽ generate một friendly id trùng lặp, FriendlyId sẽ thêm một UUID để generate một slug nhằm đảm bảo tính duy nhất cho các friendly id.

ronaldo = Person.create first_name: "Cristiano", last_name: "Ronaldo"
ronaldo_junior = Person.create first_name: "Cristiano", last_name: "Ronaldo"

ronaldo.friendly_id         #=> "cristiano-ronaldo"
ronaldo_junior.friendly_id  #=> "cristiano-ronaldo-206-f9f3789a-daec-4156-af1d-fab81aa16ee5"

Candidate

UUID khó nhìn và khó hiểu cũng giống như id số, vì vậy FriendlyId cung cấp cho bạn tính năng "slug candidates" để cho phép bạn chỉ định một slug thay thế, trong trường hợp slug được tạo ra trùng với slug đã có từ trước.

class Player < ActiveRecord::Base
  extend FriendlyId
  friendly_id :slug_candidates, use: :slugged

  def slug_candidates
    [
      :name,
      [:name, :nationality]
    ]
  end
end

ronaldo_portugal = Player.create! name: 'Ronaldo', nationality: 'Portugal'
ronaldo_brazil = Player.create! name: 'Ronaldo', nationality: 'Brazil'

ronaldo_portugal.friendly_id  #=> 'ronaldo'
ronaldo_brazil.friendly_id  #=> 'ronaldo-brazil'

Quyết định khi nào cần tạo mới Slugs

Ở version mới nhất của FriendlyId ver5.0, mặc định slug chỉ được generate khi giá trị của column slug là nil. Nếu bạn muốn regenerate lại slug, hãy set giá trị của trường slug về nil.

player.friendly_id #=> "ronaldo"
player.name = "Nani"
player.save!
player.friendly_id #=> "ronaldo"
player.slug = nil
player.save!
player.friendly_id #=> "nani"

Bạn cũng có thể custom lại xử lý này bằng cách override method FriendlyId::Slugged#should_generate_new_friendly_id?

Điều này sẽ giúp bạn kiểm soát chính xác khi nào cần set lại friendly_id.

def should_generate_new_friendly_id?
  name_changed?
end

Một lỗi khi sử dụng FriendlyId cùng với Cancancan

Kết luận

Các ứng dụng web ngày nay có nhu cầu lớn về việc tối ưu hóa cho máy tìm kiếm. Và một trong những bước để tối ưu hóa đó chính là xây dựng các Search Engine-friendly URL, FriendlyId chính là một trong những công cụ hỗ trợ đắc lực cho việc xây dựng trang web của bạn.

Tham khảo

  1. https://github.com/norman/friendly_id
  2. http://norman.github.io/friendly_id/file.Guide.html