Monday, May 18, 2015

Automatically adding friendly Url to SharePoint Page according to its level/sub level

Friendly URLs

SharePoint 2013’s friendly URLs capability is extremely straightforward in that these URLs are links that correspond directly to a term within your organization or on a particular site or page as well as correspond to your organization’s navigation term set.
The .aspx ending is no longer required after site or page name as well as the default.aspx page can be dropped from the URL's reference entirely.
One of the most important point that would make you implement friendly Urls is the advantage of SEO, as now your Urls will not be like http://yourdoamain/en/MediaGallery/AlbumDetails.aspx?Id=3 , instead you will have  http://yourdoamain/en/MediaGallery/Albums/season-1, Which can highly indicates the content I'll be reading in this url

The issue

When you enable Managed Metadata Navigation, you could have this option "Create friendly URLs for new pages automatically", This will create a Term for each page and attach this term to the Page Urls, Till this point everything is wonderful .



This feature in action will behave in a strange way, which will give us a non desirable results, For example , let's assume having a Variation site collection with a News sub-site http://mySiteCollection/en/News, The default page for this sub-site is http://mySiteCollection/en/News/Pages/default.aspx.

Now we enabled the Managed Metadata Navigation to have Smart Urls, So the default page now would be connected to a Managed Navigation Term "News & Articles" , So the Url will be http://mySiteCollection/en/news-articles.

Add new Article Page under this sub-site let's say its title will be "Article 1", After the page creation you will find that the new page is attached to the parent navigation term



SharePoint adds new page terms to the root Term Set assigned for site navigation, no matter any  level the new page has been created.

The Solution

When creating a SharePoint page, A List Item Event Receiver could be attached to the sub-site pages library to check if the current sub-site default page is attached to a Navigation Term, If so the page event receiver will create a new Navigation Term under the default page term based on the page title as a child to maintain the same hierarchy.

So, In our example when creating a new page under the News sub-site, The event receiver will check the sub-site default page and will found it is attached to "News & Articles" Navigation term, Then the new page navigation term will be created under this term, The result will be http://mySiteCollection/en/news-articles/article-1

This solution is easy to apply but there are some challenges to keep in mind:

Page titles and special characters
As we are handling creating the new terms, We should handle the term friendly url segment the same way SharePoint handles it to avoid any broken urls, and to keep consistency


string formattedFriendlyUrlSegment = PageTitle.RemoveAccent();
friendlyUrlterm.FriendlyUrlSegment.Value = formattedFriendlyUrlSegment.Slugify();

//Removes white spaces
public static string RemoveAccent(this string txt)
{
 byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt);
 return System.Text.Encoding.ASCII.GetString(bytes);
}

public static string Slugify(this string phrase)
{
 string str = phrase.RemoveAccent().ToLower();
 str = System.Text.RegularExpressions.Regex.Replace(str, @"[^a-z0-9\s-]", ""); // Remove all non valid chars          
 str = System.Text.RegularExpressions.Regex.Replace(str, @"\s+", " ").Trim(); // convert multiple spaces into one space  
 str = System.Text.RegularExpressions.Regex.Replace(str, @"\s", "-"); // //Replace spaces by dashes
 return str;
}

Variations 
In some cases we have we needed to maintain the same term for both variation labels (Source and target) for both pages, So our plan will be to create a Term, and for each variation language will create a navigation term label to translate the term

Full Code:
Here is the full code for this article 


///<summary>
/// Creates a Friendly URL for a Publishing Page
/// </summary>
/// <param name="contextWeb">SPWeb Object which contains Publishing Page</param>
/// <param name="parentName">Parent Navigation Term</param>
/// <param name="page">Publishing Page for which the Friendly URL is added</param>
/// <param name="friendlyName">Friendly Name for the Page</param>
/// <param name="AddToNavigation">Flag to indicate whether to add this Navigation Term to Quick Launch and Top Naviagation</param>
/// <returns></returns>
public static string AddFriendlyUrl(SPWeb contextWeb, PublishingPage page, bool AddToNavigation)
{
 return AddFriendlyUrl(contextWeb, page, AddToNavigation, GetWebDefaultPageTerm(contextWeb));
}

public static string AddFriendlyUrl(SPWeb contextWeb, PublishingPage page, bool AddToNavigation, NavigationTerm ParentNavigationTerm)
{
 string relativeUrl = string.Empty;
 SPSecurity.RunWithElevatedPrivileges(() =>
 {
  try
  {
   IList<NavigationTerm> terms = TaxonomyNavigation.GetFriendlyUrlsForListItem(page.ListItem, true);

   // create Taxonomy Session for the context web
   TaxonomySession taxonomySession = TaxonomyNavigation.CreateTaxonomySessionForEdit(contextWeb);

   //Page is already attached to a navigation term / Page Updated
   if (terms.Count > 0)
   {
    //already associated to Metadata navigation
    NavigationTerm friendlyUrlterm = terms[0].GetAsEditable(taxonomySession);
    Term taxonomyTerm = friendlyUrlterm.GetTaxonomyTerm(taxonomySession);

    //for variation taxonomy terms
    if (!taxonomyTerm.IsSourceTerm)
    {
     friendlyUrlterm.FriendlyUrlSegment.UsesDefaultValue = false;
     taxonomyTerm = taxonomyTerm.SourceTerm;
    }


    bool labelUpdated = false;
    //Iterate through all labels for term translation  
    foreach (Microsoft.SharePoint.Taxonomy.Label label in taxonomyTerm.Labels)
    {
     //Update the label that suites the cuurent web, i.e if current web is en-US then this will update the English Value
     //if this web is ar-SA this will update the Arabic label value
     if (label.Language == contextWeb.Language)
     {
      label.Value = page.Title;
      labelUpdated = true;
     }
     
     //Always build the FriendlyUrlSegment from the English label
     if (label.Language == 1033)
     {
      string formattedFriendlyUrlSegment = label.Value.RemoveAccent();
      friendlyUrlterm.FriendlyUrlSegment.Value = formattedFriendlyUrlSegment.Slugify();
     }
    }

    //Create the language label if it doesn't exist
    if (!labelUpdated)
    {
     taxonomyTerm.CreateLabel(page.Title, (int)contextWeb.Language, false);
    }

    taxonomyTerm.TermStore.CommitAll();
   }
   else
   {
    //New Page 
    if (page.PublishingWeb.Label.IsSource)
    {
     // get the current Navigation Term Set
     NavigationTermSet currNavTermSet = TaxonomyNavigation.GetTermSetForWeb(contextWeb, StandardNavigationProviderNames.GlobalNavigationTaxonomyProvider, true);

     // Make the Term Set Editable
     NavigationTermSet editableTermSet = currNavTermSet.GetAsEditable(taxonomySession);

     // Get the Parent Term using the Parent Name Parameter matching the term Label
     NavigationTerm defaultPageFriendlyUrlterm = ParentNavigationTerm;

     // if a matching parent Term exists get a refernce to it
     if (defaultPageFriendlyUrlterm != null && !String.IsNullOrEmpty(page.Title))
     {
      // make the parent Term editable
      NavigationTerm editNewTerm = defaultPageFriendlyUrlterm.GetAsEditable(taxonomySession);

      // create the new FriendlyUrl along with the new Navigation Term
      relativeUrl = page.AddFriendlyUrl(page.Title, editNewTerm, AddToNavigation);
     }
    }
   }

   if (page.ListItem[new Guid(Constant.PublishingIsFurlPageId)] != null && !Boolean.Parse(page.ListItem[new Guid(Constant.PublishingIsFurlPageId)].ToString()))
   {
    page.ListItem[new Guid(Constant.PublishingIsFurlPageId)] = true;
    page.ListItem.SystemUpdate();
   }

  }
  catch (Exception ex)
  {
   Logger.LogException(ex);
   Logger.LogException(new Exception("StackTrace : " + ex.StackTrace));
   throw ex;
  }
 });
 return relativeUrl;
}

public static NavigationTerm GetWebDefaultPageTerm(SPWeb web)
{
 //list for saving the urls
 NavigationTerm friendlyUrlterm = null;

 //check if the current web is a publishing weg
 if (PublishingWeb.IsPublishingWeb(web))
 {
  PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(web);
  SPListItem DefaultPage = publishingWeb.DefaultPage.Item;

  //get the pages list id
  Guid listId = PublishingWeb.GetPagesListId(web);

  //retrieve the pages list
  SPList pagesList = web.Lists[listId];


  //retrieve the terms used for the navigation (this can be multiple terms)
  IList<NavigationTerm> terms = TaxonomyNavigation.GetFriendlyUrlsForListItem(DefaultPage, true);

  string url = string.Empty;

  //check if the pages has terms associated with it
  if (terms.Count > 0)
  {
   //use the GetResolvedDisplayUrl to retrieve the page friendly urls
   friendlyUrlterm = terms[0];
  }

 }

 return friendlyUrlterm;
}


Hope you found this article useful :) 

5 comments:

  1. Good Job Islam.

    ReplyDelete
  2. Hello,

    Looks great :) Can you tell me what the value is of constant Constant.PublishingIsFurlPageId?

    ReplyDelete
  3. I am trying something very similar like this. IN my case i am not able to determine the level. In my managed metadata navigation i have say three nested levels site collection (root web) --> first subsite --> second subsite--> third subsite ---> fourth subsite--> nth subsite

    now when i add pages to nth subsite level how do i add term pages to to nth subsite.
    I have done it for say 2 3 levels where you know the levels but is there i was we can make a generic solution which can work for any level automatically.
    I can share my code if u want to help.

    ReplyDelete
    Replies
    1. If I have a good understanding from what you have just descriped, I think if you just set the default page of each subsite in any level , the above code detrmines the default node for the current site's default page, then adds the new page term as a child navigation node, please let me know if you have the same assumbtion

      Delete
  4. Thank you for this great post, i have a variation with two labels
    the source is English with label name is en-us
    can i change "en-us" to be "en" after creating the labels without dropping the label ?

    ReplyDelete