2015 rubyconf - 百大媒體網站從 Wordpress 到 Rails 的大小事

Post on 19-Jan-2017

6.338 views 2 download

Transcript of 2015 rubyconf - 百大媒體網站從 Wordpress 到 Rails 的大小事

百⼤大媒體網站 從 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

http://xkcd.com/323/

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

wpdb_activerecord

Server Side

Visitors

i0.wp.com/…

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

Mobile APP• http://apps.thenewslens.com/

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