127

现在有很多很酷的工具可以用来制作强大的“单页”JavaScript 网站。在我看来,通过让服务器充当 API(仅此而已)并让客户端处理所有 HTML 生成内容,这是正确的。这种“模式”的问题在于缺乏搜索引擎支持。我可以想到两个解决方案:

  1. 当用户进入网站时,让服务器完全按照客户端在导航时呈现的页面。因此,如果我直接访问,服务器将呈现与通过 pushStatehttp://example.com/my_path访问客户端相同的内容。/my_path
  2. 让服务器只为搜索引擎机器人提供一个特殊的网站。如果一个普通用户访问http://example.com/my_path服务器应该给他一个 JavaScript 重版本的网站。但是如果谷歌机器人访问,服务器应该给它一些我希望谷歌索引的内容的最小 HTML。

第一个解决方案在这里进一步讨论。我一直在做一个网站,这不是一个很好的体验。它不是 DRY,就我而言,我不得不为客户端和服务器使用两个不同的模板引擎。

我想我已经看到了一些好的 ol' Flash 网站的第二种解决方案。我比第一种方法更喜欢这种方法,并且使用服务器上的正确工具可以轻松完成。

所以我真正想知道的是以下几点:

  • 你能想出更好的解决方案吗?
  • 第二种解决方案有什么缺点?如果 Google 以某种方式发现我为 Google bot 提供的内容与普通用户不同,那么我会在搜索结果中受到惩罚吗?
4

9 回答 9

44

虽然 #2 对于作为开发人员的您来说可能“更容易”,但它只提供搜索引擎抓取。是的,如果 Google 发现您提供不同的内容,您可能会受到处罚(我不是这方面的专家,但我听说过这种情况)。

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

pushState根据我的经验,不会增加这个负担。它只会将过去的事后想法和“如果我们有时间”带到 Web 开发的最前沿。

您在选项 #1 中的描述通常是最好的方法 - 但是,与其他可访问性和 SEO 问题一样,pushState在 JavaScript 繁重的应用程序中执行此操作需要预先规划,否则将成为一个重大负担。它应该从一开始就融入页面和应用程序架构 - 改造是痛苦的,并且会导致不必要的重复。

我最近一直在pushState为几个不同的应用程序使用和 SEO,我发现我认为是一个很好的方法。它基本上遵循您的第 1 项,但说明不复制 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。当第一个视图被加载时,我没有调用渲染或让视图为我生成一个。el在视图启动并运行并且页面都是 JavaScript 之后,我有一个渲染方法可用。如果需要,这可以让我稍后重新渲染视图。

在启用 JavaScript 的情况下单击“说出我的名字”按钮会出现一个警告框。如果没有 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支持滚动的好方法。

希望有帮助。

于 2011-09-26T02:14:06.770 回答
22

我认为你需要这个:http ://code.google.com/web/ajaxcrawling/

您还可以安装一个特殊的后端,通过在服务器上运行 javascript 来“呈现”您的页面,然后将其提供给 google。

将这两件事结合起来,您就有了一个解决方案,而无需两次编程。(只要您的应用程序可以通过锚片段完全控制。)

于 2011-09-26T00:00:04.330 回答
17

所以,似乎主要的问题是干燥

  • 如果您使用 pushState,则让您的服务器为所有 url 发送相同的确切代码(不包含提供图像的文件扩展名等)“/mydir/myfile”、“/myotherdir/myotherfile”或 root“/ " -- 所有请求都收到相同的确切代码。你需要有某种 url 重写引擎。您还可以提供一小部分 html,其余的可以来自您的 CDN(使用 require.js 来管理依赖项——请参阅https://stackoverflow.com/a/13813102/1595913)。
  • (通过将链接转换为您的 url 方案并通过查询静态或动态源来测试内容是否存在来测试链接的有效性。如果它无效,则发送 404 响应。)
  • 当请求不是来自 google bot 时,您只需正常处理即可。
  • 如果请求来自 google bot,则使用 phantom.js——headless webkit 浏览器(“无头浏览器只是一个没有可视界面的全功能 Web 浏览器。”)在服务器上呈现 html 和 javascript 并发送google bot 生成的 html。当机器人解析 html 时,它可以点击服务器上的其他“pushState”链接/somepage,服务器<a href="/someotherpage">mylink</a>将 url 重写为您的应用程序文件,将其加载到 phantom.js 中,然后将生成的 html 发送到机器人,等等。 ..
  • 对于您的html,我假设您正在使用带有某种劫持的普通链接(例如,使用backbone.js https://stackoverflow.com/a/9331734/1595913
  • 为避免与任何链接混淆,请将提供 json 的 api 代码分隔到单独的子域中,例如 api.mysite.com
  • 为了提高性能,您可以在下班时间通过使用与 phantom.js 相同的机制创建页面的静态版本来提前为搜索引擎预处理您的网站页面,然后将静态页面提供给谷歌机器人。预处理可以通过一些可以解析<a>标签的简单应用程序来完成。在这种情况下,处理 404 会更容易,因为您可以简单地检查名称包含 url 路径的静态文件是否存在。
  • 如果你使用 #! 您的站点链接的 hash 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

于 2012-12-12T04:51:42.963 回答
4

如果您使用的是 Rails,请尝试poirot。这是一个让重用mustache车把模板客户端和服务器端变得非常简单的 gem。

在您的视图中创建一个文件,例如_some_thingy.html.mustache.

渲染服务器端:

<%= render :partial => 'some_thingy', object: my_model %>

将模板放在您的脑海中以供客户端使用:

<%= template_include_tag 'some_thingy' %>

渲染客户端:

html = poirot.someThingy(my_model)
于 2012-05-23T12:56:23.610 回答
3

从稍微不同的角度来看,您的第二种解决方案在可访问性方面是正确的……您将为无法使用 javascript 的用户(具有屏幕阅读器等的用户)提供替代内容。

这将自动增加搜索引擎优化的好处,并且在我看来,不会被谷歌视为一种“顽皮”的技术。

于 2011-09-25T23:59:35.687 回答
1

在服务器端使用 NodeJS,浏览您的客户端代码并通过服务器端客户端路由每个 http-request(静态 http 资源除外)uri,以提供第一个“bootsnap”(页面状态的快照)。使用类似 jsdom 的东西来处理服务器上的 jquery dom-ops。bootsnap 返回后,设置 websocket 连接。最好通过在客户端建立某种包装连接来区分 websocket 客户端和服务器端客户端(服务器端客户端可以直接与服务器通信)。我一直在做这样的事情:https ://github.com/jvanveen/rnet/

于 2013-07-26T15:40:26.523 回答
1

有趣的。我一直在寻找可行的解决方案,但这似乎很有问题。

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

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

这是我对解决问题的看法。虽然它没有被确认可以工作,但它可能会为其他开发人员提供一些见解或想法。

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

假设您的路线设置如下:

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

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

据我了解,呈现浏览器不可见的内容是一个坏主意。所以当谷歌索引它时,人们通过谷歌访问给定的页面并且没有任何内容,那么你可能会受到惩罚。想到的是,您在 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

这样,对于 Google 和任何使用 JavaScript 禁用设备的人来说,他们将看到原始/静态内容。因此,内容物理存在的,并且任何使用禁用 JavaScript 的设备的人都可以看到。

但是,当用户访问同一页面并且实际上启用JavaScript 时,该#no-js节点将被删除,因此它不会使您的应用程序混乱。然后,您的客户端框架将通过其路由器处理请求,并显示启用 JavaScript 时用户应该看到的内容。

我认为这可能是一种有效且相当容易使用的技术。尽管这可能取决于您的网站/应用程序的复杂性。

不过,如果不是,请纠正我。只是想我会分享我的想法。

于 2012-08-16T23:11:33.027 回答
0

使用Google Closure Template呈现页面。它编译为 javascript 或 java,因此很容易在客户端或服务器端呈现页面。在第一次遇到每个客户端时,呈现 html 并将 javascript 添加为标题中的链接。爬虫只会读取 html,但浏览器会执行你的脚本。来自浏览器的所有后续请求都可以针对 api 完成以最小化流量。

于 2012-05-11T22:03:28.613 回答
0

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

逻辑

  • 浏览器从服务器请求您的单页应用程序,该应用程序将从单个 index.html 文件加载。
  • 您编写一些中间服务器代码来拦截客户端请求并区分请求是来自浏览器还是某些社交爬虫机器人。
  • 如果请求来自某个爬虫机器人,请对您的后端服务器进行 API 调用,收集您需要的数据,将该数据填充到 html 元标记,并将这些标记以字符串格式返回给客户端。
  • 如果请求不是来自某个爬虫机器人,那么只需从单页应用程序的builddist文件夹返回 index.html 文件。
于 2020-10-06T12:59:54.447 回答