mandag den 29. september 2014

Not able to change field values after installing a package with the template having it's fields changed.

Okay, so this is an interresting issue.

First some background - when changing a field on a template in Sitecore to/from "Shared", the value gets moved between the SharedFields table and the UnversionedFields/VersionedFields tables in the database.

This is also why it warns, that changing this might take a while (and also result in data loss, but thats not because of this issue).

If you have items created from a template, where a field is shared, and you install a package that has a new version of the template, where the field is no longer shared, things will break.

In this case, if the field was changed to "Non-shared" using the package installer, you will not be able to change the value on the items based on this template.

This is likely because the value is not moved from the SharedFields table, like it would be if the field was changed from the content editor - resulting in your changes being saved to the right table, but everything being read from the SharedFields table, which is never changed.

You can test this yourself, by following these steps:

  1. Install two identical Sitecore solutions - I'll refer to them as A and B
  2. In A, create a template with a simple text field, make the text field shared.
  3. In A, create a few items based on this template, and fill out the field with some values, and save the items.
  4. In A, make a package with the template and the items.
  5. In B, install the package.
  6. In A, change the template, so the field is no longer shared.
  7. In A, create a package with just the template, and not the items.
  8. In B, install the package, and overwrite the template, template section and template field items.
  9. In B, try and change the value on some of the items, and notice how they will not save when clicking save.
I contacted Sitecore Support with this, and they found sent me a workaround that fixes the issue.

So if you are having this issue, contact Sitecore Support, and ask for support dll #310642 .

Do note, that this has to be installed before the package gets installed, since that is when the fix is applied, that moves the values to the right tables.

onsdag den 3. september 2014

A small processor to make sure page URL's are SEO friendly

If you have been in the business of creating web solutions for a while, you have most likely been asked about what to do to improve the SEO-friendliness of the current project you are working on.

Since SEO is not always clearly defined, I'll not go into details on everything that can be done, but instead pinpoint two things.

These two URL's are not the same, when being indexed by a search engine:

http://www.test.org/Some/Page
http://www.test.org/Some/Page/

Also, there two links are not the same:

http://www.test.org/Some/Page
http://www.test.org/Some/PaGe

Since users are able to type URL's, we cannot ensure that they always type them the way we want them to - but we can force the browser (and search engines) to act like we want.

One way (there are most likely several other ways) to do this, is to create a small processor, that looks at the incoming URL, and changes it a bit if needed.

So here we go - create a new class in Visual Studio, and enter the following code:

public class SeoUrlProcessor : HttpRequestProcessor
{
    public override void Process(HttpRequestArgs args)
    {
        if (args == null || args.Context == null)
        {
            return;
        }

        if (Sitecore.Context.Item == null)
        {
            return;
        }

        if (!Sitecore.Context.PageMode.IsNormal)
        {
            return;
        }

        // Extend this list with other sites you find will be broken by this.
        if (Sitecore.Context.Site.Name == "shell" || Sitecore.Context.Site.Name == "publishing")
        {
            return;
        }

        bool urlHasBeenChanged = false;
        string incomingUrl = args.Context.Request.RawUrl;

        // In case language embedding is used, this is needed to make sure incomingUrl contains the language.
        // Otherwise, the comparing of the URL's will not match up.
        if (!string.IsNullOrEmpty(args.Context.Request.Url.Query))
        {
            incomingUrl = incomingUrl.Replace(args.Context.Request.Url.Query, string.Empty);
        }

        UriBuilder builder = new UriBuilder(args.Context.Request.Url)
        {
            Path = incomingUrl
        };

        // This works like a one-time flip-bit, once it has been set to true, it stays as true.
        urlHasBeenChanged |= HandleTrailingSlash(builder);
        urlHasBeenChanged |= HandleCasing(builder);

        if (urlHasBeenChanged)
        {
            RedirectToUrl(args, builder.ToString());
        }
    }

    private static bool HandleTrailingSlash(UriBuilder builder)
    {
        if (!builder.Path.EndsWith("/"))
        {
            return false;
        }

        builder.Path = builder.Path.TrimEnd('/');

        return true;
    }

    private static bool HandleCasing(UriBuilder builder)
    {
        string destinationUrl = LinkManager.GetItemUrl(Sitecore.Context.Item);

        if (!builder.Path.Equals(destinationUrl, StringComparison.InvariantCultureIgnoreCase))
        {
            return false;
        }

        if (builder.Path == destinationUrl)
        {
            return false;
        }

        builder.Path = destinationUrl;

        return true;
    }

    private static void RedirectToUrl(HttpRequestArgs args, string url)
    {
        args.Context.Response.Clear();
        args.Context.Response.Headers.Add("Location", url);
        args.Context.Response.Status = "301 - Moved Permanently";
        args.Context.Response.StatusCode = 301;
        args.Context.Response.End();

        args.AbortPipeline();
    }
}

Save the file.

Let me explain the HandleCasing function, since that can be a bit confusing.

First we compare the URL's without looking at casing at all - if they aren't identical, we can be pretty sure the user has hit some kind of aliasing, since they are standing on this item, but the incoming URL is totally different - in that case, we don't wanna redirect the user anywhere.

Then, if they are identical, we compare them again - this time look at if the are completly identical - and if the aren't, that means that the casing is wrong somewhere (like the user typing "products" instead of "Products").

In that case, we replace the Path part of the URL with the one Sitecore's link manager generated, which ensures that every view of the page has the URL typed as it is inside Sitecore.

Finally, you just need to create an include file, for Sitecore to run this.
So create an XML file, and paste the following test into it, replacing Namespace, Classname and Assembly with your own values:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="Namespace.Classname, Assembly" patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Now you are done. :)

Try accessing a few pages, where you try adding a tailing slash to the URL, and where you try mixing the casing differently than inside Sitecore.