Frederik Vig – ASP.NET developer

Follow me

Archive for September 2009

Creating a Custom EPiServer Paging Control

Earlier I wrote about the PageList control from EPiServer, I mentioned in that post that the markup that the PagingControl renders is not the best. For instance it will not work without JavaScript enabled on the client (for search engines the links wont work). I therefor decided to create my own.

To start with I went to the EPiServer SDK to have a look at the PagingControl class. Here I was able to see the namespace and what .dll file it lives in (EPiServer.dll). I then opened up Reflector, to inspect EPiServer.dll and have a closer look at the code.

Description of Reflector by Red Gate.

.NET Reflector enables you to easily view, navigate, and search through, the class hierarchies of .NET assemblies, even if you don’t have the code for them. With it, you can decompile and analyze .NET assemblies in C#, Visual Basic, and IL.

To find the PagingControl class you can either use the search feature or navigate the namespaces.

PagingControl class with Reflector

The great thing is that all the methods are virtual which means that we can override them :) .

Start by creating a new class and inherit from PagingControl (remember to add using EPiServer.Web.WebControls;).

using System;
using System.Text;
using System.Web;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Classes
{
    public class CustomPager : PagingControl
    {
    }
}

Then create a new page template with a PageList control.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Page.aspx.cs" Inherits="FV.Templates.FV.Pages.Page" MasterPageFile="~/Templates/Public/MasterPages/MasterPage.master" %>
<asp:Content runat="server" ContentPlaceHolderID="MainBodyRegion">
    <EPiServer:PageList runat="server" ID="pglList">
        <HeaderTemplate>
            <ul>
        </HeaderTemplate>
        <ItemTemplate>
            <li><EPiServer:Property runat="server" PropertyName="PageLink" /></li>
        </ItemTemplate>
        <FooterTemplate>
            </ul>
        </FooterTemplate>
    </EPiServer:PageList>
</asp:Content>
using System;
using EPiServer;
using EPiServer.Core;
using FV.Templates.FV.Classes;
 
namespace FV.Templates.FV.Pages
{
    public partial class Page : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
 
            pglList.Paging = true;
            pglList.PageLink = PageReference.StartPage;
            pglList.PagingControl = new CustomPager();
            pglList.PagesPerPagingItem = 2;
        }
    }
}

Notice that we set the PageList’s PagingControl property to a new instance of our CustomPager web control.

If you open up a browser and run the code, you’ll just get a standard PagingControl with the JavaScript and bad markup (since we haven’t overridden anything yet).

Lets start by adding an ordered list for the paging items.

To do this we have to override the CreatePagingItems, AddSelectedPagingLink and AddUnselectedPagingLink methods.

using System;
using System.Text;
using System.Web;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Classes
{
    public class CustomPager : PagingControl
    {
        private HtmlGenericControl Container
        {
            get; set;
        }
 
        private static string Translate(string text)
        {
            return LanguageManager.Instance.Translate("/webcontrols/paging/" + text);
        }
 
        protected override LinkButton AddSelectedPagingLink(int pagingIndex, string text, string altText)
        {
            var li = new HtmlGenericControl("li");
            li.Attributes.Add("class", this.CssClassSelected);
            li.Attributes.Add("title", altText);
            li.InnerText = text;
            this.Container.Controls.Add(li);
            return null;
        }
 
        protected override LinkButton AddUnselectedPagingLink(int pagingIndex, string text, string altText, bool visible)
        {
            var li = new HtmlGenericControl("li");
            var child = this.CreatePagingLink(pagingIndex, text, altText);
            child.CssClass = this.CssClassUnselected;
            li.Visible = visible;
 
            li.Controls.Add(child);
            this.Container.Controls.Add(li);
            return null;
        }
 
        public override void CreatePagingItems(PageDataCollection pages)
        {
            int pagingIndex = this.CurrentPagingItemCount - 1;
            bool visible = this.CurrentPagingItemIndex > 0;
            bool flag2 = this.CurrentPagingItemIndex < pagingIndex;
            if ((pagingIndex != 0) || !this.AutoPaging)
            {
                this.Container = new HtmlGenericControl("ol");
                this.LinkCounter = 0;
                if (this.FirstPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(0, this.FirstPagingItemText, Translate("firstpage"), visible);
                }
                if (this.PrevPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex - 1, this.PrevPagingItemText, Translate("prevpage"), visible);
                }
                for (int i = 0; i <= pagingIndex; i++)
                {
                    if (i == this.CurrentPagingItemIndex)
                    {
                        this.AddSelectedPagingLink(i, Convert.ToString((int)(i + 1)), Translate("currentpage"));
                    }
                    else
                    {
                        this.AddUnselectedPagingLink(i, Convert.ToString((int)(i + 1)), string.Format(Translate("pagenumber"), i + 1), true);
                    }
                }
                if (this.NextPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex + 1, this.NextPagingItemText, Translate("nextpage"), flag2);
                }
                if (this.LastPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(pagingIndex, this.LastPagingItemText, Translate("lastpage"), flag2);
                }
 
                this.Controls.Add(this.Container);
            }
        }
    }
}

I added a private property to hold my ordered list (Container), so that I can easily add child controls to it. I Also removed the calls to the AddLinkSpacing method (since we don’t need it).

If you browse the page you’ll see that everything is inside an ordered list now.

To fix the JavaScript links I created a new method CreatePagingHyperLink that will create the HyperLink control with the correct url. I Then added code in the OnInit method to get the query string and set the PageList’s CurrentPagingItemIndex property.

using System;
using System.Text;
using System.Web;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using EPiServer;
using EPiServer.Core;
using EPiServer.Web;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Classes
{
    public class CustomPager : PagingControl
    {
        private HtmlGenericControl Container
        {
            get; set;
        }
 
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
 
            if (HttpContext.Current.Request.QueryString["p"] == null)
            {
                return;
            }
 
            var p = HttpUtility.HtmlEncode(HttpContext.Current.Request.QueryString["p"]);
            int num;
 
            int.TryParse(p, out num);
            this.CurrentPagingItemIndex = num;
        }
 
        private static string Translate(string text)
        {
            return LanguageManager.Instance.Translate("/webcontrols/paging/" + text);
        }
 
        protected override LinkButton AddSelectedPagingLink(int pagingIndex, string text, string altText)
        {
            var li = new HtmlGenericControl("li");
            li.Attributes.Add("class", this.CssClassSelected);
            li.Attributes.Add("title", altText);
            li.InnerText = text;
            this.Container.Controls.Add(li);
            return null;
        }
 
        protected override LinkButton AddUnselectedPagingLink(int pagingIndex, string text, string altText, bool visible)
        {
            var li = new HtmlGenericControl("li");
            var child = this.CreatePagingHyperLink(pagingIndex, text, altText);
            child.CssClass = this.CssClassUnselected;
            li.Visible = visible;
 
            li.Controls.Add(child);
            this.Container.Controls.Add(li);
            return null;
        }
 
        private static string CreateUrl(int count)
        {
            var url = new UrlBuilder(HttpContext.Current.Request.Url);
 
            if (UrlRewriteProvider.IsFurlEnabled)
            {
                Global.UrlRewriteProvider.ConvertToExternal(url, null, Encoding.UTF8);
            }
 
            return UriSupport.AddQueryString(url.ToString(), "p", count.ToString());
        }
 
        protected HyperLink CreatePagingHyperLink(int pagingIndex, string text, string altText)
        {
            var link = new HyperLink();
            this.LinkCounter++;
            link.ID = "PagingID" + this.LinkCounter;
            link.NavigateUrl = CreateUrl(pagingIndex);
            link.Text = text;
            link.ToolTip = altText;
 
            return link;
        }
 
        public override void CreatePagingItems(PageDataCollection pages)
        {
            int pagingIndex = this.CurrentPagingItemCount - 1;
            bool visible = this.CurrentPagingItemIndex > 0;
            bool flag2 = this.CurrentPagingItemIndex < pagingIndex;
            if ((pagingIndex != 0) || !this.AutoPaging)
            {
                this.Container = new HtmlGenericControl("ol");
                this.LinkCounter = 0;
                if (this.FirstPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(0, this.FirstPagingItemText, Translate("firstpage"), visible);
                }
                if (this.PrevPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex - 1, this.PrevPagingItemText, Translate("prevpage"), visible);
                }
                for (int i = 0; i <= pagingIndex; i++)
                {
                    if (i == this.CurrentPagingItemIndex)
                    {
                        this.AddSelectedPagingLink(i, Convert.ToString((int)(i + 1)), Translate("currentpage"));
                    }
                    else
                    {
                        this.AddUnselectedPagingLink(i, Convert.ToString((int)(i + 1)), string.Format(Translate("pagenumber"), i + 1), true);
                    }
                }
                if (this.NextPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(this.CurrentPagingItemIndex + 1, this.NextPagingItemText, Translate("nextpage"), flag2);
                }
                if (this.LastPagingItemText.Length > 0)
                {
                    this.AddUnselectedPagingLink(pagingIndex, this.LastPagingItemText, Translate("lastpage"), flag2);
                }
 
                this.Controls.Add(this.Container);
            }
        }
    }
}

I also added some CSS code to give you an idea of how easy it is to style and change the look.

.PagingContainer ol {
    margin: 0;
    padding: 0;
    overflow: hidden;
}
 
.PagingContainer li {
    list-style: none;
    display: inline;
}
 
.PagingContainer a, .SelectedPagingItem {
    text-decoration: none;
    float: left;
    padding: .2em;
    border: 1px solid #303233;
    color: #303233;
    font-weight: bold;
    margin-right: .1em;
}
 
.PagingContainer .SelectedPagingItem {
    background: #303233;
    color: #fff;
}

The result

Custom paging control

Markup rendered

<div class="PagingContainer">
        <ol>
            <li class="SelectedPagingItem" title="Current page">1</li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID4" title="Page 2" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=1">2</a>
            </li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID5" title="Page 3" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=2">3</a>
            </li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID6" title="Next page" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=1">></a>
            </li>
            <li>
                <a id="ctl00_MainRegion_MainContentRegion_MainBodyRegion_pglList_ctl05_PagingID7" title="Last page" class="UnselectedPagingItem" href="http://fv/en/Test-Paging/?p=2">ยป</a>
            </li>
        </ol>
    </div>

Download the code

Extending search field with suggestion box

Disclaimer: In this example I’ve kept the code simple to make it easier to read and to give you an idea of how you might approach something like this with EPiServer. This code should not be used in production scenarios since it will use a lot of resources. Use at own risk :) .

Update – 12.10.2009

Updated production code with a new custom filter that removes pages that the Everyone role doesn’t have access to.
Also updated the Get caching method. See comments below for further details.

Update – 19.09.2009

Added example of production code you might consider.

We’ve all used Google Suggest before. When you start typing a word or sentence, Google comes up with suggestions to what we might be searching for.
Example of Google Suggest
This is to help users quickly find what they want.

In this blog post we’re going to do the same by extending QuickSearch that ships with the EPiServer Public Templates.

I decided to use jQuery and the AutoComplete plugin for this, but you can use any script you like (most are pretty similar).

The result

Example of search suggest

The Code

First start by modifying the QuickSearch.ascx:

<asp:Panel runat="server" CssClass="QuickSearchArea" DefaultButton="SearchButton">
	<asp:Label ID="SearchLabel" runat="server" AssociatedControlID="SearchText" CssClass="hidden" Text="<%$ Resources: EPiServer, navigation.search %>" />
    <asp:TextBox ID="SearchText" TabIndex="1" runat="server" CssClass="quickSearchField" autocomplete="off" />
    <asp:ImageButton ID="SearchButton" runat="server" ImageUrl="~/Templates/Public/Images/MainMenuSearchButton.png" ToolTip="<%$ Resources: EPiServer, navigation.search %>" CausesValidation="false" CssClass="quickSearchButton" OnClick="Search_Click" />
</asp:Panel>
 
<script type="text/javascript">
//<![CDATA[ 
    $("#<%=SearchText.ClientID %>").autocomplete("/path/to/file/SearchSuggest.aspx", {
        minChars: 2,
        max: 10
    });
//]]> 
</script>

A few things to note. I removed the default text from the TextBox and added the attribute autocomplete=”off”, this is to ensure that the users browser does not store the information (How to Turn Off Form Autocompletion). I then attached the AutoComplete plugin to the TextBox and changed a few of its default properties.

As you can see there is not much code that is needed when mostly using the default settings. You can of course do a lot more. Browse through the AutoComplete plugin documentation and see the demos to get a few ideas.

Now for the main part, SearchSuggestion.aspx. This is just an empty web forms page with all the code in the code-behind:

using System;
using System.Text;
using System.Web;
using EPiServer;
using EPiServer.Core;
using EPiServer.Filters;
using EPiServer.Security;
 
namespace FV.Templates.FV.Pages
{
    public partial class SearchSuggest : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            // #1
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
 
                // #2
                var criterias = new PropertyCriteriaCollection();
 
                var queryCriteria = new PropertyCriteria
                                        {
                                            Condition = CompareCondition.StartsWith,
                                            Name = "PageName",
                                            Value = query,
                                            Type = PropertyDataType.String,
                                            Required = true
                                        };
 
                var pubCriteria = new PropertyCriteria
                                      {
                                          Condition = CompareCondition.Equal,
                                          Name = "PagePendingPublish",
                                          Value = false.ToString(),
                                          Type = PropertyDataType.Boolean,
                                          Required = true
                                      };
 
                criterias.Add(queryCriteria);
                criterias.Add(pubCriteria);
 
                // #3
                var pages = DataFactory.Instance.FindPagesWithCriteria(PageReference.StartPage, criterias, CurrentPage.LanguageBranch);
 
                int tempLimit = Convert.ToInt32(limit);
                int pagesCount = pages.Count;
 
                // #4
                if (tempLimit > pagesCount)
                {
                    tempLimit = pagesCount;
                }
 
                // #5
                for (int i = 0; i < tempLimit; i++)
                {
                    sb.Append(pages[i].PageName);
                    sb.Append(Environment.NewLine);
                }
 
                // #6
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
    }
}
  • In #1 we retrieve the user input and the limit (which we set earlier with the max property in QuickSearch.ascx), we then HTML encode it (something you should always do when receiving user input).
  • In #2 we build the criteria collection with our search options. We search for pages that start with what the user typed in and then make sure that only published pages are returned.
  • In #3 we do the actual searching with the FindPagesWithCriteria method where we also set the current language branch (only return pages in that language).
  • In #4 we make sure that we don’t have less pages than the limit, if so we set the pages count to be the limit.
  • In #5 we go through pages (PageDataCollection) and add the PageName to sb (StringBuilder).
  • In #6 we return the result back

Below is the code for SearchSuggest.aspx.cs updated to use the SearchDataSource instead of FindPagesWithCriteria (this will not only search the PageName but all properties that are search able):

using System;
using System.Text;
using System.Web;
using System.Web.UI;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Pages
{
    public partial class SearchSuggest : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
 
                var searchDataSource = new SearchDataSource
                {
                    SearchQuery = query,
                    AccessLevel = AccessLevel.Read,
                    PublishedStatus = PagePublishedStatus.Published,
                    PageLink = PageReference.StartPage,
                    LanguageBranches = CurrentPage.LanguageBranch,
                    MaxCount = Convert.ToInt32(limit)
                };
 
                foreach (PageData page in searchDataSource.Select(DataSourceSelectArguments.Empty))
                {
                    sb.Append(page.PageName);
                    sb.Append(Environment.NewLine);
                }
 
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
    }
}

Production code

Like I said in the beginning, this code should not be used in production scenarios since it goes through the entire site searching for matching PageNames each time there is a request.

I’ve added some example code of how you might approach this in a production scenario. Basically I just use the FindPagesWithCriteria method to find all the pages and then store the PageName of each in an array that I then store in the cache for easy access.

using System;
using System.Linq;
using System.Text;
using System.Web;
using EPiServer;
using EPiServer.Core;
using EPiServer.Filters;
using EPiServer.Security;
using FV.Templates.FV.Filters;
 
namespace FV.Templates.FV.Pages
{
    public partial class ProductionSearchSuggestion : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
                string[] pageNames;
 
                const string pageNamesCacheKey = "AllPageNames";
 
                if (!Get(pageNamesCacheKey, out pageNames))
                {
                    var criterias = new PropertyCriteriaCollection();
 
                    var pubCriteria = new PropertyCriteria
                    {
                        Condition = CompareCondition.Equal,
                        Name = "PagePendingPublish",
                        Value = false.ToString(),
                        Type = PropertyDataType.Boolean,
                        Required = true
                    };
 
                    criterias.Add(pubCriteria);
 
                    var pages = DataFactory.Instance.FindPagesWithCriteria(PageReference.StartPage, criterias, CurrentPage.LanguageBranch);
 
                    new FilterRole("Everyone").Filter(pages);
 
                    var pagesCount = pages.Count;
                    pageNames = new string[pagesCount];
 
                    for (int i = 0; i < pagesCount; i++)
                    {
                        pageNames[i] = pages[i].PageName;
                    }
 
                    Add(pageNames, pageNamesCacheKey);
                }
 
                int tempLimit = Convert.ToInt32(limit);
 
                var result = new string[tempLimit];
                int resultCounter = 0;
 
                for (int i = 0; i < pageNames.Length; i++)
                {
                    if (resultCounter <= tempLimit && pageNames[i].ToLower().StartsWith(query.ToLower()) && !result.Contains(pageNames[i]))
                    {
                        result[resultCounter++] = pageNames[i];
                    }
                }
 
                for (int i = 0; i < resultCounter; i++)
                {
                    sb.Append(result[i]);
                    sb.Append(Environment.NewLine);
                }
 
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
 
        // Caching methods from http://johnnycoder.com/blog/2008/12/10/c-cache-helper-class/
        public bool Exists(string key)
        {
            return HttpContext.Current.Cache[key] != null;
        }
 
        public bool Get<T>(string key, out T value) where T:class
        {
            try
            {
                value = HttpContext.Current.Cache[key] as T;
                if (value == null)
                {
                    value = default(T);
                    return false;
                }
            }
            catch
            {
                value = default(T);
                return false;
            }
 
            return true;
        }
 
        public void Add<T>(T o, string key)
        {
            HttpContext.Current.Cache.Insert(
                key,
                o,
                null,
                DateTime.Now.AddMinutes(3600),
                System.Web.Caching.Cache.NoSlidingExpiration);
        }
    }
}

The FilterRole class

using System;
using EPiServer.Core;
using EPiServer.Filters;
 
namespace FV.Templates.FV.Filters
{
    public class FilterRole : IPageFilter
    {
        public string RoleName
        {
            get;
            set;
        }
 
        public FilterRole()
        {
        }
 
        public FilterRole(string roleName)
        {
            this.RoleName = roleName;
        }
 
        public void Filter(PageDataCollection pages)
        {
            for (int i = pages.Count - 1; i >= 0; i--)
            {
                bool isMatch = false;
                var acl = pages[i].ACL;
 
                if (acl != null)
                {
                    foreach (var role in acl)
                    {
                        if (role.Key == this.RoleName)
                        {
                            isMatch = true;
                        }
                    }
                }
 
                if (!isMatch)
                {
                    pages.RemoveAt(i);
                }
            }
        }
 
        public void Filter(object sender, FilterEventArgs e)
        {
            this.Filter(e.Pages);
        }
 
        public bool ShouldFilter(PageData page)
        {
            throw new NotImplementedException();
        }
    }
}

Download the code

Custom Property: ShareIt

You’ve probably seen the addthis/sharethis toolbars on various sites that let you easily share a page on social media sites. I’ve made a custom property that allows editors to create their own, from a predefined list of sites (this list can easily be updated with new ones).

Result

Here are some pictures of the end result, both in view and edit mode.

View mode

Picture of ShareIt in EPiServer view mode

Picture of ShareIt expanded in EPiServer view mode

Edit mode

Picture of ShareIt in EPiServer edit mode

To add other sites you simply drag and drop them from Social media sites into Active social media sites.

Picture of ShareIt in EPiServer edit mode

Picture of ShareIt in EPiServer edit mode

The number of icons to show textfield is the number of icons to display on the page (the show more link will show the rest). No value here means that all icons will be displayed.

Picture of ShareIt expanded in EPiServer view mode

Installation

To add ShareIt to your site download the code (bottom of the page), unzip it, go to the install directory and copy FV.ShareIt.dll file into your sites bin folder, copy the language file into the lang folder and then copy the whole FV.ShareIt folder to your sites root folder.

Example

You can now add the ShareIt property to your Page Type. To display it on your page you just need to add an EPiServer Property control and a link to the css file (located under /FV.ShareIt/Styles/styles.css).

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
    <link href="/FV.ShareIt/Styles/styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
        <EPiServer:Property runat="server" PropertyName="MyShareItPropertyName" />
    </form>
</body>
</html>

Extending

To add more predefined sites you just edit SocialMediaSites.xml (located in the xml folder).

<site>
        <name>del.icio.us</name>
        <url><![CDATA[http://delicious.com/post?url={0}&title={1}&notes={2}]]></url>
        <cssclass>delicious</cssclass>
</site>

Where {0} is the url of the page, {1} the title and {2} the introduction (MainIntro).

Downlod ShareIt

© Copyright Frederik Vig. Based on Fluid Blue theme