Sitefinity: Index and Search Events

NOTE: This post has been superceded by the Sitefinity Toolkit whcih contains a search provider for news, events, and generic content items. This post remains for reference only.

Sitefinity has a very useful search-indexing feature that works pretty much out-of-the-box to index pages, blogs and news. Unfortunately, there doesn’t seem to be any built-in provider for indexing events. Since I’ve changed the 404 page to include search results, I want to also include upcoming events in those results, just in case a date has changed or something.

This option may be available after the next release, but as of version 3.5, I couldn’t find any information in the developer manual on how to create a custom index provider… Fortunately, I found an excellent resource in the sitefinity forums that contained a CustomIndex Provider sample from Nikola on the Telerik team. In this very brief post I’ll show you how easily this can be modified to index events (or anything, really).

First, download the example from the sitefinity forum post here. Extract it to a subfolder of your App_Code folder, which I called EventsIndexProvider. You should get 4 class files: CustomIndexerInfo.cs, CustomIndexProvider.cs, SettingsControl.cs and ViewControl.cs.

The first thing I did was rename the first two to EventIndexerInfo.cs and EventIndexProvider.cs as well as renaming the classes within to match. In addition, I changed the namespace inside each file from CustomIndex to EventIndex. While this is certainly not a necessary step, it will help to keep you organized should you decide to add more index providers in the future.

From here it’s pretty straightforward: change the Name and Description properties in the EventIndexProvider class to match your index provider behavior.

/// <summary>/// Defines the name of the provider. This name is used to mange providers within Indexing Service./// </summary>public string Name
{
    get {
        return "EventsIndexProvider";
    }
}

/// <summary>/// Provides detailed description of the client/// </summary>public string Description
{
    get {
        return "Provides indexing for events.";
    }
}

/// <summary>/// Meta fields for this provider/// </summary>protected string[] MetaFields
{
    get {
        return new string[]{
            "Title" };
    }
}

Also, for some reason, the EventIndexerInfo class did not implement the IIndexerInfo interface, so be sure to include those as well.

#region IIndexerInfo Members

public string Culture
{
    get { return string.Empty; }
}

public Guid ItemID
{
    get { return Guid.Empty; }
}

#endregion

Now all that is left is to modify the GetContentToIndex method to index the event items. This is a simple matter of using the EventsManager to retrieve the events and index each one. Be sure to modify the url parameter to match the structure of your events page.

// get current eventsEventsManager mgr = new EventsManager("Events");
IMetaSearchInfo[] filters = new IMetaSearchInfo[2];
filters[0] = new MetaSearchInfo(MetaValueTypes.DateTime, "Expiration_Date", DateTime.Now, SearchCondition.GreaterOrEqual);
filters[1] = new MetaSearchInfo(MetaValueTypes.DateTime, "Publication_Date", DateTime.Now, SearchCondition.LessOrEqual);
IList events = mgr.Content.GetContent("Event_Start ASC", filters);

foreach (IContent ev in events)
{
    Hashtable metaFields = new Hashtable();            
    foreach (string key in MetaFields)
    {
        metaFields.Add(key, "");
    }

    metaFields["Title"] = ev.GetMetaData("Title");

    list.Add(
        new EventIndexerInfo(
        string.Format("/events/details{0}.aspx", ev.Url),
        metaFields,
        ev.Content.ToString())
        );
}

return list.ToArray();

Easy or what? That’s all there is to it! All we have to do now is include the index provider in web.config:

<add name="EventIndex" type="EventIndex.EventIndexProvider, App_Code" settingsControl="EventIndex.SettingsControl" viewSettingsControl="EventIndex.ViewControl" description="Provides indexing for events." />

Finally simply navigate to the Administration page of the Sitefinity admin and add include this new index in the search service and reindex! Events will now be included in search results, and show up on the 404 error page as well. For an example take a look at this invalid page on the McAllen Public Library website: http://www.mcallenlibrary.net/art_walk.aspx and notice that the artwalk event is listed in the results (at the top in fact!).

I hope that this was helpful to someone out there! I’ve included a link to a zip of the completed files available from mybloop below. As always your comments are welcome and appreciated.

Download no longer available. This project has been merged into the Sitefinity Toolkit.

On a somewhat related note, I am in the process of making a new website into which I will migrate all of my software and development content. The website selarom.com will continue to be my personal website, with all my music, videos, and long-winded rants. But now that I’m dedicating more time to developing, I think a more professional site is warranted. Stay tuned and please pardon the inevitable issues and broken links during the transition. Thanks for reading!

Sitefinity: Custom 404 Page With Related Links

One of the more interesting challenges I’ve encountered with Sitefinity has been how to handle error pages, specifically the 404 error that should be returned if a page is not found. Sitefinity uses the built in ASP.NET error handling mechanism, which allows you to redirect to a specific page based on the error code returned by the server. This means if a page is not found, you can catch that 404 error, and redirect to a custom 404 page informing the user of the error while avoiding the dreaded yellow screen of death.

Unfortunately, since the 404 error was trapped, and a page was served, the resulting response is 200 (or “ok”) instead of the 404 “not found” that it should be. That means if someone goes to, say yoursite.com/news/defalut.aspx instead of yoursite.com/news/default.aspx, they will be redirected to an error page such as yoursite.com/404.aspx?aspxerrorpath=/news/defalut.aspx. As you can see, it did “find” a page, so technically it’s not a 404 error!

In addition to that, I want my error page to be more helpful than just telling the user there was an error. I want them to be able to see a list of possible reslated links that the user MAY have been looking for. While Google provides such a tool in their Webmaster Tools suite, it unfortunately requires that you do NOT redirect to a generic page. Since asp.net redirects to 404.aspx, the google tool will always try to search for that term instead.

Google's 404 Widget

Fortunately, through the use of Sitefinity’s search API, we can search our own index for possible links and display them to the user on the 404 page! It’s actually very simple so let’s go right to the code.

404 Status

The first thing we need to do is change the response code so that the page returns the correct 404 NOT FOUND error code instead of the 200 OK code that it returns by default. This turns out to be as simple as chaging the Response.StatusCode in the Code-behind for the 404 page. However, while researching this, I discovered that there is a problem if your 404 page is using a Master Page (which mine is) that requires an extra step. Instead of just changing the Response.StatusCode in the Page_Load, we have to actually override the Render method:

protected override void Render(HtmlTextWriter writer)
{
    base.Render(writer);
    Response.StatusCode = 404;
}

Now the 404.aspx page will correctly return the 404 NOT FOUND error code, ensuring that search engines do not crawl invalid pages!

Displaying Related Page Links

Now that we’re returning the correct response code to the search engines, we also want to show some more helpful information to our visitors. This is especially helpful in cases where you’ve renamed a page or news item, since in Sitefinity, these titles are what form the urls for accessing the content.

Fortunately this is made simple due to the fact that our custom error page traps the original request in the aspxerrorpath querystring field. All we have to do is get that parameter, parse through it and create a new search query to pass to the Sitefinity Search Service.

// attempt to locate desired page  if (string.IsNullOrEmpty(Request.QueryString["aspxerrorpath"].ToLower())) return;

string[] req = Request.QueryString["aspxerrorpath"].Split('/');
if (req.Length == 0) return;

string q = req[req.Length - 1].Replace('_', ' ');
if (q.Contains("default.aspx") && req.Length > 1)
q = req[req.Length - 2];

Notice I used the / character as a delimiter, then replaced the _ with blank spaces, and use the text after the last slash (/) as my query. If that happens to be “default.aspx” (which is likely to return hundreds of results), I grab the next previous item in the url. Feel free to experiment with different logic to suit your site’s URL structure and piece together a valid search.

Now we need to add a repeater to write our results to the page, and fortunately, we can copy and paste this straight from an existing search results control located in /Sitefinity/ControlTemplates/SearchResult.ascx.

<asp:Repeater ID="rptResults" runat="server"> <HeaderTemplate> <h2>Possible Related Links</h2> <dl class="searchResults"> </HeaderTemplate> <ItemTemplate> <dt><strong><a href='<%#DataBinder.Eval(Container.DataItem, "Url")%>'> <%#DataBinder.Eval(Container.DataItem, "Title")%></a></strong></dt> <dd> <%#DataBinder.Eval(Container.DataItem, "Snippet")%></dd> <dd> <em><a href='<%#DataBinder.Eval(Container.DataItem, "Url")%>'> <%#DataBinder.Eval(Container.DataItem, "Url")%></a> </em> </dd> </ItemTemplate> <FooterTemplate> </dl> </FooterTemplate> </asp:Repeater>

Finally, all we do is use the Search Manager to issue a query, and bind the results to our repeater. There are several overloads to the search method you can use, but I used one that is actually not documented that takes the paramters (string SearchQuery, string SearchIndexName, int StartIndex, int MaxResults, string SearchMode, out int ResultCount).

int count;
IList<Telerik.Search.Engine.ResultItem> results = Telerik.Search.Engine.SearchManager.Search(q, "McAllen-NET", 0, 5, "AnyWord", out count);
if (count == 0)
     rptResults.Visible = false;
else
{
     rptResults.DataSource = results;
     rptResults.DataBind();
}

And that’s all there is to it! Take a look at this in action by visiting this link: http://www.mcallen.net/news/default/2009-01-05/28th_annual_golden_age_olympics.aspx. The event is actually supposed to be the 29th Annual Golden Age Olympics, but since it was entered incorrectly, the link is now broken since the title was changed. However, if you click the link, you’ll see that the first result is the correct one to the news item. Not too shabby!

I hope that this article was helpful. Feel free to submit your comments and criticisms (I can take it!) As always, thanks for reading!