翻译自 https://medium.com/la-revanche-des-sites/seo-ruby-on-rails-the-comprehensive-guide-2018-b4101cc51b78,虽然该文章的标题是写着 2018,但译者以为即便在今天,许多建议依然十分实用。于是把这篇文章的标题翻译成《Ruby On Rails 中 SEO 综合指南》。原译文发布于:https://www.lanzhiheng.com/translations/seo-ruby-on-rails-the-comprehensive-guide
在la revanche des sites,我们相信存在着一种更好的方式来做市场推广。而这种方式不仅能够为客户带来收益,并且价值更高,侵入性更低。这也是我们专注于搜索引擎优化(SEO)的原因,这能够驱动客户去访问你的网站,或者让他们直接前往你线下的店铺。
我们许多客户的网站都是基于 Ruby On Rails 来构建的,这是一款用 Ruby 编程语言开发的 Web 应用框架。在这篇文章中,我们会归纳出曾经使用过的一些最为通用的 SEO 技术,你可以根据这些案例以及小贴士来优化你的 Rails 应用程序的网站排名。
源码可以参考Github。
我们先从 URL 开始。Rails 默认采用主键(也称作 id)来生成资源对应的路由以及 URL。举个例子,如果你创建了一个新的 post 条目,在默认情况下,这个资源所对应的 URL 将会是/posts/1
,这里的数字1
是唯一标识。不管怎么说,为了能够有更好的用户体验,并且让搜索引擎更好地处理我们的 URL,我们通常会建议 URL 应该由与资源相关的关键字来组成。这些 URL 要同时具有合法性跟唯一性!
你可以自行开发一套系统来管理这些 URL,对你来说并非什么难事。只不过,从效率和可维护性层面来考虑,我们会引入一个名为friendly_id的 Gem。它让你可以用一个与资源相关的字符串来生成链接,这个字符串通常称之为 slug。slug 不会包含特殊字符,并且不能够重复出现(毕竟两个资源不能有相同的 URL)。再来看我们的例子,/posts/1
将会变成/posts/your-title
,your-title
就是资源对应的 slug,要从我们已有的数据模型中选择一个字段来存储它。
跟随指示安装好 Gem 之后,它让你可以这样去配置资源:
# app/models/post.rb
class Post < ApplicationRecord
# friendly url
extend FriendlyId
friendly_id :title, use: :slugged
end
# app/controllers/posts_controller.rb
def show
@post = Post.friendly.find(params[:id])
end
错误页面不仅仅可以给予用户提示,还能够指示机器人说这是一个不可用的资源。从技术角度上看,你的 Rails 服务会根据资源的不同状态来返回对应的 HTTP 状态码。这些页面对于 SEO 来说是很重要的,包括 Google 在内的搜索引擎都会用到它们,这是为了更好地去理解你网站的行为与架构。
默认情况下,Rails 提供了 404(页面不存在),422(不完整或者是错误的资源),500(服务器错误)等静态页面,它们位于/public
目录下。你需要定制化这几个页面,让这些页面能够更好地反映出站点的设计风格(通过layouts/application.html.erb
)又或者是能够基于不同用户动态渲染相应的内容。
要定制化这些页面,首先需要为它们设定路由:
# config/routes.rb
match "/404", to: "errors#not_found", via: :all
match "/422", to: "errors#unacceptable", via: :all
match "/500", to: "errors#internal_server_error", via: :all
接下来创建对应的控制器:
# app/controllers/errors_controller.rb
# do not forget to delete public/{404, 422, 500}.html
# rm public/{404, 422, 500}.html
class ErrorsController < ApplicationController
def not_found
render status: 404
end
def unacceptable
render status: 422
end
def internal_server_error
render status: 500
end
end
不要忘记删除 public 目录下的老页面,可以通过命令
rm -rf public/{404,422,500}.html
来一次性删除它们。
最后,修改 Rails 应用的配置,设置当异常发生的时候所采用的异常程序(默认是ActionDispatch::PublicExceptions.new(Rails.public_path))
# config/application.rb
config.exceptions_app = self.routes
在你把应用程序部署到生产环境之后,可能还会对网站进行一定程度的调整。你可能会添加,修改或者删除其中的一些页面;更改其中一篇文章的名字,从而导致 slug 也因此而更改;调整网站的架构,网站的 URL 集合也会随之更改。不管怎么说,如果在这个阶段搜索引擎已经完成了对网站的检索工作,你将需要对一些老旧的页面(或者说旧的 URL)进行重定向。让它们跳转到新的页面或者新生的 URL 中去,这样的话你网站的访客就不会收到一些 404 页面了。在这种情况下,你就必须要创建 301 跳转规则,让一个页面能够永久性地跳转到另外一个页面去。
Rails 的路由系统让你能够很容易地配置这些 301 的重定向,只需要调整routes.rb
这个文件
# 301 redirect from old URLs
match "/old_path_to_posts/:id", to: redirect("/posts/%{id}s")
你当然也可以定义一些正则表达式,来对某些类型的页面进行重定向。
元标签让搜索引擎跟社交网络能够更好地理解你的页面内容,并进行一定程度的可视化。因此它们也是 SEO 的基本要素。元标签必须是动态的,不同页面的元标签内容应该有所不同,要避免重复内容而引起的冲突。
Ruby On Rails 并没有为元标签准备一个默认的管理系统,因此我们要找个地方来放它们。
首先在config
目录下创建一个名为meta.yml
的文件,它会包含以下内容:
# config/meta.yml
meta_title: "SEO & Ruby On Rails"
meta_description: "The 2018 comprehensive guide on SEO in Rails"
meta_image: "logo.png" # Une image dans votre dossier app/assets/images/
twitter_account: "@RevancheSites"
接下来我们需要初始化 Ruby 常量DEFAULT_META
(内容从上面的配置文件读取),让你能够在程序的任何地方使用这些值
# config/initializers/default_meta.rb
# Initialize default meta tags.
DEFAULT_META = YAML.load_file(Rails.root.join("config/meta.yml"))
创建一些帮助方法来读取这些值
# app/helpers/meta_tags_helper.rb
module MetaTagsHelper
def meta_title
content_for?(:meta_title) ? content_for(:meta_title) : DEFAULT_META["meta_title"]
end
def meta_description
content_for?(:meta_description) ? content_for(:meta_description) : DEFAULT_META["meta_description"]
end
def meta_image
meta_image = (content_for?(:meta_image) ? content_for(:meta_image) : DEFAULT_META["meta_image"])
# ajoutez la ligne ci-dessous pour que le helper fonctionne indifféremment
# avec une image dans vos assets ou une url absolue
meta_image.starts_with?("http") ? meta_image : image_url(meta_image)
end
end
现在可以把元标签添加到你的 layout 中去了
title = meta_title
meta name="description" content="#{meta_description}"
/ Facebook Open Graph data
meta property="og:title" content="#{meta_title}"
meta property="og:type" content="website"
meta property="og:url" content="#{request.original_url}"
meta property="og:image" content="#{meta_image}"
meta property="og:description" content="#{meta_description}"
meta property="og:site_name" content="#{meta_title}"
/ Twitter Card data
meta name="twitter:card" content="summary_large_image"
meta name="twitter:site" content="#{DEFAULT_META["twitter_account"]}"
meta name="twitter:title" content="#{meta_title}"
meta name="twitter:description" content="#{meta_description}"
meta name="twitter:creator" content="#{DEFAULT_META["twitter_account"]}"
meta name="twitter:image:src" content="#{meta_image}"
就这样,你现在就可以根据你的资源来动态设置这些变量了。举个例子,posts#show
这个动作所对应的视图可以这样去写:
# app/views/posts/show.html.slim
- content_for :meta_title, "#{@post.title} | #{DEFAULT_META["meta_title"]}"
结构化数据能够针对你的应用以及内容往搜索引擎发送合适的信号,帮助搜索引擎更好的理解你网站,还能够以Rich Results的方式来增强搜索结果的可视化程度。当然也会提高被Knowledge Graph选中的可能性。你真的需要在标记模式 (Markup Scheme) 的实现上下点功夫,因为这些数据对于 Google 来说越来越重要了。
要了解有哪些模式能够在你的网站中使用,我们推荐 Google 公司提供的系列文章。
有许多不同的格式可以选择:JSON-LD, Microdata 或者 RDFA。Google 会推荐你使用 JSON-LD。不过简单起见,我们通常会使用 Microdata。
green_monkey这个 Gem 能够帮你简化往应用程序中集成 Microdata 的工作。
# app/models/post.rb
class Post < ActiveRecord::Base
html_schema_type :BlogPosting
end
Post.html_schema_type # => Mida::SchemaOrg::BlogPosting
Post.new.html_schema_type # => Mida::SchemaOrg::BlogPosting
%article[post]
= link_to "/posts/#{post.id}", :itemprop => "url" do
%h3[:name]>= post.title
.post_body[:articleBody]= post.body.html_safe
= time_tag(post.created_at, :itemprop => "datePublished")
面包屑为你提供类似 GPS 的服务,让你能够记得自己所在的页面正处于当前网站这颗大树的哪个位置。而这对于 SEO 来说也是很重要的,Google 官方也推荐您为你的网站提供面包屑功能。
现在已经有一些 Gem 能够自动地为你的应用程序生成面包屑,比如:gretel,loaf或者breadcrumbs_on_rails。我们将使用breadcrumbs_on_rails
这个 Gem,它不仅定制化过程简便,而且还能够跟 Bootstrap 兼容。
在Gemfile
中添加下列代码来安装这个 Gem:
gem "breadcrumbs_on_rails"
接着给需要显示面包屑的页面所对应的控制器添加代码:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
add_breadcrumb "Blog", :posts_path
def index
@posts = Post.all
end
def show
@post = Post.friendly.find(params[:id])
add_breadcrumb @post, post_path(@post)
end
end
请注意:如果你想要给你的面包屑添加结构化数据,可以定义下面这个构造器:
# lib/breadcrumbs/builders/structured_data_breadcrumbs_builder.rb
class Breadcrumbs::Builders::StructuredDataBreadcrumbsBuilder < BreadcrumbsOnRails::Breadcrumbs::Builder
def render
@elements.collect do |element|
render_element(element)
end.join(@options[:separator] || " » ")
end
def render_element(element)
if element.path == nil
content = compute_name(element)
else
content = @context.link_to_unless_current(compute_name(element), compute_path(element), element.options.merge({
itemscope: "",
itemtype: "http://schema.org/Thing",
itemprop: "item"
}))
end
if @options[:tag]
@context.content_tag(
@options[:tag],
content,
{itemprop:"itemListElement", itemscope: "", itemtype:"http://schema.org/ListItem"}
)
else
ERB::Util.h(content)
end
end
end
然后把面包屑添加到 layout 里面去:
# layouts/application.html.slim
ul.breadcrumb itemscope="" itemtype="http://schema.org/BreadcrumbList"
= render_breadcrumbs tag: 'li', separator: '',
builder: Breadcrumbs::Builders::StructuredDataBreadcrumbsBuilder
网站地图实质上就是一个包含了该网站 URL 列表的文件。这个文件是 XML 格式的。它把你网站的 URL 以及架构相关的信息提供给搜索引擎。它为网站提供实质性的内容及复杂的网眼,是一个很不错的参考,借此搜索引擎能够更好地理解你的网站。
一些现有的 Gem,比如sitemap_generator,或者dynamic_sitemaps都能够直接用来生成这个文件。只是 Rails 已经提供了XML builder
,你完全可以自己去构建它。
# config/routes.rb
get '/sitemap.xml' => 'sitemaps#index', defaults: { format: 'xml' }
接着,让我们关心一下控制器的构建:
# app/controllers/sitemaps_controller.rb
class SitemapsController < ApplicationController
layout :false
before_action :init_sitemap
def index
@posts = Post.all
end
private
def init_sitemap
headers['Content-Type'] = 'application/xml'
end
end
以及它所关联的视图
# app/views/sitemaps/index.xml.builder
xml.instruct! :xml, version: '1.0'
xml.tag! 'sitemapindex', 'xmlns' => "http://www.sitemaps.org/schemas/sitemap/0.9" do
xml.tag! 'url' do
xml.tag! 'loc', root_url
end
xml.tag! 'url' do
xml.tag! 'loc', contact_url
end
@posts.each do |post|
xml.tag! 'url' do
xml.tag! 'loc', post_url(post)
xml.lastmod post.updated_at.strftime("%F")
end
end
end
译者比较推荐使用 sitemap_generator,可以配合 Capistrano 在部署的时候自动生成对应的网站地图。
robots.txt
是一个文本文件,通常搜索引擎机器人链接你网站的时候会用到。这个文件包含了一些指令,让搜索引擎知道,在网站检索的过程中,哪些页面应该被检索,哪些页面不应该被收录。你可以访问robots-txt.com来获取更多关于它的信息。
Ruby On Rails 为用户准备了一个默认的robots.txt
文件,它坐落在public/
目录下。你可以像处理错误页面以及网站地图那样,把它弄成动态的。让它能够根据不同的环境来定义不同的指令。比如说:开发环境(develoopment),预生产环境(pre-production),或生产环境(production)。
# config/routes.rb
get "/robots.:format", to: "pages#robots"
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def robots
# Don't forget to delete /public/robots.txt
respond_to :text
end
end
这里也会指定网站地图所对应的 URL,让机器人更容易找到它。
- if Rails.env == "production"
= "User-Agent: *\n"
= "Disallow: \n"
- else
= "User-Agent: *\n"
= "Noindex: /\n"
= "\nSitemap: #{root_url}sitemap.xml"
AMP,全名 Accelerated Mobile Pages(加速手机页面),是 Google 设计的一个协议,为了能够得到“更加快速的页面”。AMP 页面是传统网页的一个变体,是通过精简 HTML 以及 JavaScript 来实现的。AMP 只会保留展示所需要的信息,因此非常快速。
首先,在你的应用程序中注册一个新的 MIME 类型:
# config/initializers/mime_types.rb
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
Mime::Type.register 'text/html', :amp
接下来你需要创建一个新的 AMP 页面的 layout:
# layouts/application.amp.erb
<!doctype html>
<html amp>
<head>
<meta charset="utf-8">
<link rel="canonical" href="<%= url_for(format: :html, only_path: false) %>" >
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>
<% if Rails.application.assets && Rails.application.assets['amp/application'] %>
<style amp-custom><%= Rails.application.assets['amp/application'].to_s.html_safe %></style>
<% else %>
<style amp-custom><%= File.read "#{Rails.root}/public#{stylesheet_path('amp/application', host: nil)}" %></style>
<% end %>
</head>
<body>
<div class="amp">
<%= yield %>
</div>
</body>
</html>
AMP 只能够以内联的方式引入样式(不能用外链)。在这里使用 Rails 的 Assets Pipeline 完成编译流程,接着把已经编译完成的 CSS 代码引入到视图页面中去:首先创建一个新的 SASS 格式的样式文件app/assets/stylesheets/amp/application.scss
,这个文件包含了你想要在 AMP 中使用的样式。然后把这个文件添加到预编译流中去,只需要在config/application.rb
中添加指令代码
# config/application.rb
config.assets.precompile << 'amp/application.scss'
还有最后一个问题要去解决。我们需要处理掉那些 AMP 限制使用的标签,根据要求做些相应的调整。举个例子,img
这个标签需要转换成amp-img
,iframe
这个标签也一样。解决方案是实现一个“洗涤器”,用来对 Rails 的 ActionView 中用来过滤敏感标签的方法进行定制化。
这个“洗涤器”会尝试将原始的 DOM 转换成“AMP 认证”的 DOM。它看起来像是这样
# app/scrubbers/amp_scrubber.rb
class AmpScrubber < Rails::Html::PermitScrubber
TAG_MAPPINGS = {
'img' => lambda { |node|
if node['width'] && node['height']
node.name = 'amp-img'
node['layout'] = 'responsive'
node['srcset'] = node['src']
else
node.remove
end
},
'iframe' => lambda { |node|
find_parent(node).add_child(node)
node['src'] = node['src'].gsub(%r{^(\/\/|http:\/\/)}, 'https://')
url = URI(node['src'])
node['layout'] = 'responsive'
if url.host.include?('youtube.com')
node.name = 'amp-youtube'
node['data-videoid'] = node['src'].match(%r{(\/embed\/|watch?v=)(.*)})[2]
node.remove_attribute('src')
else
node.name = 'amp-iframe'
end
}
}.freeze
def initialize
super
@tags = %w(a em p span h1 h2 h3 h4 h5 h6 div strong s u br blockquote)
@attributes = %w(style contenteditable frameborder allowfullscreen)
end
def self.find_parent(node)
node = node.parent while node.parent
node
end
protected
def scrub_attribute?(name)
!super
end
def scrub_node(node)
if node.name.in?(TAG_MAPPINGS.keys)
remap_node! node, TAG_MAPPINGS[node.name]
else
super
end
end
def remap_node!(node, filter)
case filter
when String
node.name = filter
when Proc
filter.call(node)
end
end
end
最后,你就可以创建一个新的并且符合 AMP 规范的视图页面了
# app/views/posts/show.amp.slim
h1 = @post.title
div = sanitize @post.content, scrubber: AmpScrubber.new
别忘了在 layout 那里指定这些 AMP 页面所对应的 URL,让 Google 能够检索到它们
# layouts/application.html.erb
<link rel="canonical" href="<%= url_for(format: :html, only_path: false) %>" >
<link rel="amphtml" href="<%= url_for(format: :amp, only_path: false) %>" >
默认情况下,如果你已经为你的服务器配置了SSL 证书,应该自觉地让你的 Rails 应用程序走 HTTPS 协议。为了在特定的环境下开启这个功能,你需要开启下面这个选项:(译者一般在 Nginx 做这个配置)
# config/environments/production.rb
Rails.application.configure do
# ...
# force HTTPS on production
config.force_ssl = true
end
在做 SEO 的时候,很常见的一个问题就是会出现重复内容,主要体现在两个不同的页面包含了完全相同的内容。这个问题常见于 non-www 的页面(http://example.comwww的页面(http://www.example.com)包含了相同的内容。)跟
围绕这个问题,有个简单的技术可以解决:
# Force www redirect
# Start server with rails s -p 3000 -b lvh.me
# Then go to http://www.lvh.me:3000
constraints(host: /^(?!www\.)/i) do
match '(*any)' => redirect { |params, request|
URI.parse(request.url).tap { |uri| uri.host = "www.#{uri.host}" }.to_s
}
end
另外一个用于避免重复内容的方案就是使用设置了rel="canonical"
属性的link
标签。它告知搜索引擎,当前页面所对应的基准 URL(Canonical URL)。
如果你网站中的任一页面都能够通过多个不同的 URL 来访问,或者是网站中不同的页面具有相似的内容(比如,同一个页面有移动版本跟 PC 版本之分),那么你必须要显式地告知 Google 这些页面基准的 URL(或者说是有权威性的 URL)是什么。换句话说,Google 就会收录你提供的这个基准页面,或者它会把那些相似的页面(但他们的基准 URL 都相同)都看成是同一个页面。
在 Rails 应用上设置的小技巧:
# layouts/application.html.slim
= yield :canonical
# app/helpers/application_helper.rb
def canonical(url)
content_for(:canonical, tag(:link, rel: :canonical, href: url)) if url
end
# app/views/posts/show.html.slim
- canonical(blog_post_url(@blog_post))
我们在先前的一篇文章SEO 实现策略中有阐述过,对 SEO 来说缓存策略并不是微不足道的存在,尤其是当你的网站包含了许多 URL 的时候。针对每个网站的爬虫工作,Google 的机器人都会分配一个特定的时长,你的页面加载得越快,它在这段时间内就能够爬取到你网站中更多的页面。
Rails 提供了一些缓存相关的功能。针对这个主题可以另起一篇文章来专门描述,故而我建议你去参考官方的文档。
当你把网站上线之后,通常会通过像PageSpeed这样的工具从技术层面监测网站的状态,它让你知道从技术层面来看 Google 给予你网站的评分等级。那就是说 Google PageSpeed 所给出的评分,也是 SEO 中影响网站排名的一个因素,因此你需要把目标锁定在尽可能得到最好的评分等级上。
一般来说,PageSpeed 所抛出的问题会包括,你的 JavaScript 资源所放置的位置以及静态资源的压缩情况。针对第一个问题,你只需要把它们统一打包放置在 layout 层。而针对压缩问题,只需要简单开启它就行,通过配置Rack::Deflater这个中间件就能够做到。在你的应用程序中插入相关的代码,能够显著减少响应的大小。
# config/application.rb
module SeoRubyOnRails
class Application < Rails::Application
# Deflater
# See also : https://robots.thoughtbot.com/content-compression-with-rack-deflater
config.middleware.use Rack::Deflater
end
end
你已经了解到,在 SEO 中,链接(或者说锚点)是关键要素,它们让你的访问者或机器人能够正常浏览你的网站。这个时候了解一下nofollow
这个指令就真的很重要了。默认情况下,如果机器人看到了一个链接,它会跟踪这个链接,并且会尝试递归地去检索整个页面。这个规则不仅仅用于站内链接,对于外部链接也是一样的。重点关注那些可能会被垃圾信息制造者利用的字段(情况往往发生在表单的填写,提交之后结果会显示在页面上),他们会在这些地方贴上自己网站的链接,来提高自己网站的可见度。为了解决这个问题,我们可以采用Nokogiri这个 Gem。只需要针对那些可能会包含链接的字段在入库之前进行审查就可以了。
# app/models/post.rb
class Post < ApplicationRecord
# ...
# callbacks
before_save :anti_spam
def anti_spam
doc = Nokogiri::HTML::DocumentFragment.parse(self.content)
doc.css('a').each do |a|
a[:rel] = 'nofollow'
a[:target] = '_blank'
end
self.content = doc.to_s
end
# ...
end
Nokogiri 这个 gem 也可以用来写爬虫程序。
在 SEO 中,一个页面要能够起到定位的作用。在网站的主页,我们通常会把公司的 logo(这里会采用品牌的名称来作为alt
属性的值)包含在<h1>
这个标签里面。除此之外其他的所有页面都会在它们自身的页面中包含<h1>
标签以及相关文本。(代码如下,译者也不太清楚实际意义)
/app/views/layouts/_header.html.slim
header
nav
ul
li
= link_to root_path do
- if current_page?(root_url)
h1 = image_tag 'lrds_logo.svg', alt: "SEO & Ruby On Rails", height: "20px"
- else
= image_tag 'lrds_logo.svg', alt: "SEO & Ruby On Rails", height: "20px"
li = link_to "Blog", posts_path
li = link_to "Contact", contact_path
总结一下:Ruby On Rails 是一个非常灵活的快速开发框架,在它上面配置各种 SEO 相关的建议是非常简便的事情。其中有很多种因素会影响到网站自身在搜索引擎上的排名,这篇指南仅仅列出那些跟开发工作相关的建议。我们不能够忘记要为访问者提供原创,高质量并且组织有方的内容,以达到他们的需求及期望。这依旧是网站能够得到首选的关键。
请随意分享,可以在评论区进行评论或者问我们任何相关的问题,我们很乐意作答。
这里是la revanche des sites的Bastien(似乎是个法语的专门做 SEO 的网站)。
特别感谢: