移动环境下的 SEO

真的有很久很久没来理这个博客,距离上一篇文章的发布日期是两年前。 毕业工作后来了豆瓣,最近对移动环境(主要是浏览器及微信)的 SEO 相关进行了下研究,正好在这里分享一下。

以下,Enjoy。


1. SEO:Search Engine Optimazition

产品:搜索引擎优化,促进内容网站信息分享/传播,提高目标转化率,留存率

工程:为各类页面实现统一的信息分享、传播,提高搜索排名(search ranking)

个人觉得:

  • 豆瓣是一个内容网站,拥有一些 UGC 优质内容,SEO 间接帮助用户分享。 且分享率越高,也会提高搜索排名。
  • M 站的目前面向 人(User) 的 UX 比较完善,但是仍需要丰富一些细节,让 Search Engine 的 UX 更好,更好地理解 M 站。

2. 前端在 SEO 中的位置

  • 帮助产品进行TDK (Title / Description / Keyword)优化
  • 添加一些 Social Meta Tag (Facebook / Twitter / G+ / WeChat / Weibo)
  • 页面内容优化(HTML5 标签语义化、唯一的 H1 标题、img 设置 alt 属性,不需要爬虫跟踪的链接加 nofollow
  • URL 优化 (canonical,标签表示页面的唯一性)
    • 在搜索引擎里,只有链接完全一样,才会认为是同一个链接,如果链接带上参数,虽然访问到的内容还是一样,但是在搜索引擎看来确是不同的链接,页面抓取也会出现多次,从而导致多个链接,内容一样。
    • 因此 URL 中最好是不要带上查询参数。但为了产品统计数据,总需要 ?from=xxx&refer=xxx等参数,所以使用 canonical标签来确保链接唯一性,避免权重分散、流失。
    • 比如 m.douban.com/group/topics[?start=xxx]均视为同一个页面这里的 ?start=xxx是一个分页查询参数,所以为了确保这些页面都能继承 m.douban.group/toics 这个 url 的权重,在head中增加了canonical标签
  • Meta robots
  • Sitemap

3. 具体实现

统一 SNS 抓取

提供一个 mako 函数,统一分享的标题、图片、描述

  • Open Graph
  • Twitter Card

实现统一的分享接口

可变的图片、标题、描述

  • 微信 (iOS、Android 系统分享菜单、微信内置分享)

    • 外部调用分享可能会抓取标题、第一个图片地址(Open Graph)
    • 调用内置分享可用第三个链接 (API 跨子域)
  • 微博

参考链接:

https://developers.facebook.com/docs/sharing/opengraph

https://dev.twitter.com/cards/overview

http://github.intra.douban.com/frodo/Talion/blob/master/views/j/wechat/init.py

— From Douban FE Dropbox

实现方案

1. 增加了 seo_meta() Mako widget

http://github.intra.douban.com/frodo/Talion/blob/master/templates/card/widgets.html#L527

<%def name="seo_meta(title, desc, image=None, url=None, type=None, rating_count=None, rating_val=None, wechat_timeline_title=None,wechat_chat_title=None, wechat_desc=None, wechat_image=None)">
    <%
        from douban.image.qiniu import qiniu_proxy_url

        default_title = '豆瓣'
        default_desc = '读书、看电影、涨知识、学穿搭...,加入兴趣小组,获得达人们的高质量生活经验,找到有相同爱好的小伙伴。'
        default_image = static('/pics/icon/m_logo_180.png')
        default_url = 'http://m.douban.com/'
        default_type = 'article'
        image = qiniu_proxy_url(image, 300, 300, mode='1') if image else ''
    %>
    <!-- Schema.org markup for Google+ -->
    <meta itemprop="name" content="${ title or default_title }">
    <meta itemprop="description" content="${ desc or default_desc }">
    <meta itemprop="image" content="${ image or default_image }">
    % if rating_count:
    <meta itemprop="reviewCount" content="${ rating_count }">
    % endif
    % if rating_val:
    <meta itemprop="ratingValue" content="${ rating_val }">
    % endif
    <!-- Twitter meta -->
    <meta name="twitter:card" content="summary" />
    <!-- Open Graph meta -->
    <meta property="og:title" content="${ title or default_title }" />
    <meta property="og:description" content="${ desc or default_desc }" />
    <meta property="og:site_name" content="豆瓣(手机版)" />
    <meta property="og:url" content="${ url or default_url }" />
    <meta property="og:image" content="${ image or default_image }" />
    <meta property="og:image:type" content="image/png" />
    <meta property="og:image:width" content="300" />
    <meta property="og:image:height" content="300" />
    <meta property="og:type" content="${ type or default_type }" />
    <!-- Wechat meta -->
    <meta property="weixin:timeline_title" content="${ wechat_timeline_title or '' }" />
    <meta property="weixin:chat_title" content="${ wechat_chat_title or '' }" />
    <meta property="weixin:description" content="${ wechat_desc or '' }" />
    <meta property="weixin:image" content="${ image or default_image }" />
    <%block filter="collect_js">
    ;(function () {
        window.setMeta = function (name, val) {
          var meta = document.querySelectorAll('meta[property="' + name + '"], meta[name="' + name + '"]')
          if (!meta.length) {
            meta = document.createElement('meta')
            meta.name = name
            document.head.appendChild(meta)
          }
         meta[0].content = val || ''
        }
        window.getMeta = function (name) {
          var meta = document.querySelectorAll('meta[property="' + name + '"], meta[name="' + name + '"]')
          if (!meta.length) {
            return ''
          } else {
            return meta[0].content
          }
        }
        setMeta('og:url', location.href)
        !getMeta('weixin:chat_title') && setMeta('weixin:chat_title', document.title)
        !getMeta('weixin:timeline_title') && setMeta('weixin:timeline_title', document.title)
        !getMeta('weixin:description') && setMeta('weixin:description', getMeta('og:description'))
    })();
    </%block>
</%def>

2. 给 m 站大部分页面增加了 SEO Meta Tags

之前分享出去抓不到自定义的 description 和 image ,现默认分享到 weixin 的 title 、 description 、image 值都和 Open Graph 一样,也可以自定义。

默认情况下

  • og:titleog:description 和之前的 title, description 一样;
  • og:url : 当前页面 url;
  • og:image
    • 小组帖子页和单个日记页如有图片,则为第一张图片的 src url,没有的话为现在默认的分享 icon src url;
    • 条目页的均为该条目的 image src url;
    • 相册页为该相册第一张 image src;
    • 豆列、标签页为默认的分享 icon src
  • Hashtag 页

    • image: 运营头图 || 豆 icon;
    • title: { hashtag_name} - 豆瓣;
    • description: 运营描述 || '有关#{ hashtag_name }#的话题讨论,豆瓣用户参与的精彩话题';
  • 单条广播页

    • 微信聊天、朋友圈:image = 广播第一张图 || 豆 icon;
    • 微信聊天页: title = XXX 的豆瓣广播; description = 广播文字内容的前十五个字 || 默认的 m 站描述;
    • 微信朋友圈: title = 广播文字内容前十五个字 || XXX 的豆瓣广播
  • 关于 image 图片规格:

    为了使抓取的图片更优雅,不被压缩变形,使用「七牛图片服务」进行剪裁。

    默认 og:image 等 image 均会被以 width = height = 300 进行处理。

效果截屏:

http://github.intra.douban.com/frodo/Talion/pull/253

3. 统一分享接口(微信重度使用患者)


wx.onMenuShareTimeline($.extend({}, shareData,{
    title: window.getMeta('weixin:timeline_title') || document.title,
    success: function () {
        wxShareStat.target = 'timeline';
        $.get('/j/wechat/shared', wxShareStat);
    },
    cancel: function () {}
}));

wx.onMenuShareAppMessage($.extend({}, shareData,{
    title: window.getMeta('weixin:chat_title') || document.title,
    desc: window.getMeta('weixin:description') || window.getMeta('og:description'),
    success: function () {
        wxShareStat.target = 'friend';
        $.get('/j/wechat/shared', wxShareStat);
    },
    cancel: function () {}
}));

结合使用 meta[name="weixin:image"]

<meta property="weixin:timeline_title" content="${ wechat_timeline_title or '' }" />
<meta property="weixin:chat_title" content="${ wechat_chat_title or '' }" />
<meta property="weixin:description" content="${ wechat_desc or '' }" />
<meta property="weixin:image" content="${ image or default_image }" />
  • 自定义微信分享到聊天、朋友圈的显示内容,文案setMeta(), getMeta()

  • Tips: 如果没有设置 wx.onMenuShareTimeline()/ wx.onMenuShareAppMessage() 中的 title / imgUrl / desc 参数,微信内置浏览器默认抓取对应的og:xxx 相关。

  • Bugs: 通过 Safari Extension 分享出来的永远抓的是 apple-touch-icon 😂

<link rel="apple-touch-icon-precomposed" href="https://img3.doubanio.com/f/talion/315ba4fc82e5cdb5c50ef1d9f88c8aa2c5c0c4ba/pics/icon/m_logo_76.png">

微信分享效果图: after_seo_1 after_seo_2 after_seo_3

Further Reading & Resource

  1. Sitemap & Deadlink
  2. Social Meta Tag Cheatsheet
  3. Google SEO TOP1: Baidu Baike 😂
  4. Apple Safari supported meta tags
  5. Google SEO search-engine-optimization-starter-guide
  6. Google 结构化数据标记辅助工具
  7. SEO Tutorial
  8. 京东 SEO
  9. SEO Tools

cool4ZBL

👻 Web Developer. 👾 Front End. 🤖 DevOps. 🤓 Tool Enthusiast.