Article Page Definition and URL Routing

Custom functionality may require that URLs be defined that can be referenced by code to enable specific workflows (editing content, viewing content, listing content, etc). New pages can be declared in code at custom routed URLs to provide reliable URLs for specific functionality to custom plugins and UI extensions.

Routed Pages vs. Custom Pages

Custom pages are useful for custom UI specific to a single blog, group, or site that is not required as part of a larger application. When a page/URL must exist to enable a workflow, a routed page may be a better option. The table below shows all differences between routed pages and custom pages:

  Routed Pages Custom Pages
Supports parsing the URL into a programmatic context Yes

No

Defined within a specific theme instance (a single blog, group, site, etc) No

Yes

Has a factory default implementation Yes

No

Created in code Yes

No

Created in the user interface No

Yes

Requires /p/ in URL path No

Yes

Defining Site Pages

The INavigable Plugin type is used to define routed site pages.  In addition to the base IPlugin members, INavigable adds the following:

void RegisterUrls(IUrlController controller);

The RegisterUrls method provides a reference to the IUrlController.  The controller allows you to register urls with the platform.

Example 1:  Site Page

The Site Page example will create a blank page at the root of the site with the URL "custom."  For example, if your community URL was  http://mycommunity, the new page would be found at http://community/custom.

using Telligent.Evolution.Extensibility.Urls.Version1;

namespace Telligent.Evolution.Examples
{
    public class CustomSitePageExample : INavigable
    {
        void INavigable.RegisterUrls(IUrlController controller)
        {
            controller.AddPage("page-custom", "custom", null, null, "page-custom", new PageDefinitionOptions());
        }

        public string Description
        {
            get { return "Define a custom site page"; }
        }

        public void Initialize()
        {
        }

        public string Name
        {
            get { return "Custom Site Page Example"; }
        }
    }
}

We use the controller.AddPage method to add a new widget page.  For additional information on the IUrlController and its methods see the IUrlController documentation.

Example 2: Members Page

The Members page adds a page for each user on the site at http://mycommunity/{username}/custompage.  

using System;
using System.Web;
using Telligent.Evolution.Extensibility;
using Telligent.Evolution.Extensibility.Api.Version1;
using Telligent.Evolution.Extensibility.Urls.Version1;
using Telligent.Evolution.Extensibility.Version1;

namespace Telligent.Evolution.Examples
{
    public class CustomPageExamples : INavigable, ITranslatablePlugin
    {
        private ITranslatablePluginController _translationController;

        void INavigable.RegisterUrls(IUrlController controller)
        {
            object userNameConstraints = new { UserName = @"^[a-zA-Z0-9\-\._]+$" };

            controller.AddPage("page-members-custom", "members/{UserName}/custompage", null, userNameConstraints, "page-members-custom", new PageDefinitionOptions()
            {
                DefaultPageXml = @"<contentFragmentPage pageName=""page-members-custom"" isCustom=""false"" layout=""Content"" themeType=""0c647246-6735-42f9-875d-c8b991fe739b"">
      <regions>
        <region regionName=""Content"">
          <contentFragments>
            <contentFragment type=""Telligent.Evolution.ScriptedContentFragments.ScriptedContentFragment, Telligent.Evolution.ScriptedContentFragments::796a5296bbb74d24b4113eee9352d431"" showHeader=""False"" cssClassAddition=""no-wrapper with-spacing responsive-1"" isLocked=""False"" configuration="""" />
         </contentFragments>
        </region>
      </regions>
      <contentFragmentTabs />
    </contentFragmentPage>",
                TitleFunction = () => _translationController.GetLanguageResourceValue("page-members-custom"),
                DescriptionFunction = () => _translationController.GetLanguageResourceValue("page-members-custom-description"),
                ParseContext = ParseUserContext
            });
        }

        private void ParseUserContext(PageContext context)
        {
            var userName = context.GetTokenValue("UserName");

            if (userName != null)
            {
                var usersApi = Apis.Get<IUsers>();
                string decodedUserName = PublicApi.Url.DecodeFileComponent(userName.ToString());
                var user = usersApi.Get(new UsersGetOptions() { Username = decodedUserName });

                ContextItem contextItem = null;

                if (user != null && !user.HasErrors())
                {
                    contextItem = new ContextItem()
                    {
                        TypeName = "User",
                        ApplicationId = user.ContentId,
                        ApplicationTypeId = usersApi.ContentTypeId,
                        ContainerId = user.ContentId,
                        ContainerTypeId = usersApi.ContentTypeId,
                        ContentId = user.ContentId,
                        ContentTypeId = usersApi.ContentTypeId,
                        Id = user.Id.ToString()
                    };
                }
                else
                {
                    if (usersApi.AccessingUser.IsSystemAccount.Value)
                    {
                        var url = Apis.Get<ICoreUrls>().LogIn(new CoreUrlLoginOptions() { ReturnToCurrentUrl = true });
                        HttpContext.Current.Response.Redirect(url);
                    }
                    else
                    {
                        throw new CustomException(String.Format(_translationController.GetLanguageResourceValue("UserNotFoundException"), decodedUserName));
                    }
                }

                if (contextItem != null)
                    context.ContextItems.Put(contextItem);
            }
        }

        Translation[] ITranslatablePlugin.DefaultTranslations
        {
            get
            {
                var t = new Translation("en-us");

                t.Set("page-members-custom", "Sample User Page");
                t.Set("page-members-custom-description", "Shows a page at the url members/{username}/custompage.");
                t.Set("UserNotFoundException", "User {0} not found.");

                return new Translation[] { t };
            }
        }

        public void SetController(ITranslatablePluginController controller)
        {
            _translationController = controller;
        }

        public string Description
        {
            get { return "Define a custom user page"; }
        }

        public void Initialize()
        {
        }

        public string Name
        {
            get { return "Custom User Page Example"; }
        }
    }

    public class CustomException : Exception, Telligent.Evolution.Extensibility.Version1.IUserRenderableException
    {
        public CustomException() { }
        public CustomException(string message) : base(message) { }

        public string GetUserRenderableMessage()
        {
            return this.Message;
        }
    }
}

In this example the Url parameter is defined as "members/{UserName}/custompage", the  new { UserName = @"^[a-zA-Z0-9\-\._]+$" } constraint has also been applied to the UserName variable.  The Url will only respond if the requested Url matches the format and constraints that are defined when adding the page.

The example also uses some of the options available in the PageDefinitionOptions parameter.  DefaultPageXml is used to define the factory default set of widgets for this page.  The TitleFunction and DescriptionFunction options determine the page's name and description when working with the page in the theme editing panels.  ParseContext defines the method that is used to examine the Url and determine what item are in context for this page.  

The ParseContext method in the example performs a few functions.  First we attempt to load the username from the URL.  If it's valid, we add that user to the page's ContextItems collection. This in turn enables other methods in the platform.  For instance, having a user in the ContextItems collection allows the $core_v2_user.Current velocity call to return that user.  If the user is not valid we either redirect the user to login to the site if they are not currently logged in or we throw an exception identifying the username to be invalid.

Defining Application Pages

The IApplicationNavigable Plugin type is used to extend existing applications (and groups) by adding new routed pages.  In addition to the base IPlugin members, IApplicationNavigable adds the following:

void RegisterUrls(IUrlController controller);

The RegisterUrls method provides a reference to the IUrlController.  The controller allows you to register urls with the platform.

Guid ApplicationTypeId { get; }

The ApplicationTypeId property specifies which application type will display the new page. 

Example 3: Blog Raw Page

This sample adds a url to a blog that is handled by an IHttpHandler.  The page defined in the sample would have the following URL pattern: http://community/{grouppath}/{b}/{blogappkey}/raw.

using System;
using System.Linq;
using System.Web;
using Telligent.Evolution.Extensibility;
using Telligent.Evolution.Extensibility.Api.Version1;
using Telligent.Evolution.Extensibility.Urls.Version1;

namespace Telligent.Evolution.Examples
{
    public class CustomBlogRawExample : IApplicationNavigable
    {
        Guid IApplicationNavigable.ApplicationTypeId
        {
            get { return Apis.Get<IBlogs>().ApplicationTypeId; }
        }

        void IApplicationNavigable.RegisterUrls(IUrlController controller)
        {
            controller.AddRaw("blog-raw", "{WeblogApp}/raw", null, null,
               (a, p) =>
               {
                   var handler = new CustomBlogRawHttpHandler();
                   handler.ProcessRequest(a.ApplicationInstance.Context);
               }, new RawDefinitionOptions() { ParseContext = ParseBlogContext });
        }

        private void ParseBlogContext(PageContext context)
        {
            var appKey = context.GetTokenValue("WeblogApp");
            var blogsApi = Apis.Get<IBlogs>();
            var groupsApi = Apis.Get<IGroups>();

            if (appKey != null)
            {
                var groupItem = context.ContextItems.GetAllContextItems().FirstOrDefault(a => a.ContentTypeId == groupsApi.ContentTypeId);
                if (groupItem != null)
                {
                    var blog = blogsApi.Get(new BlogsGetOptions() { GroupId = int.Parse(groupItem.Id), Key = appKey.ToString() });
                    if (blog != null)
                    {
                        var contextItem = new ContextItem()
                        {
                            TypeName = "Blog",
                            ApplicationId = blog.ApplicationId,
                            ApplicationTypeId = blogsApi.ApplicationTypeId,
                            ContainerId = blog.Group.ApplicationId,
                            ContainerTypeId = groupsApi.ContentTypeId,
                            ContentId = blog.ApplicationId,
                            ContentTypeId = blogsApi.ApplicationTypeId,
                            Id = blog.Id.ToString()
                        };

                        context.ContextItems.Put(contextItem);
                    }
                }
            }
        }

        public string Description
        {
            get { return "Define custom blog raw url"; }
        }

        public void Initialize()
        {
        }

        public string Name
        {
            get { return "Custom Blog Pages"; }
        }
    }

    public class CustomBlogRawHttpHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            var blog =
                PublicApi.Url.CurrentContext.ContextItems.GetAllContextItems()
                    .FirstOrDefault(b => b.ContentTypeId == PublicApi.Blogs.ContentTypeId);

            if (blog != null)
            {
                var posts = Apis.Get<IBlogPosts>().List(new BlogPostsListOptions() { BlogId = int.Parse(blog.Id)} );
                foreach (var post in posts)
                    context.Response.Write(post.Title + "<br />");
            } 

            context.Response.End();
        }
    }
}

The URL is defined as {WeblogApp}/raw, since we are adding a page to the blog application, the portion of the URL up to and including the blog application identifier ('/b/') accounted for.  You are only required to define the portion of the url inside the group or application. 

What is the difference between adding a widget page and adding a raw page?

The IUrlController provides the option to add a widget page or a raw page.  A widget page is a themeable page that supports a widget-based layout.  Raw pages give complete control over the output when defining the Url.  The typical use case for raw pages are RSS Feeds, file downloads or Xml.