“单页”JS网站和SEO
现在有很多很酷的工具可以制作function强大的“单页”JavaScript网站。 在我看来,这是通过让服务器充当一个API(而不是其他任何东西)并让客户端处理所有的HTML生成东西来完成的。 这种“模式”的问题是缺乏search引擎支持。 我可以想到两个解决scheme:
- 当用户进入网站时,让服务器完全按照客户端的导航来呈现页面。 所以如果我直接去
http://example.com/my_path
那么如果我通过pushState去到/my_path
,服务器将会呈现和客户端一样的东西。 - 让服务器只为search引擎机器人提供一个特殊的网站。 如果一个普通用户访问
http://example.com/my_path
,服务器应该给他一个JavaScript网站的繁重版本。 但是,如果谷歌机器人访问,服务器应该给它一些最小的HTML与我要谷歌索引的内容。
第一个解决scheme在这里进一步讨论。 我一直在做这个网站,这不是一个很好的经验。 这不是干的,在我的情况下,我不得不为客户端和服务器使用两个不同的模板引擎。
我想我已经看到了一些好的Flash网站的第二个解决scheme。 我比第一个更喜欢这种方法,并且在服务器上使用正确的工具,可以非常轻松地完成。
所以我真正想知道的是以下几点:
- 你能想到更好的解决scheme吗?
- 第二种解决scheme有什么缺点? 如果Google以某种方式发现我没有像普通用户那样为Google bot提供完全相同的内容,那么我是否会在search结果中受到惩罚?
虽然#2对于开发者来说可能“更容易”,但它只提供search引擎抓取。 是的,如果谷歌发现你的服务不同的内容,你可能会受到惩罚(我不是一个专家,但我听说过这种情况发生)。
search引擎优化和可访问性(不只是残疾人,但通过移动设备,触摸屏设备,以及其他非标准计算/互联网平台)都具有类似的基本理念:语义丰富的标记是“可访问的”被访问,查看,读取,处理或以其他方式使用)到所有这些不同的浏览器。 屏幕阅读器,search引擎抓取工具或启用了JavaScript的用户应该都能够毫无问题地使用/索引/理解您网站的核心function。
pushState
不会增加这个负担,以我的经验。 它只是把曾经是事后的想法和“如果我们有时间”带到networking发展的最前沿。
在选项#1中描述的通常是最好的方法 – 但像其他的可访问性和search引擎优化问题一样,在JavaScript应用程序中使用pushState
做这件事需要pushState
规划,否则将会成为一个重大的负担。 它应该从一开始就被烘焙到页面和应用程序体系结构中 – 加装是痛苦的,会导致比必要的更多的重复。
最近我一直在使用pushState
和SEO来处理几个不同的应用程序,我发现我认为是一个好方法。 它基本上跟随您的项目#1,但帐户不重复的HTML /模板。
大部分的信息可以在这两篇博文中find:
和
http://lostechies.com/derickbailey/2011/06/22/rendering-a-rails-partial-as-a-jquery-template/
其要点是我使用ERB或HAML模板(运行Ruby on Rails,Sinatra等)来进行服务器端渲染,并创buildBackbone可以使用的客户端模板以及Jasmine JavaScript规范。 这样就消除了服务器端和客户端之间的标记重复。
从那里,你需要采取一些额外的步骤,让你的JavaScript与服务器呈现的HTML一起工作 – 真正的渐进增强; 采用已经交付的语义标记并使用JavaScript进行增强。
例如,我正在用pushState
构build一个图像库应用pushState
。 如果您从服务器请求http://img.dovov.com1
,它将呈现服务器上的整个图像库,并将所有的HTML,CSS和JavaScript下载到您的浏览器。 如果你禁用了JavaScript,它将会工作得很好。 您执行的每个操作都将从服务器请求一个不同的URL,服务器将呈现浏览器的所有标记。 但是,如果您启用了JavaScript,则JavaScript将拾取已经呈现的HTML以及由服务器生成的一些variables,并从那里接pipe。
这是一个例子:
<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的“说出我的名字”button将导致一个警告框。 没有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>
现在,您需要遍历这个列表,并为每个<li>
项目附加一个Backbone视图和模型。 通过使用data-id
属性,您可以轻松find每个标签所来自的模型。 然后,您需要一个足够聪明的集合视图和项目视图来将自己附加到这个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>
标签,并为每个标签附加一个正确模型的视图对象。 它为模型的名称更改事件设置事件处理程序,并在发生更改时更新元素的显示文本。
这种types的过程,采取服务器呈现并让我的JavaScript接pipe并运行它的html,是让SEO,辅助function和pushState
支持滚动的好方法。
希望有所帮助。
我认为你需要这个: http : //code.google.com/web/ajaxcrawling/
您还可以安装一个特殊的后端,通过在服务器上运行JavaScript“呈现”您的页面,然后将其提供给谷歌。
结合这两件事情,你有一个解决scheme,没有两次编程的事情。 (只要你的应用程序完全可以通过锚点片段来控制)。
所以,主要担心的是DRY
- 如果您使用的是pushState,请让您的服务器为所有的URL(不包含用于提供图片的文件扩展名)发送相同的确切代码“/ mydir / myfile”,“/ myotherdir / myotherfile”或root“ “ – 所有请求都会收到相同的确切代码。 你需要有一些types的url重写引擎。 您也可以提供一小部分的HTML,其余的可以来自您的CDN(使用require.js来pipe理依赖项 – 请参阅https://stackoverflow.com/a/13813102/1595913 )。
- (通过将链接转换为您的urlscheme并通过查询静态或dynamic源来testing内容是否存在来testing链接的有效性,如果无效,则发送404响应。
- 当请求不是从谷歌机器人,你只是正常处理。
- 如果请求是来自谷歌机器人,请使用phantom.js – 无头webkit浏览器( “无头浏览器只是一个没有可视化界面的全function的Web浏览器” )来在服务器上呈现html和javascript,并发送谷歌机器人由此产生的HTML。 当botparsinghtml时,它可以在服务器
<a href="/someotherpage">mylink</a>
上打开其他“pushState”链接/ somepage,服务器将url重新写入应用程序文件,并将其加载到phantom.js中并将生成的html发送到机器人,等等… - 对于你的HTML我假设你正在使用正常的链接与某种劫持(例如使用backbone.js https://stackoverflow.com/a/9331734/1595913 )
- 为了避免混淆与任何链接分开您的api代码,提供json到一个单独的子域名,例如api.mysite.com
- 为了提高性能,您可以通过使用与phantom.js相同的机制创build页面的静态版本,从而在下class时间提前为search引擎预处理您的网站页面,从而将静态页面提供给Google机器人。 预处理可以使用一些简单的应用程序来parsing
<a>
标签。 在这种情况下,处理404比较容易,因为您可以简单地检查包含urlpath的名称是否存在静态文件。 - 如果你使用#! 哈希爆炸语法为您的网站链接类似的情况适用,除了重写URL服务器引擎将寻找URL中的_escaped_fragment_并将格式化URL到您的urlscheme。
- 在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
如果您使用Rails,请尝试poirot 。 这是一个gem,使它死了很简单,重用胡子或句柄模板客户端和服务器端。
在您的视图中创build一个文件,如_some_thingy.html.mustache
。
渲染服务器端:
<%= render :partial => 'some_thingy', object: my_model %>
把模板放在客户端使用:
<%= template_include_tag 'some_thingy' %>
Rendre客户端:
html = poirot.someThingy(my_model)
要采取一个稍微不同的angular度,你的第二个解决scheme将是无障碍方面正确的…你将提供替代内容的用户谁不能使用JavaScript(屏幕阅读器等)。
这将自动增加search引擎优化的好处,在我看来,不会被视为谷歌“淘气”的技术。
有趣。 我一直在寻找可行的解决scheme,但似乎是相当成问题的。
我其实更倾向于你的第二种方法:
让服务器只为search引擎机器人提供一个特殊的网站。 如果一个普通用户访问http://example.com/my_path ,服务器应该给他一个JavaScript网站的繁重版本。 但是,如果谷歌机器人访问,服务器应该给它一些最小的HTML与我要谷歌索引的内容。
这是我解决问题的方法。 虽然它没有被证实工作,它可能会为其他开发人员提供一些见解或想法。
假设您使用支持“推送状态”function的JS框架,而后端框架是Ruby on Rails。 你有一个简单的博客网站,你希望search引擎索引所有的文章index
和show
页面。
假设你有这样的路线:
resources :articles match "*path", "main#index"
确保每个服务器端控制器呈现与客户端框架运行相同的模板(html / css / javascript / etc)。 如果没有一个控制器在请求中匹配(在这个例子中,我们只对ArticlesController
有一个REST风格的操作),那么只需匹配其他任何东西,然后渲染模板,让客户端框架处理路由。 点击控制器和点击通配符匹配器的唯一区别就是能够根据向JavaScript禁用的设备请求的URL呈现内容。
根据我的理解,渲染浏览器不可见的内容是一个坏主意。 因此,当Google进行索引时,人们会通过Google访问某个页面,而且没有任何内容,那么您可能会受到处罚。 想到的是,你呈现在你display: none
的div
节点的内容display: none
CSS中display: none
。
但是,我敢肯定,如果你只是这样做并不重要:
<div id="no-js"> <h1><%= @article.title %></h1> <p><%= @article.description %></p> <p><%= @article.content %></p> </div>
然后使用JavaScript,当JavaScript禁用的设备打开页面时,JavaScript不会运行:
$("#no-js").remove() # jQuery
这样,对于Google,以及任何使用JavaScript的设备,他们都会看到原始/静态内容。 所以这些内容就在那里,任何有JavaScript禁用的设备的人都可以看到。
但是,当用户访问相同的页面并且实际上启用了JavaScript时, #no-js
节点将被删除,所以它不会混淆您的应用程序。 然后,您的客户端框架将通过路由器处理请求,并显示启用JavaScript时用户应该看到的内容。
我认为这可能是一个有效的,相当简单的使用技巧。 虽然这可能取决于您的网站/应用程序的复杂性。
虽然,如果不是,请纠正我。 只是想我会分享我的想法。
在服务器端使用NodeJS,浏览您的客户端代码,并通过服务器端客户端路由每个http请求(除了静态http资源)uri以提供第一个“bootsnap”(它是状态页面的快照)。 使用类似jsdom的东西来处理服务器上的jquery dom-ops。 bootnap返回后,设置websocket连接。 可能最好通过在客户端(服务器端客户端可以直接与服务器通信)上进行某种包装连接来区分websocket客户端和服务器端客户端。 我一直在做这样的事情: https : //github.com/jvanveen/rnet/
使用Google Closure模板呈现网页。 它编译为JavaScript或Java,所以很容易在客户端或服务器端渲染页面。 在第一次遇到每个客户端,呈现HTML和添加JavaScript作为链接在标题。 爬虫只会读取html,但浏览器将执行您的脚本。 所有后续的浏览器请求都可以通过api来完成,以尽量减lessstream量。