"单页"JS网站和SEO

127
现在有很多酷炫的工具可以制作强大的“单页”JavaScript网站。在我看来,正确的方法是让服务器充当API(仅此而已),并让客户端处理所有HTML生成内容的工作。这种“模式”的问题在于缺乏搜索引擎支持。我能想到两个解决方案:
  1. 当用户进入网站时,让服务器呈现与客户端在导航时完全相同的页面。所以如果我直接访问http://example.com/my_path,服务器会呈现与我通过pushState访问/my_path时客户端将要呈现的内容相同。
  2. 让服务器为搜索引擎机器人提供一个特殊的网站。如果普通用户访问http://example.com/my_path,服务器应该给他一个JavaScript重型版本的网站。但是如果Google机器人访问,则服务器应该给它一些仅包含我想让Google索引的最小HTML内容。
第一种解决方案在这里进一步讨论。我一直在做这样的网站,但体验并不好。这不是DRY,并且在我的情况下,我必须为客户端和服务器使用两种不同的模板引擎。
我认为我曾经在一些传统的Flash网站上看到过第二种解决方案。与第一种解决方案相比,我更喜欢这种方法,并且使用适当的服务器工具可以轻松完成。
所以我真正想知道的是:
  • 您能想到更好的解决方案吗?
  • 第二种解决方案有什么缺点?如果Google以某种方式发现我没有为Google机器人提供与普通用户完全相同的内容,那么我是否会受到搜索结果的惩罚?
9个回答

44

虽然选项#2对开发者来说可能更"容易",但它只提供搜索引擎爬行。如果Google发现您提供了不同的内容,您可能会受到惩罚(我不是专家,但我听说过这种情况发生)。

SEO和可访问性(不仅针对残障人士,而且还包括通过移动设备、触屏设备和其他非标准计算/互联网启用平台进行可访问性)都有着相似的基本理念:语义丰富的标记是“可访问的”(即可以被访问、查看、阅读、处理或以其他方式使用),适用于所有这些不同的浏览器。屏幕阅读器、搜索引擎爬虫或启用JavaScript的用户都应该能够使用/索引/理解您站点的核心功能。

pushState从我的经验来看并不会增加这个负担。它只是把之前被忽视的和“如果我们有时间”的东西带到了Web开发的前沿。

你描述的第一种选择通常是最好的选择-但是,像其他可访问性和SEO问题一样,在JavaScript重度应用程序中使用pushState需要事先规划,否则它将成为一个重大负担。它应该从一开始就融入到页面和应用程序架构中--后期改装很痛苦,并会导致比必要的更多的重复。

我最近在几个不同的应用程序中使用pushState和SEO,我发现了一种我认为不错的方法。它基本上遵循了你的第一条建议,但考虑到不重复html/模板。

这些信息大部分可以在以下两篇博客文章中找到:

http://lostechies.com/derickbailey/2011/09/06/test-driving-backbone-views-with-jquery-templates-the-jasmine-gem-and-jasmine-jquery/

http://lostechies.com/derickbailey/2011/06/22/rendering-a-rails-partial-as-a-jquery-template/

其实它的核心是我使用ERB或HAML模板(在Ruby on Rails、Sinatra等下运行)进行服务器端渲染,并创建Backbone可以使用的客户端模板,以及我的Jasmine JavaScript规范。这样可以消除服务器端和客户端之间的标记重复。

从那里开始,您需要采取一些额外的步骤,使JavaScript能够处理服务器渲染的HTML--真正的渐进增强,将传递的语义标记与JavaScript相结合。

例如,我正在使用 pushState 构建一个图片库应用程序。如果您从服务器请求 /images/1,它将在服务器上呈现整个图像库并将所有HTML、CSS和JavaScript发送到您的浏览器。如果您禁用了JavaScript,它仍然可以正常工作。您采取的每个操作都会向服务器请求不同的URL,服务器将为您的浏览器呈现所有标记。但是,如果启用了JavaScript,JavaScript 将接管已经呈现的 HTML ,以及由服务器生成的一些变量,并从那里接管。
<form id="foo">
  Name: <input id="name"><button id="say">Say My Name!</button>
</form>

服务器呈现后,JavaScript 将会获取它(在此示例中使用 Backbone.js 视图)。

FooView = Backbone.View.extend({
  events: {
    "change #name": "setName",
    "click #say": "sayName"
  },

  setName: function(e){
    var name = $(e.currentTarget).val();
    this.model.set({name: name});
  },

  sayName: function(e){
    e.preventDefault();
    var name = this.model.get("name");
    alert("Hello " + name);
  },

  render: function(){
    // do some rendering here, for when this is just running JavaScript
  }
});

$(function(){
  var model = new MyModel();
  var view = new FooView({
    model: model,
    el: $("#foo")
  });
});

这是一个非常简单的示例,但我认为它表达了重点。

当页面加载后,我实例化视图时,将服务器呈现的表单的现有内容作为视图的el提供给视图实例。 在加载第一个视图时,我不会调用render或让视图为我生成el。 在视图已启动并且页面全部都是JavaScript之后,我可以使用可用的render方法重新渲染视图,以便稍后再次需要。

启用JavaScript后,单击“Say My Name”按钮将导致弹出警告框。 没有JavaScript,则会将其提交到服务器,并且服务器可以将名称呈现到某个html元素中。

编辑

考虑一个更复杂的示例,在此示例中您需要附加列表(请参见下面的评论)

假设您在<ul>标记中拥有用户列表。 当浏览器发出请求时,服务器呈现了此列表,结果如下所示:

<ul id="user-list">
  <li data-id="1">Bob
  <li data-id="2">Mary
  <li data-id="3">Frank
  <li data-id="4">Jane
</ul>

现在你需要遍历这个列表,并将Backbone视图和模型附加到每个<li>项目上。通过使用data-id属性,您可以轻松地找到每个标记来自哪个模型。然后,您需要一个集合视图和项视图,它足够智能,可以附加到此HTML。

UserListView = Backbone.View.extend({
  attach: function(){
    this.el = $("#user-list");
    this.$("li").each(function(index){
      var userEl = $(this);
      var id = userEl.attr("data-id");
      var user = this.collection.get(id);
      new UserView({
        model: user,
        el: userEl
      });
    });
  }
});

UserView = Backbone.View.extend({
  initialize: function(){
    this.model.bind("change:name", this.updateName, this);
  },

  updateName: function(model, val){
    this.el.text(val);
  }
});

var userData = {...};
var userList = new UserCollection(userData);
var userListView = new UserListView({collection: userList});
userListView.attach();
在这个例子中,UserListView 将循环遍历所有 <li> 标签,并为每个标签附加一个正确的模型视图对象。它设置了该模型名称更改事件的事件处理程序,并在更改发生时更新元素的显示文本。
这种过程将服务器呈现的HTML交由JavaScript接管运行,对于SEO、可访问性和pushState支持等方面有很大帮助。
希望能对您有所帮助。

我理解你的意思,但有趣的是在“JavaScript 接管后”渲染是如何完成的。在更复杂的示例中,您可能需要在客户端上使用未编译的模板,通过循环遍历用户数组来构建列表。每当用户模型更改时,视图都会重新呈现。如果不重复使用模板(并且不要求服务器为客户端呈现视图),您将如何实现? - user544941
我提供的两篇博客文章可以共同向您展示如何拥有可在客户端和服务器上使用的模板 - 无需重复。如果您希望页面可访问且对SEO友好,则服务器需要呈现整个页面。我已更新我的答案,包括一个更复杂的示例,演示如何附加到由服务器呈现的用户列表。 - Derick Bailey

22
我认为你需要这个:http://code.google.com/web/ajaxcrawling/ 你也可以安装一个特殊的后端,通过在服务器上运行JavaScript来“呈现”页面,然后将其提供给Google。
将两者结合起来,如果你的应用程序完全可通过锚点片段控制,就可以实现不重复编程的解决方案。

实际上,这不是我要找的。那些只是第一个解决方案的一些变体,正如我所提到的,我对这种方法并不满意。 - user544941
2
你没有完整地阅读我的回答。你还使用了一个特殊的后端来为你呈现JavaScript - 你不需要重复编写代码。 - Ariel
是的,我确实读过那个。但如果我理解正确的话,那将是一个非常复杂的程序,因为它必须模拟触发pushState的每个动作。或者,我可以直接将这些动作提供给它,但这样我们就不再DRY了。 - user544941
2
我认为它基本上是一个没有前端的浏览器。但是,是的,您必须使程序完全可从锚点片段控制。您还需要确保所有链接中都有适当的片段,以及或者替代onClicks。 - Ariel

17
因此,主要问题似乎在于保持DRY(Don't Repeat Yourself)原则。
  • 如果您正在使用pushState,请让服务器为所有URL发送完全相同的代码(不包含用于提供图像等文件扩展名的URL),如“/ mydir / myfile”,“/ myotherdir / myotherfile”或根路径“/”--所有请求都接收完全相同的代码。您需要一种url重写引擎。您还可以提供少量HTML,其余内容可以来自CDN(使用require.js管理依赖项--请参见https://dev59.com/7Gct5IYBdhLWcg3wIqDW#13813102)。
  • (通过将链接转换为您的URL方案并通过查询静态或动态源的内容存在性来测试链接的有效性。如果无效,则发送404响应。)
  • 当请求不来自Google机器人时,您只需正常处理。
  • 如果请求来自Google机器人,则使用Phantom.js--无头Webkit浏览器(“无头浏览器只是带有没有可视界面的完整功能的网络浏览器。”)在服务器上呈现HTML和JavaScript,并向Google机器人发送生成的HTML。当机器人解析HTML时,它可以访问服务器上的其他“pushState”链接/ somepage:<a href="/someotherpage">mylink</a>,服务器将URL重写为您的应用文件,在Phantom.js中加载该文件,并将生成的HTML发送给机器人,以此类推......
  • 对于您的HTML,我假设您正在使用带有某种劫持(例如使用backbone.js https://dev59.com/gGox5IYBdhLWcg3wQSEO#9331734)的常规链接。
  • 为了避免任何链接混淆,将提供JSON的API代码分离到单独的子域名中,例如api.mysite.com。
  • 为了提高性能,可以在非工作时间预处理站点页面以供搜索引擎使用,方法是使用Phantom.js创建页面的静态版本,并因此向Google机器人提供静态页面。可以使用一些简单的应用程序解析<a>标记来进行预处理。在这种情况下,处理404更容易,因为您可以简单地检查具有包含URL路径的名称的静态文件的存在。
  • 如果您使用#!哈希bang语法用于站点链接,则应用类似的方案,只是重写URL服务器引擎将查找URL中的_escaped_fragment_,并将URL格式化为您的URL方案。
  • GitHub上有几个Node.js与Phantom.js的集成,您可以使用Node.js作为Web服务器生成HTML输出。

以下是使用Phantom.js进行SEO的几个示例:

http://backbonetutorials.com/seo-for-single-page-apps/

http://thedigitalself.com/blog/seo-and-javascript-with-phantomjs-server-side-rendering


4
如果您使用的是Rails,请尝试poirot。这是一个gem,使得在客户端和服务器端重用mustachehandlebars模板变得非常简单。
在您的视图中创建一个文件,如_some_thingy.html.mustache
服务器端渲染:
<%= render :partial => 'some_thingy', object: my_model %>

将模板放入头部以供客户端使用:
<%= template_include_tag 'some_thingy' %>

将客户端化:
html = poirot.someThingy(my_model)

3
采用另一种角度来看,从 可访问性 方面考虑,你的第二个解决方案是正确的...你将为无法使用JavaScript(如屏幕阅读器等)的用户提供替代内容。

这将自动增加SEO的好处,并且在我看来,Google不会认为这是一种“不良”技术手段。


有人证明你错了吗?这条评论已经发布了一段时间。 - jkulak

1

在服务器端使用NodeJS,在客户端使用browserify对代码进行处理,并通过服务器端客户端路由每个http请求的uri(除了静态http资源)来提供第一个“bootsnap”(页面状态的快照)。使用类似jsdom的工具来处理服务器上的jquery dom操作。在返回bootsnap之后,建立websocket连接。最好通过在客户端建立某种包装连接来区分websocket客户端和服务器端客户端(服务器端客户端可以直接与服务器通信)。我一直在研究这样的东西: https://github.com/jvanveen/rnet/


1

有趣。我一直在寻找可行的解决方案,但似乎问题相当棘手。

实际上,我更倾向于您的第二种方法:

让服务器为搜索引擎提供一个特殊的网站。如果普通用户访问http://example.com/my_path,服务器应该给他一个JavaScript重型版本的网站。但是如果Google机器人访问,服务器应该给它一些最小的HTML和我想要Google索引的内容。

这是我解决问题的看法。虽然尚未确认是否有效,但可能会为其他开发人员提供一些见解或想法。

假设您正在使用支持“推送状态”功能的JS框架,并且您的后端框架是Ruby on Rails。您拥有一个简单的博客网站,希望搜索引擎可以索引所有文章的indexshow页面。

假设您的路由设置如下:

resources :articles
match "*path", "main#index"

确保每个服务器端控制器都呈现与您的客户端框架运行所需的相同模板(html/css/javascript等)。 如果请求中没有匹配任何控制器(在此示例中,我们仅为ArticlesController设置了一组RESTful操作),则只需匹配其他任何内容并呈现模板,让客户端框架处理路由。 按下控制器和按下通配符匹配器之间唯一的区别是能够根据请求的URL呈现内容以供禁用JavaScript的设备使用。

据我所知,呈现对浏览器不可见的内容是一个坏主意。 因此,当Google索引它时,人们通过Google访问给定页面时没有任何内容,那么您可能会受到惩罚。 我想到的是,在CSS中将内容呈现在div节点中,然后将其display:none

但是,我非常确定如果您只是这样做:

<div id="no-js">
  <h1><%= @article.title %></h1>
  <p><%= @article.description %></p>
  <p><%= @article.content %></p>
</div>

然后使用JavaScript,当JavaScript被禁用的设备打开页面时不会运行:

$("#no-js").remove() # jQuery

这样一来,对于谷歌和任何禁用JavaScript的设备用户,他们将看到原始/静态内容。因此,该内容实际上是存在的,并且对于任何禁用JavaScript的设备用户都是可见的。
但是,当用户访问同一页面并且实际启用了JavaScript时,#no-js节点将被删除,以便不会混乱您的应用程序。然后,您的客户端框架将通过其路由处理请求,并在启用JavaScript时显示用户应该看到的内容。
我认为这可能是一种有效且相当容易使用的技术。尽管这可能取决于您的网站/应用程序的复杂性。
如果我有所错误,请纠正我。只是想分享我的想法。

1
如果您先显示内容,稍后再将其删除,那么最可能的情况是最终用户会在浏览器中看到内容闪烁/闪烁 :) 特别是如果它是缓慢的浏览器,您尝试显示/删除的HTML内容非常庞大,并且在JS代码加载和执行之前存在一些延迟。 您认为呢? - Evereq

0
使用Google Closure Template来渲染页面。它可以编译成JavaScript或Java,因此很容易在客户端或服务器端渲染页面。在与每个客户端的第一次接触中,渲染HTML并将JavaScript作为链接添加到头部。爬虫只会读取HTML,但浏览器将执行您的脚本。所有后续来自浏览器的请求都可以针对API进行,以最小化流量。

0

这可能会对你有帮助:https://github.com/sharjeel619/SPA-SEO

逻辑

  • 浏览器从服务器请求你的单页应用,该应用将从单个 index.html 文件加载。
  • 你编写了一些中间服务器代码来拦截客户端请求,并区分请求是来自浏览器还是某些社交爬虫机器人。
  • 如果请求来自某些爬虫机器人,则向后端服务器发出 API 调用,收集所需数据,填充 html meta 标记并以字符串格式将这些标记返回给客户端。
  • 如果请求没有来自某些爬虫机器人,则从单页应用的 builddist 文件夹中简单地返回 index.html 文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接