Silverlight 4+RIA Services–搜索引擎优化(SEO)
继续我们的系列文章,接着来看一下Silverlight和SEO。互联网上大部分流量都是搜索驱动的。搜索引擎通常是很多用户在公共互联网上的第 一站,企业环境上的情况也在朝这个趋势发展。搜索也是很多广告获得收益的关键因素。所以无须多讲,SEO非常重要。但对于Silverlight程序来 说,很多有意思的内容都是动态生成的,怎么做SEO呢?接下来我会给大家展示一个只需耗费最小工作的应用模式来实现Silverlight应用程序的 SEO优化。
要实现Silverlight应用程序的SEO优化,需要按照以下三个有趣而又简单的步骤来实施:
- 步骤1:做好重要的页面的深层链接
- 步骤2:使用网站地图(Sitemap)让搜索引擎知道你网站的所有深层链接
- 步骤3:对于重要内容,提供一个备选版本
下面我会用例子来讲解以上这三个步骤的具体含义。我会继续沿用我在PDC209上的“美食浏览器”的演示。你可能需要看一下我之前的演示(PDC09 Talk: Building Amazing Business Applications with Silverlight 4, RIA Services and Visual Studio 2010),这样可以在正式开始下面的讲解之前,能够熟悉一些背景。
步骤1: 做好重要的页面的深层链接
对 于你网站上的任何内容,如果你想让搜索引擎能够爬取到,那么必须给这份内容起一个独特的Url。例如如果我想在Bing(或者Google,或者其他搜索 引擎)上搜索“乡村炸牛排(Country Fried Steak)”,能看到我网站上这个列举了很多乡村炸牛排图片的页面,那么我需要为这个页面提供一个Url,从而引导你来到这个页面。 http://foo.com/foodieExplorer.aspx这个Url还不够好,我需要提供一个类似于http://foo.com /foodieExplorer.aspx?food=CountryFriedsteak这样的Url。注意,使用这种格式的Url还有很多其他的好 处,例如用户可以在Twitter上传播,通过电子邮件或者即时通讯软件去讨论这些内容。
幸运的是,Silverlight的导航特性使到实现内容的深层链接变得非常简单。我们来看看具体是怎么做的。
我 们首先需要做的就是提供一个Url来唯一标识一个特定的餐馆,或者一个特定的餐馆加特定的美食。出于SEO或者用户友好的角度考虑,我们希望Url的格式 类似于http://www.hanselman.com/abrams/restaurant/25/plate/4,这表示餐馆编号=25,美食编 号=4。要实现这个,我们需要在Web工程的global.aspx文件中定义导航路径(Routes)。
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { RegisterRoutes(RouteTable.Routes); } void RegisterRoutes(RouteCollection routes) { routes.MapPageRoute( "deepLinkRouteFull", "restaurant/{restaurantId}/plate/{plateId}", "~/default.aspx", false, new RouteValueDictionary { { "restaurant", "-1" }, { "plate", "-1" } }); routes.MapPageRoute( "deepLinkRoute", "restaurant/{restaurantId}", "~/default.aspx", false, new RouteValueDictionary { { "restaurant", "-1" } });
在上面代码中,我们定义了深层链接的Url模板,支持Url中添加餐馆编号(restaurantId)和美食编号(plateId)的参数。我们定义的导航路径的顺序是从最复杂到不那么复杂。如果这两个编号在Url中都缺失的话,那么将使用上面设置的默认值。
现在,我们来看看Silverlight客户端怎么解析这个Url的。在Plates.xaml.cs文件中:
// Executes when the user navigates to this page. protected override void OnNavigatedTo(NavigationEventArgs e) { int plateID = -1; int restaurantId =-1; var s = HtmlPage.Document.DocumentUri.ToString().Split(new char[] {'/','#'}); int i = Find(s, "plate"); if (i != -1) { plateID = Convert.ToInt32(s[i + 1]); plateDomainDataSource.FilterDescriptors.Add( new FilterDescriptor("ID", FilterOperator.IsEqualTo, plateID)); } i = Find(s, "restaurant"); if (i != -1) restaurantId = Convert.ToInt32(s[i + 1]); else restaurantId = Convert.ToInt32(NavigationContext.QueryString["restaurantId"]); plateDomainDataSource.QueryParameters.Add( new Parameter() { ParameterName = "resId", Value = restaurantId } ); }
基本上,上面代码所做的事情就是获取完整的Url,然后将其结构化,再提取出其中的餐馆编号和美食编号。在18-23行中,我们将餐 馆编号作为参数传给查询方法,但是在11-14行中,我们并没有这么干,而是应用了一个过滤器,这就相当于最终发给服务器的LINQ查询就多了一个 “where”子句。这样,我们就无需修改服务端的代码了。
我们还需要做一点小工作,那就是保证客户端最终导航到美食(Plates)的页 面。这在Silverlight的导航框架中是通过锚点“#/Plates”来实现的。但由于锚点只是一个客户端特性,而搜索引擎并不能很好的处理锚点, 因此我们需要在客户端动态添加锚点。我发现用一点点Javascript脚本就可以很好的完成这个工作。我在服务端的Default.aspx页面上加入 了这段脚本:
protected void Page_Init(object sender, EventArgs e) { string resId = Page.RouteData.Values["restaurant"] as string; if (resId != null) { Response.Write("<script type=text/javascript>window.location.hash='#/Plates';</script"+">"); } }
还有一个小事情需要注意的,由于我们启用了路径导航的功能,现在就可以通过不同的Url来访问default.aspx页面了,因此 如果使用相对路径去引用Silverlight.js以及MyApp.xap包的时候就会出现问题。例如我们会看到浏览器去请求 http://www.hanselman.com/abrams/restaurant/25/plate/4/Silverlight.js,而不是 http://www.hanselman.com/abrams/silverlight.js,这会导致下面错误:
行: 56
错误: Unhandled Error in Silverlight Application
异常代码: 2104
类别: InitializeError
消息: 无法下载Silverlight程序,请检查Web服务器的设置
要解决这个问题,我们需要修改脚本的src属性:
<script src='<%= this.ResolveUrl("~/Silverlight.js") %>'
还有Silverlight插件参数:
<param name="source" value="<%= this.ResolveUrl("~/ClientBin/MyApp.xap") %>"/>
现在我们再通过http://localhost:30045/restaurant/48/plate/119#/Plates这个包含美食编号的Url来访问,就能直接来到相应的美食页面了:
步骤2:使用网站地图让搜索引擎知道你网站的所有深层链接
现 在我们有了一个已经做好深层链接的应用程序了,每一个有趣的内容都被赋予了一个唯一的Url。但搜索引擎怎么知道这些Url呢?我们当然希望别人在社交网 络上多谈论(谈论的时候通常会留下链接),然后搜索引擎能够发现其中的一部分Url,但我们更希望能够把这事做完整了。我们想给搜索引擎提供一个应用程序 内部所有深层链接的Url列表。这可以使用网站地图(Sitemap)来实现。
网站地图的格式是所有主流搜索引擎都认可的。你可以在http://sitemap.org网站找到相关的信息。
为了更好的了解这其中的原理,我们来看看亚马逊,http://amazon.com,这个以数据驱动的网站是如何被搜索引擎索引的。当搜索引擎第一次来到这个网站,它会去读取网站根目录下的robots.txt文件。在这个例子中,robots.txt文件的绝对地址是http://www.amazon.com/robots.txt。
在这个例子中,我们可以看到,文件上半部分列举了一些目录,这些目录是不允许搜索引擎爬取的,文件下半部分列举的sitemap清单,是希望搜索引擎去爬取这些网站内容。
注意,严格来说,你不是必须使用Robots文件来声明你的网站地图(译者注:原文写的是“你不是必须使用网站地图”)。你可以使用主流搜索引擎提供的网站管理员工具直接登记网站地图地址。
如果我们导航到上面其中一个网址,我们将看到一个网站地图文件,如下面所示:
在这个例子中,因为亚马逊网站很庞大,这些链接被放到多个网站地图中去了(上面这个文件被称为网站地图索引文件)。当我们钻进子网站地图文件中,就能看到实际产品的链接了。
格式就如你看到的:
<urlset xmlns="http://www.google.com/schemas/sitemap/0.84"> <url> <loc>http://www.amazon.com/GAITHER-COMMITTEE-EISENHOWER-COLD-WAR/dp/081425005X</loc> </url> <url> <loc>http://www.amazon.com/CONTROLLING-VICE-REGULATING-PROSTITUTION-CRIMINAL/dp/0814250076</loc> </url>
有意思的是,当亚马逊产品目录中添加或者移除商品的时候,这里的链接也会经常发生变更。
我们接着来看看如何为我们的网站建立一个网站地图。在Web工程中,打开VS的添加条目对话框,往我们工程添加一个“Search sitemap” 。
确认你机器上已经安装了RIA Services Toolkit ,否则是没有这个条目模板的。
添加成功之后,我们会得到一个robots.txt文件:
# This file provides hints and instructions to search engine crawlers.
# For more information, see http://www.robotstxt.org/.
# Allow all
User-agent: *
# Prevent crawlers from indexing static resources (images, XAPs, etc)
Disallow: /ClientBin/
# Register your sitemap(s) here.
Sitemap: /Sitemap.aspx
还有一个sitemap.aspx的文件。
更多信息,请参考:Uncovering web-based treasure with Sitemaps。
但 这个sitemap.aspx文件还是空的,要建立实际可用的网站地图,我们需要基于PlateViewDomainService创建另外的视图。在这 个例子,这个视图的消费者是一个Asp.Net页面。这里我们要用到asp:DomainDataSource。在设计器拖放一个Repeater控件到 表单中,如下所示:
在Repeater控件上右键配置数据源:
选择一个数据源
最后,我们的网站地图文件中就会得到两个链接集合。
<asp:DomainDataSource runat="server" ID="RestaurauntSitemapDataSource" DomainServiceTypeName="MyApp.Web.DishViewDomainService" QueryName="GetRestaurants" /> <asp:Repeater runat="server" id="repeater" DataSourceID="RestaurauntSitemapDataSource" > <HeaderTemplate> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </HeaderTemplate> <ItemTemplate> <url> <loc><%= Request.Url.AbsoluteUri.ToLower().Replace("sitemap.aspx",string.Empty) + "restaurant/"%><%# HttpUtility.UrlEncode(Eval("ID").ToString()) %></loc> </url> </ItemTemplate> </asp:Repeater> <asp:DomainDataSource ID="PlatesSitemapDataSource" runat="server" DomainServiceTypeName="MyApp.Web.DishViewDomainService" QueryName="GetPlates"> </asp:DomainDataSource> <asp:Repeater runat="server" id="repeater2" DataSourceID="PlatesSitemapDataSource"> <ItemTemplate> <url> <loc><%= Request.Url.AbsoluteUri.ToLower().Replace("sitemap.aspx",string.Empty) + "restaurant/"%><%# HttpUtility.UrlEncode(Eval("RestaurantID").ToString()) + "/plate/" + HttpUtility.UrlEncode(Eval("ID").ToString()) %></loc> </url> </ItemTemplate> <FooterTemplate> </urlset> </FooterTemplate> </asp:Repeater>
在第3行和第20行,我们是直接调用在DomainService中定义的GetRestaurant 和GetPlates方法。
现在,对于一个具有相当规模数据量的网站来说,这个页面将会耗费很长的时间去执行。它需要扫描数据库中的每一行。这当然可以保证数据永远最新的,但是我们希望平衡一下服务器的负载。一个简单的做法就是通过设置一个小时的输出缓存。具体操作参考ASP.NET Caching: Techniques and Best Practices。
<%@ OutputCache Duration="3600" VaryByParam="None" %>
另外一个处理这种大数据集的策略是将数据分割到多个网站地图中去,就像我们上面看到的亚马逊网站的例子。
随便取其中的一个Url,导航到此网址,OK,现在我们得到正确的页面了。
步骤3:对于重要内容,提供一个备选版本
太棒了,现在我们有了深层链接,我们也能让搜索引擎发现所有这些链接,不过,当搜索引擎来到这个页面的时候,它会看到什么?多数搜索引擎只会解析HTML,所以通过菜单"页面>查看源代码",我们就能够看到搜索引擎眼中我们页面的面目了:
或者我们在禁用Silverlight插件(工具 > 管理加载项)后去访问此页面,我们会看到:
我们会看到一个巨大的白屏!
Silverlight 生成的动态内容根本没有显示。事实上,要显示这些动态内容,必须得执行Silverlight代码。但我敢肯定,搜索引擎在任何时候都不会在他们的数据中 心上去执行我们的Silverlight(或者Flash或者Ajax)代码的。所以我们需要的是一些备选的内容。
幸运的是,要做到这一点非常简单。首先让所有备选的内容展示出来。需要特别注意的是,这些内容并不单单是给搜索引擎看。制作只给搜索引擎看的内容是“糊弄搜索引擎”,或者是垃圾网页。这些内容通常用来迷惑搜索引擎,掩盖网站的真实内容(the pernicious perfidy of page-level web spam) 。而这里的备选内容是给那些没有安装Silverlight插件的用户看的。它可能不拥有所有Silverlight的特性,但却提供了一个降级的用户体 验。当搜索引擎蜘蛛没有安装Silverlight插件的时候也是如此,这可以保证它们能够索引到一些有意义而且准确的信息。
把下面这段HTML代码添加到default.aspx页面中。
<div id="AlternativeContent" style="display: none;"> <h2>Hi, this is my alternative content</h2> </div>
注意,这里写的是display:none,意味着我们并不期望浏览器去渲染这段内容...除非Silverlight插件不可用。为了实现这个效果,把下面这小段JS代码添加到页面中:
<script type="text/javascript"> if (!isSilverlightInstalled()) { var obj = document.getElementById('AlternativeContent'); obj.style.display = ""; } </script>
注意,这个很方便的isSilverlightInsalled函数是从Petr的old-but-good post on the subject文章中找到的。我只是把这个函数添加到我的Silverlight.js文件而已。
function isSilverlightInstalled() { var isSilverlightInstalled = false; try { //check on IE try { var slControl = new ActiveXObject('AgControl.AgControl'); isSilverlightInstalled = true; } catch (e) { //either not installed or not IE. Check Firefox if (navigator.plugins["Silverlight Plug-In"]) { isSilverlightInstalled = true; } } } catch (e) { //we don't want to leak exceptions. However, you may //to add exception tracking code here } return isSilverlightInstalled; }
当我们使用一个没有启用Silverlight插件的浏览器中去浏览此页面的时候,我们就会看到备选的内容:
但是如果安装了Silverlight,我们则会看到漂亮的Silverlight应用程序内容:
这 很不错,但我们怎么让页面显示正确的内容呢?我们想要展示和Silverlight应用程序完全一致的数据,而且又不想写太多代码。我们也实在不想去维护 太多页面。所以让我们在页面的AlternativeContent这个DIV里头加入一些基本的代码。这个ListView是用来显示餐馆详细信息的。
<asp:ListView ID="RestaurnatDetails" runat="server" EnableViewState="false"> <LayoutTemplate> <asp:PlaceHolder ID="ItemPlaceHolder" runat="server"/> </LayoutTemplate> <ItemTemplate> <asp:DynamicEntity ID="RestaurnatEntity" runat="server"/> </ItemTemplate> </asp:ListView>
现在我们需要把它和我们的数据源绑定...我发现在VS的设计视图里头很容易就能够做到这一点。注意,要在设计器中编辑这个DIV,你需要将DIV改成可见模式。
然后我们来配置一下数据源:
下一步,我们需要基于我们之前定义的导航路径来绑定查询参数。
然后,我们对美食的ListView控件也重复同样的操作...
现在我们得到的代码非常简单:
<asp:ListView ID="RestaurnatDetails" runat="server" EnableViewState="false" DataSourceID="restaurantsDomainDataSource"> <LayoutTemplate> <asp:PlaceHolder ID="ItemPlaceHolder" runat="server"/> </LayoutTemplate> <ItemTemplate> <asp:DynamicEntity ID="RestaurnatEntity" runat="server"/> </ItemTemplate> </asp:ListView> <asp:DomainDataSource ID="restaurantsDomainDataSource" runat="server" DomainServiceTypeName="MyApp.Web.DishViewDomainService" QueryName="GetRestaurant"> <QueryParameters> <asp:RouteParameter name="id" RouteKey="restaurantId" DefaultValue ="-1" Type = "Int32"/> </QueryParameters> </asp:DomainDataSource>
下一步,我们需要让这些控件基于数据动态生成UI。
protected void Page_Init(object sender, EventArgs e) { RestaurnatDetails.EnableDynamicData(typeof(MyApp.Web.Restaurant)); PlateDetails.EnableDynamicData(typeof(MyApp.Web.Plate)); string resId = Page.RouteData.Values["restaurant"] as string; if (resId != null) { Response.Write("<script type=text/javascript>window.location.hash='#/Plates';</script"+">"); } }
注意到,我们在第4-5行处为这两个ListView控件启用了动态数据。
最后一步,我们需要添加DynamicData需要用到的模板集。你可以从任何DynamicData的工程中获得相应的文件。将其拷贝到Web工程根目录下。
你可以通过编辑这些模板来控制数据的显示。
在EntityTemplates目录中,我们需要为每个实体创建一个实体模板(在这个例子中,有美食和餐馆这两个实体)。这会决定他们是如何显示的。
<%@ Control Language="C#" CodeBehind="Restaurant.ascx.cs" Inherits="MyApp.Web.RestaurantEntityTemplate" %> <asp:DynamicControl ID="DynamicControl8" runat="server" DataField="ImagePath" /> <ul class="restaurant"> <li> <ul class="restaurantDetails"> <li><h2><asp:DynamicControl ID="NameControl" runat="server" DataField="Name" /> </h2> </li> <li><asp:DynamicControl ID="DynamicControl1" runat="server" DataField="ContactName" /> (<asp:DynamicControl ID="DynamicControl2" runat="server" DataField="ContactTitle" />)</li> <li><asp:DynamicControl ID="DynamicControl3" runat="server" DataField="Address" /> </li> <li><asp:DynamicControl ID="DynamicControl4" runat="server" DataField="City" />, <asp:DynamicControl ID="DynamicControl5" runat="server" DataField="Region" /> <asp:DynamicControl ID="DynamicControl6" runat="server" DataField="PostalCode" /> </li> <li><asp:DynamicControl ID="DynamicControl7" runat="server" DataField="Phone" /> </li> <li><asp:HyperLink runat="server" ID="link" NavigateUrl="<%#GetDetailsUrl() %>" Text="details.."></asp:HyperLink></li> </ul> </li> </ul>
这里我们只是做了些简单的格式化操作,然后把我们需要在备选内容中显示的字段挑选出来。对美食实体也重复同样的操作…
现在我们已经可以准备运行了。
访问根目录,在没有添加任何Url参数的情况下,我们会看到一个餐馆列表,当然,全都是HTML代码。
接着我们可以添加一个导航来定位某个特定餐馆的所有美食。
但是,让我们在真正的浏览器中浏览此页面,确保我们看到的和搜索引擎看到的是一样的。
Lynx还活着!Lynx是我92年在北卡罗来纳州州立大学Leazar实验室的DEC2100机器上用过的第一个浏览器...现在还可以正常工作!
还有细节信息:
这款经典的文本浏览器让我们看到了纯文本模式下页面的样子,这恰恰是搜索引擎蜘蛛所看到的。
现在,来看真正的测试。
我们使用Bing去搜索“我的美食浏览器——和Joe学烹饪”,显然,这个页面确实在那。
点击这个链接?
浏览器导航到的页面正是我们期望的页面。
当然,这在另外一个搜索引擎来说也是一样的... 如果你刚好是另外这个搜索引擎的用户:-)
总结
在上面这个例子中,我们学习了如何创建一个SEO优化过的、以数据驱动的Silverlight应用程序的基本操作。总共分为三个步骤:
- 步骤1:做好重要的页面的深层链接
- 步骤2:使用网站地图(Sitemap)让搜索引擎知道你网站的所有深层链接
- 步骤3:对于重要内容,提供一个备选版本
希望你们喜欢~
更多信息,请参考: 点亮Silverlight的SEO之路。