I wrote several posts about cross-site publishing in Sharepoint 2013 already (you may find them here). In this post I will describe the process of enabling SEO features on sites with cross-site publishing – important mechanism for public sites. Before to start I recommend to read useful document prepared by Waldek Mastykarz: Optimizing SharePoint Server 2013 websites for Internet search engines. It contains good overview of the related functionality. In my post I will go deeper and show how some of these features work internally.
So suppose we have common scenario for cross-site publishing: authoring site with published catalog and public site which is connected to this catalog. On public site there is target page which is used for displaying catalog items (it contains ContentBySearchWebPart or CatalogItemReuseWebPart or both). And we want to display title of the displayed item in browser title, which, as you probably know, has big sorting weight for search engines.
First of all let’s say a few words about how html title is displayed in Sharepoint (we will talk here about publishing sites). This information will be useful for understanding the concept, described later. First of all there is PageTitle control on the masterpage and content placeholder PlaceHolderPageTitle in it:
1: <SharePoint:PageTitle ID="PageTitle1" runat="server">
2: <asp:ContentPlaceHolder id="PlaceHolderPageTitle" runat="server" />
3: </SharePoint:PageTitle>
It may also contain some default content, but it is not important for us now. Particular page layouts may have Content control for this placeholder, and thus may override content in page title. Often they just contain FieldValue control for Title field, i.e. title specified in publishing page’s metadata will be used for browser title:
1: <asp:Content ContentPlaceholderId="PlaceHolderPageTitle" runat="server">
2: <SharePointWebControls:FieldValue ID="HtmlPageTitle" runat="server" FieldName="Title"/>
3: </asp:Content>
Returning to our example, target page for displaying catalog items is also regular publishing page created with some page layout. I.e. it also may have content for PlaceHolderPageTitle as shown above. But of course we don’t want it to display title of the page – because in this case it would mean that for all displayed items, browser title would be the same and would retrieved from metadata of details page itself, but not from search index with other item’s data. And here ContentBySearchWebPart.AddSEOPropertiesFromSearch property comes on the scene. With proper usage it allows to use data from search index for using e.g. in browser title (or description and keywords meta tags). As it often be however, documentation doesn’t help us a lot saying just that it:
Specifies properties to be used for search engines; these properties will optimize the crawlers and improve SEO.
But it didn’t say neither of how it does it nor how to properly configure it. Let’s fill this space by ourselves then.
First of all if you want to use SEO features, you need to enable Search Engine Optimization Feature (Site scope) on your site collection (we will check it later in this post), and at second you need to set AddSEOPropertiesFromSearch property to true for your web part. You can’t do it in UI, but it is possible to do by modifying webpart file:
1: <property name="AddSEOPropertiesFromSearch" type="bool">True</property>
Let’s analyze code of ContentBySearchWebPart in reflector and see how this property is used. It is used in 2 places: in OnLoad() and in OnQueryResolved() methods of the web part. Here is how it is used in OnLoad:
1: protected override void OnLoad(EventArgs e)
2: {
3: ...
4: if (this.AddSEOPropertiesFromSearch)
5: {
6: CatalogSearchContext.Current.SetContextValue("IsSearchPage", bool.TrueString);
7: }
8: ...
9: }
I.e. it just sets some search context IsSearchPage parameter to true. And that’s how it is used in OnQueryResolved:
1: private void OnQueryResolved(object sender, EventArgs e)
2: {
3: ...
4: if (this.AddSEOPropertiesFromSearch)
5: {
6: PopulateSEOSearchContext(this.substrateRow);
7: }
8: ...
9: }
Let’s check the code of PopulateSEOSearchContext() method used here:
1: private static void PopulateSEOSearchContext(DataRow resultRow)
2: {
3: CatalogSearchContext current = CatalogSearchContext.Current;
4: foreach (Tuple<string, string> tuple in SEOManagedProperties)
5: {
6: if (resultRow.Table.Columns.Contains(tuple.Item1))
7: {
8: string str = resultRow[tuple.Item1] as string;
9: if (!string.IsNullOrEmpty(str))
10: {
11: current.SetContextValue(tuple.Item2, str);
12: }
13: }
14: }
15: }
SEOManagedProperties is private static collection populated in class constructor:
1: SEOManagedProperties = new Tuple<string, string>[]
2: {
3: new Tuple<string, string>("Title", "SeoPropBrowserTitle"),
4: new Tuple<string, string>("Path", "CanonicalURLWithParameters"),
5: new Tuple<string, string>("SeoBrowserTitleOWSTEXT", "SeoPropBrowserTitle"),
6: new Tuple<string, string>("SeoKeywordsOWSTEXT", "SeoPropKeywords"),
7: new Tuple<string, string>("SeoMetaDescriptionOWSTEXT", "SeoPropDescription")
8: };
I.e. PopulateSEOSearchContext() checks for each row does it contain column defined in Item1 of tuples collection (like Title, Path, etc. – managed properties from search index), and if yes, stores values of these columns in search context with Item2 of corresponding tuple (e.g. for Title managed property it uses SeoPropBrowserTitle key). Ok, now let’s return to Search Engine Optimization feature and will see how these context values are used on the pages.
Search Engine Optimization feature adds a lot of functionalities for optimizing your web site for search engines. Among with other components it specifies the following delegate controls with AdditionalPageHead id for the site collection:
- SeoMetaDescription
- SeoKeywords
- SeoNoIndex
- SeoCanonicalLink
- SeoCustomMeta
- SeoBrowserTitle
Using these controls you may significantly improve rank of your pages in search results. But it is not enough just activate this feature on the site collection. In order to really work, they should have some content when page is rendered. And in case of target page for displaying catalog items it is even more important, because they should contain different content depending on the current catalog item.
Let’s analyze last control SeoBrowserTitle. First of all its OnPreRender() method:
1: protected override void OnPreRender(EventArgs args)
2: {
3: if (CatalogSearchContext.Current.GetContextValue("IsSearchPage") ==
4: bool.TrueString)
5: {
6: this.SetRenderDelegate();
7: }
8: else
9: {
10: string currentTermPropertyValue =
11: SeoBaseControl.GetCurrentTermPropertyValue("_Sys_Seo_PropBrowserTitle");
12: if (!string.IsNullOrWhiteSpace(currentTermPropertyValue))
13: {
14: this.browserTitle = currentTermPropertyValue;
15: this.SetRenderDelegate();
16: }
17: else
18: {
19: currentTermPropertyValue =
20: SeoBaseControl.GetFieldContents("SeoBrowserTitle") as string;
21: if (!string.IsNullOrWhiteSpace(currentTermPropertyValue))
22: {
23: this.browserTitle = currentTermPropertyValue;
24: this.SetRenderDelegate();
25: }
26: }
27: }
28: }
First of all it checks whether IsSearchPage search context parameter is set to true. If yes it calls SetRenderDelegate() method which replaces current content of the PlaceHolderPageTitle placeholder (in out case FieldValue control for Title field) with own RenderBrowserTitle() method:
1: private void SetRenderDelegate()
2: {
3: ContentPlaceHolder holder = this.Page.Master.FindControl("PlaceHolderPageTitle")
4: as ContentPlaceHolder;
5: if (holder != null)
6: {
7: holder.SetRenderMethodDelegate(new RenderMethod(this.RenderBrowserTitle));
8: }
9: }
And the rendering method itself:
1: private void RenderBrowserTitle(HtmlTextWriter writer, Control control)
2: {
3: string contextValue =
4: CatalogSearchContext.Current.GetContextValue("SeoPropBrowserTitle");
5: if (!string.IsNullOrWhiteSpace(contextValue))
6: {
7: this.browserTitle = contextValue;
8: }
9: writer.Write(SPHttpUtility.HtmlEncode(this.browserTitle));
10: }
Now remember that IsSearchPage search context parameter is set to true by ContentBySearchWebPart when AddSEOPropertiesFromSearch property is set to true. That’s how this property controls SEO controls on the page. In RenderBrowserTitle() method value of SeoPropBrowserTitle search context parameter is retrieved (which according to SEOManagedProperties collection corresponds to Title managed property in search index), and if it is not empty, it is written to html output.
We need also to mention that SeoBrowserTitle control overrides browser title also when IsSearchPage is false, but there are not-empty values in _Sys_Seo_PropBrowserTitle or SeoBrowserTitle fields of current page (see code above). These site columns are added also with Search Engine Optimization feature – see Optimizing SharePoint Server 2013 websites for Internet search engines document, mentioned in the beginning and its section 6.1 Using standard SEO columns within catalogs.
This is the final of this story. Hope that it was interesting and that it will help you in your work.
HI! Great post.
ReplyDeleteCould you answer my question?
I have catalog on another site, so when I turn on "AddSEOPropertiesFromSearch"-property, i've got in the Path column the the path for the element.
Such as catalog-site.com/Lists/List/DispForm.aspx?ID=1234
But I want use custom path. Path to this element in my site. For example
mySite.com/MyCategory/SuperElement
Is there way to it?
re-gor,
ReplyDeletewhen you tell that in Path column you have path to the element on catalog site, do you mean value of Path managed property which you use in display template or something else? Did you try to set target page for catalog items in catalog connection settings on your public site? And if you set AddSEOPropertiesFromSearch to false, is Path value changed to the correct value?
Alexey Sadomov,
DeleteSorry for delay before answer...
Yes, Path column is managed property, which can be used in display template. Am I right that "PopulateSEOSearchContext" function work exatly with this column and do not use preferences of Display template? I think it is standart managed property, and there is no way to change it.
No I didn't. There is no connection to catalog in properties of site collection. Administrator of the farm had told me why it could not be done (connection to catalog). But it was a long time ago, and i forgot why. Now he is not available to call - he is in the business trip for couple of days. So in search content web part we use queries like
path:"http://catalog-cite.com/Lists/List" {URLToken.1}
When I set AddSEOPropertiesFromSearch to false there is something unreadable and unworking in _link ... rel='canonical'_. It does not use friendly urls, but use direct url to page (site.com/pages/page.aspx?a_lot_of_Parametrs). And as I say it is no work.
Catalog connection settings can be checked in Site settings > Manage catalog connections > Click on specific catalog connection name > Catalog Item URL Format. You should have it if you use cross-site publishing. Otherwise I don't fully understand how you try to use catalog data without connecting to this catalog.
ReplyDeleteAbout PopulateSEOSearchContext method: as code above shows it sets value of Path managed property to the context with CanonicalURLWithParameters key. It doesn't work with properties defined in display templates.
Ok... Thank you!
DeleteHi, im trying to recovery the seo properties in each page in SharePoint 2013, and no see way. is posible get the current seoKeyword, seCanonicalLink, etc from code behind?
ReplyDeleteThanks in advance.
Hi, i found the answer.
Deletethis work for me.
SPSite site = new SPSite(siteGuid);
SPWeb web = site.OpenWeb();
TaxonomySession session = new TaxonomySession(site);
NavigationTerm navTermino = TaxonomyNavigationContext.Current.NavigationTerm;
Term termino = navTermino.GetTaxonomyTerm(session);
//var seoTitle = termino.CustomProperties.Where(o => o.Key == "_Sys_Seo_PropBrowserTitle").SingleOrDefault();
var SEOPropBrowserTitle = termino.LocalCustomProperties.Where(o => o.Key == "_Sys_Seo_PropBrowserTitle").SingleOrDefault();
var SEOPropDescription = termino.LocalCustomProperties.Where(o => o.Key == "_Sys_Seo_PropDescription").SingleOrDefault();
var SEOPropKeyWords = termino.LocalCustomProperties.Where(o => o.Key == "_Sys_Seo_PropKeywords").SingleOrDefault();
var SEOPropSiteNoIndex = termino.LocalCustomProperties.Where(o => o.Key == "_Sys_Seo_PropSiteNoIndex").SingleOrDefault();
Thanks.
Thanks
Jorge, thank you for sharing!
Delete