Public profile pages for your Umbraco members [Part 1]

Creating public profile pages without creating a profile node for each one part one - The Url Slug

While writing this first blog, I realised that this might be better as a mini-series so I'm going to break this out into individual blog posts.

Introduction

If you have a social orientated website which allows members to register an account, it's quite likely you will want to have a public profile page for your members. However, out of the box Umbraco doesn't make this "easy". Member's don't have Url's (as far as I know) which means there is no way to route traffic.

The quickest way to over come this is to create a profile node for each member by hooking into member saved event and wire one up, but this isn't particularly good for long term maintenance. If your site ends up with lots of members your content tree will grow uncontrollably. Please don't do this!

There is, however, alternative ways that can make things much more sustainable. The approach this article series will cover is by using a Custom Route and a Render Mvc Controller, and results in profiles having their own Unique URL.

Setup

The code shared in this article is the basis for a real world solution and has been tested on Umbraco v8.9, using Models Builder in AppData mode. The reason I'm specifically sharing this information is that I've chosen to extend one of the generated models.

What will be covered?

  1. Generating a unique slug based on a members name
  2. Creating a Custom route and a custom UmbracoVirtualNodeRouteHandler
  3. Creating a custom UmbracoVirtualNodeRouteHandler - coming soon
  4. Automatic creation of a "base node" with a fixed name. - coming soon

There might well be more things covered but the above are the 4 main areas that come together to allow this to work.

Generating a unique slug based on a members name

In this scenario my Umbraco Member Type has been extended to contain 3 new fields:

  • First name - Text box
  • Last name - Text box
  • Url Slug - Label (string)

The first name and last name fields are populated via a registration form on the site however Url Slug currently doesn't get generated, so the first thing to tackle is how can we generate a unique slug for members.

There are multiple parts to this step, the first thing to cover is why? Why do we want to use a unique slug instead of something that is already available? Well we could use the Member Id, however as this is just a number it could be used by crawlers to scrape profile pages using enumeration. Not ideal! Next option is the Key of a member, but this is a GUID; it doesn't look great and it definitely isn't easily memorable. So what about using the members First and Last names? Well this would work, but it's quite easy for there to be two members who register with the same name.

Although not 100% effective, let's run with the idea of using the First and Last name to create the Url slug for a member. To do this, we are going to want to hook into the Saving event of the Member service. Although I'm not going to cover exactly how you set this bit up, you can read about it via the official docs. Once we are hooked up to the Saving event of the member service, we can look to start generating our Url Slug.

private void MemberService_Saving(IMemberService sender, 
          Umbraco.Core.Events.SaveEventArgs<Umbraco.Core.Models.IMember> e)
{
    //CM is a short namespace for Umbraco.Web.PublishedModels
    foreach(var member in e.SavedEntities)
    {
        if(member.IsPropertyDirty(CM.Member.GetModelPropertyType(p => p.FirstName).Alias) || 
            member.IsPropertyDirty(CM.Member.GetModelPropertyType(p => p.LastName).Alias) || 
            string.IsNullOrWhiteSpace(member.GetValue<string>(
                     CM.Member.GetModelPropertyType(p => p.UrlSlug).Alias)))
        {
            UpdateProfileUrlSlug(member);
        }
    }
}

We only want to update the Url slug if the first or last name fields have changed, or if it has not been set at all, which is what the checks above are checking. Now, let's look at the UpdateProfileUrlSlug method. This method is what is actually responsible for creating the URL slug.

private void UpdateProfileUrlSlug(Umbraco.Core.Models.IMember member)
{
    var firstName = member.GetValue<string>(CM.Member.GetModelPropertyType(p => p.FirstName).Alias);
    var lastName = member.GetValue<string>(CM.Member.GetModelPropertyType(p => p.LastName).Alias);
    var name = string.Join("-", firstName, lastName).ToUrlSegment().ToLower();
    member.SetValue(CM.Member.GetModelPropertyType(p => p.UrlSlug).Alias, name);
}

Okay, so the above is creating a Url Slug for the member in the format of - but as mentioned there is a problem with this. People have the same name as other people so it's likely that it won't belong before there is a conflict. The means we need to check for uniqueness which, you'd think, would be quite straight forward. Well, the checking is. Umbraco very kindly has a little known method available that allows you to get all members based on the value of a property. This method exists on the IMemberService interface so we can take advantage of Umbraco's DI (Dependency Injection) and have this passed to us.

So, the next step is searching members based on the Url Slug to see if any of them have an entry that matches our current slug.

var memberSlugs =
      memberService.GetMembersByPropertyValue(
          CM.Member.GetModelPropertyType(p => 
              p.UrlSlug).Alias, 
          name, 
          Umbraco.Core.Persistence.Querying.StringPropertyMatchType.StartsWith)
     .Select(m => m.GetValue<string>(CM.Member.GetModelPropertyType(p => p.UrlSlug).Alias)).ToList();

So, those paying a bit of extra attention might have noticed I'm searching for terms that start with the name variable from the previous code snippet, and hopefully are asking why. Well, at the start, I mentioned that this way of generating a Url Slug could easily result in conflicts, so we need to think about making this unique. When this issue cropped up for me, I recalled that Anthony Dang had tweeted something about generating unique strings not that long ago, so I reached out:

And, I was lucky, he responded with a link to his repository on GitHub, #H5YR!

This great repo proved to be exceptionally helpful, although I couldn't use it exactly as it was. The code in the repo would add an incrementing numeric suffix to name if it wasn't unique in the following format (n) which isn't great for a URL. Instead I had to modify the code slightly so that I could append the suffix in the format of -n.

To achieve this I had to modify the StructuredName from Anthony's repo in 2 places:

private const string Suffixed_Pattern = @"(.*)\s\-(\d+)$";

//and

public string FullName
{
    get
    {
        string text = string.IsNullOrWhiteSpace(Text) ? Text.Trim() : Text;
        return Number > 0 ? $"{text}-{Number}" : text;
    }
}

With these changes, we can update the method for creating the Url Slug so that it can generate unique slugs for each member that is being saved.

private void UpdateProfileUrlSlug(Umbraco.Core.Models.IMember member)
{
    var firstName = member.GetValue<string>(CM.Member.GetModelPropertyType(p => p.FirstName).Alias);
    var lastName = member.GetValue<string>(CM.Member.GetModelPropertyType(p => p.LastName).Alias);
    var name = string.Join("-", firstName, lastName).ToUrlSegment().ToLower();
    var memberSlugs =
           memberService.GetMembersByPropertyValue(
               CM.Member.GetModelPropertyType(p => 
                    p.UrlSlug).Alias, 
                name, 
                Umbraco.Core.Persistence.Querying.StringPropertyMatchType.StartsWith)
            .Select(m => 
                m.GetValue<string>(CM.Member.GetModelPropertyType(p => p.UrlSlug).Alias)).ToList();

    var uniqueName = NameGenerator.GetUniqueName(memberSlugs, name);
    member.SetValue(CM.Member.GetModelPropertyType(p => p.UrlSlug).Alias, uniqueName);
}

So, this is where we will end this first post of the series. You can now get your Umbraco Member set up nicely with a Url Slug based on their first and last name. This set's us up nicely for the next stages of this series. I hope you find this post useful, if you do or have any questions, please reach out to me on Twitter