Post on 19-Jan-2017
百⼤大媒體網站 從 wordpress 到 rails 的
⼤大⼩小事Ronald Hsu (hothero)
Technical Director, backer-founder.com
http://blog.hothero.org
@hothero
slide: http://backme.tw/ref/wp-rails
What’s the talk about?• No diabolic tricks and wicked craft
• A year experience sharing
• Goes through our practice
• You will know more gems
• You will know what media websites care about most
• You will know how to handle larger traffic for media websites
• Nothing funny
What’s the talk about?• No diabolic tricks and wicked craft
• A year experience sharing
• Goes through our practice
• You will know more gems
• You will know what media websites care about most
• You will know how to handle larger traffic for media websites
• Nothing funny
Backer-Founder
• First and leading crowdfunding consulting agency in Taiwan
• Has carried out 40 projects to domestic and abroad
• Raised over $7 million in 11 months since its establishment in October 2014.
Backer-Founder
Backer-Founder
TNL
• A group of people who are dissatisfied with the existing media environment and want to make a difference.
• We aspire to build a media that provides not only facts, but also diverse perspectives.
X
X
Our Progress & GoalBackend
+DB Schema
Frontend
Wordpress• WordPress is a free and open-source content
management system (CMS) based on PHP and MySQL.
• Features include a plugin architecture and a template system.
• WordPress was used by more than 23.3% of the top 10 million websites as of January 2015.
• Initial release May 27, 2003 (12 years ago)
http://www.wikiwand.com/en/WordPress
Wordpress
Wordpress
Wordpress
Wordpress
Wordpress
Wordpress
• More than 30000 commits
• 23+ contributors
• 127+ version releases
• Nearly 500,000 lines of code, which contains nearly 300,000 lines of php code
https://github.com/wordpress/wordpress
But why?
Three Main Issues
Customization Difficulty
Few Wordpress Developer in T.W.
Performance Issue
Three Main Issues
Customization Difficulty
Few Wordpress Developer in T.W.
Performance Issue
Three Main Issues
Customization Difficulty
Few Wordpress Developer in T.W.
Performance Issue
Three Main Issues
Customization Difficulty
Few Wordpress Developer in T.W.
Performance Issue
Three Main Issues
Customization Difficulty
Few Wordpress Developer in T.W.
Performance Issue
So...we start to use rails
Our Progress & GoalBackend
+DB Schema
Frontend
Wordpress DB Schema• posts, pages, custom
post types, attachments, links, navigation menu items, categories, tags, custom taxonomies, taxonomy terms, post metadata, widgets, options, users, hardcoded content, third party content
http://goo.gl/Tbbpfr
Wordpress Structure
posts
pages
custom post typespost metadata
options
categories
users
attachments taxonomy terms
tags
navigation menu items
widgets
custom taxonomieswp_posts wp_terms
wp_term_taxonomy
wp_post_meta
wp_options
wp_users
wp_linkslinks
Wordpress Structure
posts
pages
custom post typespost metadata
options
categories
users
attachments taxonomy terms
tags
navigation menu items
widgets
custom taxonomieswp_posts wp_terms
wp_term_taxonomy
wp_post_meta
wp_options
wp_users
wp_linkslinks
PostEditing
Tag/Category
General in Post Editing
has_many
has_manyIt means static
contentPost
Attachment
Revision
Page
posts
pages
custom post
attachments
navigation menu
wp_posts
But in wordpress …
wp_posts revisionsattachmentsnav menu items
But in wordpress …• All of those we mentioned before are in
only one table.
• Also include custom post types, links, navigation menu items!!!
post_type•post •page •revision •attachment •nav_menu_item •…
In Rails 1 has_many :attachments, -> { where(post_type: 2 "attachment") }, 3 foreign_key: 4 "post_parent", 5 class_name: 6 "Post" 7 has_many :revisions, -> { where(post_type: 8 "revision") }, 9 foreign_key: 10 "post_parent", 11 class_name: "Post"
postspages
custom post types
attachments
navigation menu items
wp_posts
Tag / Categorycategories
taxonomy terms
tags
wp_terms
Wordpress DB Schema
In practice
• Category
• wp_term_taxonomy.taxonomy: category
• wp_terms.name: Politics
• Tag
• wp_term_taxonomy.taxonomy: post_tag
• wp_terms.name: FireChat
In Rails 1 # models/w_post.rb 2 has_many :w_terms, through: : 3 w_term_relationships, foreign_key: "term_id" 4 has_many :w_term_relationships, foreign_key: 5 "object_id" 6 has_many :tags, 7 -> { where("#{WTermTaxonomy.table_name}.taxonomy = 'post_tag'") }, 9 through: :w_terms, source: :w_term_taxonomy, 10 class_name: "WTerm" 11 has_many :categories, 12 -> { where("#{WTermTaxonomy.table_name}.taxonomy = 'category'") }, 14 through: :w_terms, source: :w_term_taxonomy, 15 class_name: "WTerm" 16 17 # models/w_term_relationship.rb 18 belongs_to :w_term_taxonomy, foreign_key: 19 "term_taxonomy_id" 20 has_one :w_term, :through => :w_term_taxonomy
In Rails 1 # models/w_post.rb 2 has_many :w_terms, through: : 3 w_term_relationships, foreign_key: "term_id" 4 has_many :w_term_relationships, foreign_key: 5 "object_id" 6 has_many :tags, 7 -> { where("#{WTermTaxonomy.table_name}.taxonomy = 'post_tag'") }, 9 through: :w_terms, source: :w_term_taxonomy, 10 class_name: "WTerm" 11 has_many :categories, 12 -> { where("#{WTermTaxonomy.table_name}.taxonomy = 'category'") }, 14 through: :w_terms, source: :w_term_taxonomy, 15 class_name: "WTerm" 16 17 # models/w_term_relationship.rb 18 belongs_to :w_term_taxonomy, foreign_key: 19 "term_taxonomy_id" 20 has_one :w_term, :through => :w_term_taxonomy
If you need to know more…
• http://codex.wordpress.org/Database_Description
• http://code.tutsplus.com/tutorials/understanding-and-working-with-data-in-wordpress--cms-20567
And we made a gem
• wpdb_activerecord: https://github.com/hothero/wpdb_activerecord
• It’s a ORM wrapper for the WordPress database, using ActiveRecord.
wpdb_activerecord 1 # Gemfile 2 gem "wpdb_activerecord" 3 4 # Post 5 WPDB::Post.all # Get all posts 6 @post = WPDB::Post.find(75) 7 @post.tags 8 @post.attachments # No matter what type 9 @post.revisions 10 @post.author 11 12 # Term 13 WPDB::Term.tag # get all tags 14 WPDB::Term.category # get all categories
wpdb_activerecord - advanced
1 # config/wpdb_activerecord.yml 2 WPDB_PREFIX: "cgjbugpbs_" # the table of WPDB::Post is cgjbugpbs_posts, not wp_posts 4 WPDB_USER_CLASS: "WUser" 5 6 # models/w_user.rb 7 class WUser < WPDB::User 8 def hello 9 puts "world" 10 end 11 end 12 13 # usage 14 @author = WPDB::Post.find(25).author 15 @author.class_name # will get WUser, not WPDB:: 16 User 17 @author.hello # world
wpdb - installation
wpdb - installation
wpdb - installation
wpdb - installation
wpdb - installation
wpdb -usage
wpdb -usage
wpdb -usage
wpdb -usage
wpdb -usage
wpdb -usage
wpdb -usage
wpdb -usage
Our Progress & GoalBackend
+DB Schema
Frontend
Cloudflare - Page Rules
And we done
pcu 6000 ⬆ NT$5000 ⬇
Monthly Data • 7 million pageview ⬆ • 1.2 hundred million requests ⬆
• 4 million UU ⬆
2015/02
Our Progress & GoalBackend
+DB Schema
Frontend
CMS Admin
Edit Flow
Media Gallery
Globalize
Cronjob
editflow.org• Edit Flow gives you custom statuses, a calendar, editorial comments, and
more, all to make it much easier for your team to collaborate within WordPress.
• Calendar
• Custom Statuses
• Editorial Comments
• Editorial Metadata
• Notifications
• Story Budget
• User Groups
Editor Structure
Author
SeniorEditor
ChiefEditor
ready to review
ready to check publish
Normal Flow
Editor
Edit Flow
Calendar
Custom Statuses
Notifications
Editorial Comments• Threaded commenting in the admin for private
discussion between writers and editors.
Editorial Comments• Use this gem: acts_as_commentable
• Polymorphic Associations
1 commentable = Post.create 2 comment = commentable.comments.create 3 comment.title = "First comment." 4 comment.comment = "This is the first comment." 5 comment.save
What’s the Polymorphic Associations
• Assume a situation
• An employee has many pictures
• A Product has many pictures
• And each of those pictures have owned information detail.
What’s the Polymorphic Associations
• In general: two relationship tables to associate objects and pictures
EmployeeProduct
Employee_ Relationship
employee_idpicture_id
Product_ Relationship
product_idpicture_id
Picture
What’s the Polymorphic Associations
• Level up: an additional table to associate Employee
Product
ObjectRelationship
object_idpicture_id
Picture
What’s the Polymorphic Associations
• Polymorphic Associations
Employee
Product
Picture
imageable_idimageable_type
will be: * Employee * Product
What’s the Polymorphic Associations
• Practice in Rails.
What’s the Polymorphic Associations
• In migration
editflow.org• Edit Flow gives you custom statuses, a calendar, editorial comments, and
more, all to make it much easier for your team to collaborate within WordPress.
• Calendar
• Custom Statuses
• Editorial Comments
• Editorial Metadata
• Notifications
• Story Budget
• User Groups
Story Budget• View all of your upcoming posts in a more traditional story
budget view, and hit the print button to take it to your planning meeting.
Search Design In General• In general, we maybe use many conditions to match different
situation. 1 def search 2 @posts = Post.send(params[:scope]) 3 if params.key?(:author_id) 4 @posts = @posts.where(author_id: params[: 5 author_id]) 6 end 7 if params.key?(:post_type) 8 @posts = @posts.where(post_type: params[: 9 post_type]) 10 end 11 if params.key?(:start_date) || params.key?(: 12 end_date) 13 @posts = @posts.where(updated_at: params[: 14 start_date]..params[:end_date]) 15 end 16 # ... 17 end
Search Design In General• In general, we maybe use many conditions to match different
situation. 1 def search 2 @posts = Post.send(params[:scope]) 3 if params.key?(:author_id) 4 @posts = @posts.where(author_id: params[: 5 author_id]) 6 end 7 if params.key?(:post_type) 8 @posts = @posts.where(post_type: params[: 9 post_type]) 10 end 11 if params.key?(:start_date) || params.key?(: 12 end_date) 13 @posts = @posts.where(updated_at: params[: 14 start_date]..params[:end_date]) 15 end 16 # ... 17 end
https://github.com/activerecord-hackery/
ransack
Ransack - View Helper 1 <%= search_form_for @q do |f| %> 2 # Search if the name field contains... 3 <%= f.label :name_cont %> 4 <%= f.search_field :name_cont %> 5 6 # Search if an associated articles.title 7 starts with... 8 <%= f.label :articles_title_start %> 9 <%= f.search_field :articles_title_start %> 10 11 # Attributes may be chained. Search multiple 12 attributes for one value... 13 <%= f.label : 14 name_or_description_or_email_or_articles_t 15 itle_cont %> 16 <%= f.search_field : 17 name_or_description_or_email_or_articles_t 18 itle_cont %> 19 20 <%= f.submit %> 21 <% end %>
name_cont
Ransack - View Helper 1 <%= search_form_for @q do |f| %> 2 # Search if the name field contains... 3 <%= f.label :name_cont %> 4 <%= f.search_field :name_cont %> 5 6 # Search if an associated articles.title 7 starts with... 8 <%= f.label :articles_title_start %> 9 <%= f.search_field :articles_title_start %> 10 11 # Attributes may be chained. Search multiple 12 attributes for one value... 13 <%= f.label : 14 name_or_description_or_email_or_articles_t 15 itle_cont %> 16 <%= f.search_field : 17 name_or_description_or_email_or_articles_t 18 itle_cont %> 19 20 <%= f.submit %> 21 <% end %>
articles_title_start
Ransack - View Helper 1 <%= search_form_for @q do |f| %> 2 # Search if the name field contains... 3 <%= f.label :name_cont %> 4 <%= f.search_field :name_cont %> 5 6 # Search if an associated articles.title 7 starts with... 8 <%= f.label :articles_title_start %> 9 <%= f.search_field :articles_title_start %> 10 11 # Attributes may be chained. Search multiple 12 attributes for one value... 13 <%= f.label : 14 name_or_description_or_email_or_articles_t 15 itle_cont %> 16 <%= f.search_field : 17 name_or_description_or_email_or_articles_t 18 itle_cont %> 19 20 <%= f.submit %> 21 <% end %>
name_or_description_or_email_or_articles_title_cont
Ransack - Controller
1 def index 2 @q = Person.ransack(params[:q]) 3 @people = @q.result(distinct: true) 4 end
Ransack - Controller
1 def index 2 @q = Person.ransack(params[:q]) 3 @people = @q.result(distinct: true) 4 end
In Practice
q[status_scope]
q[author_id_eq]
q[post_type_eq]
q[author_name_cont]
q[updated_at_gteq] - q[updated_at_lteq]
editflow.org• Edit Flow gives you custom statuses, a calendar, editorial comments, and
more, all to make it much easier for your team to collaborate within WordPress.
• Calendar
• Custom Statuses
• Editorial Comments
• Editorial Metadata
• Notifications
• Story Budget
• User Groups
User Groups• Keep your users organized by department or function.
Editor Structure
Author
SeniorEditor
ChiefEditor
Editor
Maybe DB SchemaUser Author Editor Admin
orUser
user_type
STI (Single-table inheritance)class User < ActiveRecord::Baseend
class Author < User enum role: [:normal, :blogger] def publish # ex: only author can use endend
class Editor < User enum role: [:editor, :senior_editor, :chief]end
class Admin < Userend
User
typetype: rails reserved word
STI with Priority Problem
• STI is unfit for priority feature.
• Chief Editor > Senior Editor > Editor > Author
• Enum-design is more fit. 1 class User < ActiveRecord::Base 2 enum role: [:normal, :blogger, :editor, :senior_editor, :chief] 4 end
Other parts
• Except edit-flow, there are some more features
• takeover & editing lock
• media gallery
• globalize
A situation
Author
Editorwanna edit
takeover & editing lock
• Use this gem: message_bus
• MessageBus implements a Server to Server channel protocol and Server to Web Client protocol (using polling or long-polling)
• We use current_editor_id field as lock in post.
• Subscribe a channel for takeover when start editing in front-end.
• Publish a message to the channel when someone wanna edit.
takeover & editing lock
1 MessageBus.start(); 2 MessageBus.callbackInterval = 500; 3 MessageBus.subscribe("/posts/<%= @post.id %>/takeover/request", 4 function(msg){ 5 unlock_post_and_auto_save(); 6 window.location = "<%= posts_path %>"; 7 });
1 MessageBus.publish(“/posts/#{post.id}/takeover/request”, username: user.name)
Channel
Media Gallery
Media Gallery - Tricks
Browsers don't allow file uploads via XMLHttpRequest
(aka XHR) for security reasons.
General in jQuery• Hijack the forms submit event to execute our custom
iFrame-method function
• Submit the form to the iFrame normal-style (non-AJAX)
• Copy the response content from the iFrame back into the parent window.
http://www.alfajango.com/blog/ajax-file-uploads-with-the-iframe-method/
remotipart• Use this gem: remotipart
• Remotipart is a Ruby on Rails gem enabling AJAX file uploads with jQuery in Rails 3 and Rails 4 remote forms.
1 # Gemfile 2 gem "remotipart" 3 4 # js 5 //= require jquery.remotipart
Globalize
globalize• Use this gem: globalize
• Each locale have their owned tags & revisions
1 # config/initializers/post_translation.rb 2 Post::Translation.module_eval do 3 acts_as_ordered_taggable 4 has_paper_trail only: [:content, :title, : 5 excerpt] 6 end
API Design In Generalclass ApiController < ApplicationController def all # get review & news type post case params[:post_type] when "new" # latest when "hot" # hot end end
def review case params[:post_type] when "new" when "hot" end endend
def news case params[:post_type] when "new" when "hot" end end
has_scope• Use this gem: has_scope
• In model
1 class Graduation < ActiveRecord::Base 2 scope :featured, -> { where(:featured => true) 3 } 4 scope :by_degree, -> degree { where(:degree => 5 degree) } 6 scope :by_period, -> started_at, ended_at { 7 where("started_at = ? AND ended_at = ?", 8 started_at, ended_at) } 9 end
has_scope• You can use those named scopes as filters by declaring
them on your controller and just need to call apply_scopes to an specific resource. 1 class GraduationsController < 2 ApplicationController 3 has_scope :featured, :type => :boolean 4 has_scope :by_degree 5 has_scope :by_period, :using => [:started_at, 6 :ended_at], : 7 type => :hash 8 9 def index 10 @graduations = apply_scopes(Graduation).all 11 end 12 end
has_scope• You can use those named scopes as filters by declaring
them on your controller and just need to call apply_scopes to an specific resource. 1 class GraduationsController < 2 ApplicationController 3 has_scope :featured, :type => :boolean 4 has_scope :by_degree 5 has_scope :by_period, :using => [:started_at, 6 :ended_at], : 7 type => :hash 8 9 def index 10 @graduations = apply_scopes(Graduation).all 11 end 12 end
/graduations?featured=true
has_scope• You can use those named scopes as filters by declaring
them on your controller and just need to call apply_scopes to an specific resource. 1 class GraduationsController < 2 ApplicationController 3 has_scope :featured, :type => :boolean 4 has_scope :by_degree 5 has_scope :by_period, :using => [:started_at, 6 :ended_at], : 7 type => :hash 8 9 def index 10 @graduations = apply_scopes(Graduation).all 11 end 12 end
/graduations?by_period[started_at]=20100701&by_period[ended_at]=20101013
has_scope• You can use those named scopes as filters by declaring
them on your controller and just need to call apply_scopes to an specific resource. 1 class GraduationsController < 2 ApplicationController 3 has_scope :featured, :type => :boolean 4 has_scope :by_degree 5 has_scope :by_period, :using => [:started_at, 6 :ended_at], : 7 type => :hash 8 9 def index 10 @graduations = apply_scopes(Graduation).all 11 end 12 end
/graduations?featured=true&by_degree=phd
Tragedy at the admin theme
Find out a good design which have smooth operating and fancy
turbolinks & pjax• Instead of letting the browser recompile the JavaScript and
CSS between each page change, it keeps the current page instance alive and replaces only the body (or parts of) and the title in the head.
• Same advantages: good user experience, reduce bandwidth and server cost
• In particular with Rails:
• more detail: http://goo.gl/Lx7mHk
# Gemfile gem “turbolinks”
# app/assets/javascripts/application.js //= require tubolinks
$.pjax({url:’authors’, container:’#main’})
if request.headers[‘X-PJAX’] render:layout end
rails-gem-list• A workman must sharpen his tools if he is to do his work
well / ⼯工欲善其事,必先利其器
• We’ve done a gem: rails-gem-list
• You can go through rails-gem-list to know how to construct your project architecture and DB schema. Just like
• acts_as_taggable
• ransack
• …
rails-gem-list• A workman must sharpen his tools if he is to do his work
well / ⼯工欲善其事,必先利其器
• We’ve done a gem: rails-gem-list
• You can go through rails-gem-list to know how to construct your project architecture and DB schema. Just like
• acts_as_taggable
• ransack
• …
Finally• If you want to do a media website, you can follow edit-flow to
manage your posts.
• If your WordPress website becomes larger and larger, you can rewrite your front-end with Rails.
• Please don’t even think to transfer the whole site from WordPress to Rails.
• After we had spoken many topics, there are still two main parts we didn’t present
• Server Architecture
• Data Migration
Special Thanks
X
@dlackty @ymowov
@baojjeu @st0012 @nanasyu @eugg @chentyphoon @randyhsieh @fufukwang
Contact me: hothero@backer-founder.com