Understanding Redirects in Sitecore Headless with .NET Core Rendering Host

Introduction

In the ever-evolving realm of web development, Sitecore Headless has emerged as a powerful solution for crafting seamless digital experiences. With the integration of .NET Core Rendering Host, developers can harness the flexibility and efficiency of headless architecture. One crucial aspect of this synergy is the management of redirects, a fundamental mechanism for guiding users through the digital landscape. In this blog post, we’ll delve into the intricacies of redirects in Sitecore Headless within a .NET Core Rendering Host.

Understanding Sitecore Headless:

Sitecore Headless represents a paradigm shift in web development, decoupling the front end from the back end to enable greater flexibility and scalability. It allows developers to create content-rich applications by consuming data from Sitecore, a leading content management system (CMS), and presenting it through various channels without being bound to a specific front-end technology.

The Role of .NET Core Rendering Host:

.NET Core Rendering Host serves as the bridge between Sitecore and the front-end application, enabling seamless communication and data exchange. It empowers developers to build dynamic, content-driven websites using the technology stack of their choice while leveraging the robust features of Sitecore.

Redirects: Navigating the Digital Landscape:

Redirects play a pivotal role in shaping user journeys within a website. They ensure that visitors are directed to the appropriate content, enhancing user experience and optimizing the flow of information. In a headless architecture, redirects take on a nuanced role, requiring careful consideration for efficient implementation.

Types of Redirects in Sitecore Headless:

  1. 301 Permanent Redirects:
    • Ideal for cases where content has permanently moved.
    • Ensures search engines update their indexes accordingly.
  2. 302 Temporary Redirects:
    • Used when content has temporarily moved or is under maintenance.
    • Does not impact search engine indexes as significantly as a 301 redirect.
  3. Wildcard Redirects:
    • Offers the ability to redirect multiple URLs using a wildcard pattern.
    • Useful for managing dynamic content changes or restructuring.

Implementing Redirects in .NET Core Rendering Host:

  1. Sitecore.ContentSearch.Pipelines.HttpRequest.RedirectResolver:
    • Leverage Sitecore’s RedirectResolver to manage redirects efficiently.
    • Customize the resolver to suit specific project requirements.

As an alternative option and a common scenario within your Sitecore upgrade / migration, you can consider leveraging the evergreen Sitecore redirect module… Redirect Module bring to the table the Templates to handle your redirects structure such as:

and you can explore more about it from the github repo and shared source project here:

https://github.com/thecadams/301RedirectModule

using SharedSource.RedirectModule.Classes;
using SharedSource.RedirectModule.Helpers;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Links;
using Sitecore.Rules;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace XXXX.Foundation.Redirect.Pipelines.GetRedirectUrl
{
public class GetRedirectUrlProcessor
{
protected readonly IRedirectRepository RedirectRepository;
public GetRedirectUrlProcessor()
{
RedirectRepository = new RedirectRepository();
}
public void Process(GetRedirectUrlArgs args)
{
// Check for exact matches – priority over pattern matches.
if (Sitecore.Configuration.Settings.GetBoolSetting(Constants.Settings.RedirExactMatch, true))
{
args = CheckForDirectMatch(args.Database, args.RequestUrl, args.RequestPath, args);
if (!string.IsNullOrEmpty(args.RedirectUrl) || args.RedirectItem != null)
{
return;
}
}
// Next, we check for pattern matches because we didn't hit on an exact match.
if (Sitecore.Configuration.Settings.GetBoolSetting(Constants.Settings.RedirPatternMatch, true))
{
args = CheckForRegExMatch(args.Database, args.RequestUrl, args.RequestPathAndQuery, args);
if (!string.IsNullOrEmpty(args.RedirectUrl) || args.RedirectItem != null)
{
return;
}
}
// Rule matches prioriity on pattern match.
if (Sitecore.Configuration.Settings.GetBoolSetting(Constants.Settings.RedirRuleMatch, true))
{
args = CheckForRulesMatch(args.Database, args.RequestUrl, args.RequestPathAndQuery, args);
}
}
private bool IsMediaDirectMatch(string incomingUrl, string toMatchUrl)
{
if (incomingUrl == toMatchUrl)
{
return true;
}
if (toMatchUrl.StartsWith("sitecore/media library/"))
{
toMatchUrl = toMatchUrl.Replace("sitecore/media library", "/-/media");
}
if (!toMatchUrl.StartsWith("/-/media"))
{
return false;
}
if (incomingUrl == toMatchUrl)
{
return true;
}
// lastly, try matching by stripping extension and query string params
var regex = @"^(.*?)(\.[\w]{3,4}([?].+)?)?$";
var left = (Regex.IsMatch(incomingUrl, regex)) ? Regex.Match(incomingUrl, regex).Groups[1].Value : incomingUrl;
var right = (Regex.IsMatch(toMatchUrl, regex)) ? Regex.Match(toMatchUrl, regex).Groups[1].Value : toMatchUrl;
return left == right;
}
private GetRedirectUrlArgs CheckForDirectMatch(Database db, string requestedUrl, string requestedPath, GetRedirectUrlArgs args)
{
var isMediaRequest = requestedUrl.StartsWith("/-/media") || requestedPath.StartsWith("/-/media");
// Loop through the exact match entries to look for a match.
foreach (var possibleRedirect in RedirectRepository.GetRedirects(db, Constants.Templates.RedirectUrl, Constants.Templates.VersionedRedirectUrl, Sitecore.Configuration.Settings.GetSetting(Constants.Settings.QueryExactMatch)))
{
var redirectFromUrl = possibleRedirect[Constants.Fields.RequestedUrl];
if (isMediaRequest &&
(!IsMediaDirectMatch(requestedPath, redirectFromUrl)
&& !(Uri.IsWellFormedUriString(requestedUrl, UriKind.RelativeOrAbsolute)
&& Uri.TryCreate(requestedUrl, UriKind.RelativeOrAbsolute, out Uri requestedUri)
&& IsMediaDirectMatch(requestedUri.AbsolutePath, redirectFromUrl))))
{
continue;
}
else if (!isMediaRequest
&& !requestedUrl.Equals(redirectFromUrl, StringComparison.OrdinalIgnoreCase)
&& !requestedPath.Equals(redirectFromUrl, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var redirectToItemId = possibleRedirect.Fields[Constants.Fields.RedirectToItem];
var redirectToUrl = possibleRedirect.Fields[Constants.Fields.RedirectToUrl];
if (redirectToItemId.HasValue && !string.IsNullOrEmpty(redirectToItemId.ToString()))
{
var redirectToItem = db.GetItem(ID.Parse(redirectToItemId));
if (redirectToItem == null)
{
continue;
}
var responseStatus = GetResponseStatus(possibleRedirect);
args.RedirectItem = redirectToItem;
args.RedirectStatusCode = responseStatus;
return args;
}
if (redirectToUrl.HasValue && !string.IsNullOrEmpty(redirectToUrl.ToString()))
{
var responseStatus = GetResponseStatus(possibleRedirect);
args.RedirectUrl = redirectToUrl.Value;
args.RedirectStatusCode = responseStatus;
return args;
}
}
return args;
}
private GetRedirectUrlArgs CheckForRegExMatch(Database db, string requestedUrl, string requestedPathAndQuery, GetRedirectUrlArgs args)
{
// find a match
foreach (var possibleRedirectPattern in RedirectRepository.GetRedirects(db, Constants.Templates.RedirectPattern, Constants.Templates.VersionedRedirectPattern, Sitecore.Configuration.Settings.GetSetting(Constants.Settings.QueryExactMatch)))
{
var redirectPath = string.Empty;
if (Regex.IsMatch(requestedUrl, possibleRedirectPattern[Constants.Fields.RequestedExpression], RegexOptions.IgnoreCase))
{
redirectPath = Regex.Replace(requestedUrl, possibleRedirectPattern[Constants.Fields.RequestedExpression],
possibleRedirectPattern[Constants.Fields.SourceItem], RegexOptions.IgnoreCase);
}
else if (Regex.IsMatch(requestedPathAndQuery, possibleRedirectPattern[Constants.Fields.RequestedExpression], RegexOptions.IgnoreCase))
{
redirectPath = Regex.Replace(requestedPathAndQuery,
possibleRedirectPattern[Constants.Fields.RequestedExpression],
possibleRedirectPattern[Constants.Fields.SourceItem], RegexOptions.IgnoreCase);
}
if (string.IsNullOrEmpty(redirectPath))
{
continue;
}
// Query portion gets in the way of getting the sitecore item.
var pathAndQuery = redirectPath.Split('?');
var path = pathAndQuery[0];
var redirectToItem = db.GetItem(path);
if (redirectToItem == null)
{
if (LinkManager.GetDefaultUrlOptions() != null && LinkManager.GetDefaultUrlOptions().EncodeNames)
{
path = Sitecore.MainUtil.DecodeName(path);
}
redirectToItem = db.GetItem(path);
}
if (redirectToItem != null)
{
var responseStatus = GetResponseStatus(possibleRedirectPattern);
args.RedirectStatusCode = responseStatus;
args.RedirectItem = redirectToItem;
return args;
}
}
return args;
}
private GetRedirectUrlArgs CheckForRulesMatch(Database db, string requestedUrl, string requestedPathAndQuery, GetRedirectUrlArgs args)
{
//pattern match items to find a match
foreach (var possibleRedirectRule in RedirectRepository.GetRedirects(db, Constants.Templates.RedirectRule, Constants.Templates.VersionedRedirectRule, Sitecore.Configuration.Settings.GetSetting(Constants.Settings.QueryExactMatch)))
{
var ruleContext = new RuleContext();
ruleContext.Parameters.Add("newUrl", requestedUrl);
foreach (var rule in RuleFactory.GetRules<RuleContext>((IEnumerable<Item>)new Item[1] { possibleRedirectRule }, "Redirect Rule").Rules)
{
if (rule.Condition == null)
{
continue;
}
var stack = new RuleStack();
rule.Condition.Evaluate(ruleContext, stack);
if (!ruleContext.IsAborted && (stack.Count != 0 && (bool)stack.Pop()))
{
foreach (var action in rule.Actions)
{
action.Apply(ruleContext);
}
}
}
if (ruleContext.Parameters["newUrl"] != null && ruleContext.Parameters["newUrl"].ToString() != string.Empty && ruleContext.Parameters["newUrl"].ToString() != requestedUrl)
{
var responseStatus = GetResponseStatus(possibleRedirectRule);
args.RedirectUrl = ruleContext.Parameters["newUrl"].ToString();
args.RedirectStatusCode = responseStatus;
return args;
}
}
return args;
}
private static ResponseStatus GetResponseStatus(Item redirectItem)
{
var result = new ResponseStatus
{
Status = "301 Moved Permanently",
StatusCode = 301,
};
var responseStatusCodeId = redirectItem?.Fields[Constants.Fields.ResponseStatusCode];
if (string.IsNullOrEmpty(responseStatusCodeId?.ToString())) return result;
var responseStatusCodeItem = redirectItem.Database.GetItem(ID.Parse(responseStatusCodeId));
if (responseStatusCodeItem != null)
{
result.Status = responseStatusCodeItem.Name;
result.StatusCode = responseStatusCodeItem.GetIntegerFieldValue(Constants.Fields.StatusCode, result.StatusCode);
}
return result;
}
}
}
  1. Configuration in Sitecore:
    • Define redirects within the Sitecore content tree.
    • Utilize the Redirect Map feature to manage redirects centrally.
  2. Dynamic Redirects with Custom Code on the .net core rendering host:
    • Implement custom logic for dynamic redirects based on user behavior or external factors.
    • Ensure flexibility in adapting to evolving business needs.

Here you have several option, however the idea is to check that the layout service is returning a redirect item checking the appropriate template ID and process the redirect as per Sitecore instruction respecting if it is a permaent redirect and the target configured in Sitecore…

This code show how to check for the TemplateId returned by the layout service and how to “implement” and respect the redirect configured on Sitecore…

namespace XXXCUSTOM.Middleware
{
using Microsoft.AspNetCore.Http;
using Sitecore.AspNet.RenderingEngine;
using Sitecore.LayoutService.Client.Response.Model;
using Sitecore.LayoutService.Client.Response.Model.Fields;
using System.Threading.Tasks;
public class RedirMiddleware
{
private const string RedirectTemplateID = "XXXX-XXXX-XXXX-XXX-XXXX";
public RedirectMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var custrequest = context.GetSitecoreRenderingContext();
var custroute = custrequest.Response?.Content?.Sitecore?.Route;
if (custroute == null)
{
await _next(context);
}
if (!IsRedirect(route, context))
{
await _next(context);
}
}
private bool IsRedirect(Route route, HttpContext context)
{
//RUN Redirect LOGIC!!!
if (route == null)
{
return false;
}
if (route.TemplateId != RedirectTemplateID)
{
return false;
}
var isPermanent = false;
var redirectType = route.ReadField<ItemLinkField>("Response Status Code");
if (redirectType != null)
{
if (redirectType.Url != null)
{
if (redirectType.Url.Contains("301"))
{
isPermanent = true;
}
}
}
var redirectTextField = route.ReadField<TextField>("Redirect To Url");
if (!string.IsNullOrEmpty(redirectTextField.Value))
{
context.Response.Redirect(redirectTextField.Value, isPermanent);
return true;
}
var destinationField = route.ReadField<ItemLinkField>("Redirect To Item");
var destinationUrl = destinationField?.Url;
if (!string.IsNullOrEmpty(destinationUrl))
{
context.Response.Redirect(destinationUrl, isPermanent);
return true;
}
return false;
}
}
}

Best Practices for Redirect Management:

  1. Regularly Audit and Update Redirects:
    • Keep the redirect map up-to-date to reflect changes in content and URL structures.
  2. Test Redirects Thoroughly:
    • Conduct comprehensive testing to ensure redirects function as intended across various scenarios.
  3. Monitor and Analyze Redirect Performance:
    • Utilize analytics tools to monitor the impact of redirects on user behavior and site performance.

Conclusion:

In the dynamic landscape of Sitecore Headless with .NET Core Rendering Host, redirects serve as the guiding beacons that shape the user experience. By understanding the nuances of redirects and implementing them effectively, developers can ensure that users seamlessly navigate the digital terrain, enhancing engagement and satisfaction. As technology continues to advance, mastering the art of redirects in Sitecore Headless remains a crucial skill for crafting compelling and user-friendly web experiences and to keep your clients happy with the existing Redirects 🙂

Demystifying Cookie Handling in Sitecore Headless Rendering Host for .NET Core

Introduction

Sitecore Headless, a decoupled approach to content management, has revolutionized the way digital experiences are built and delivered. Its rendering host, a .NET core application, serves as the front-end gateway to Sitecore’s content repository, providing a seamless interface for rendering content and managing user interactions.

Cookies, ubiquitous in the digital landscape, play a crucial role in tracking user behavior, maintaining personalization preferences, and enabling authentication. In the context of Sitecore Headless, handling cookies within the rendering host requires careful consideration to ensure consistent user experience and to achieve functionalities between the Rendering Host and Sitecore Content Delivery server.

Cookie Handling Strategies

Several strategies can be employed to effectively manage cookies within the Sitecore Headless rendering host:

  1. Cookie Sharing: Configure the Sitecore instance and the rendering host to share the same cookie domain. This allows the rendering host to access and modify cookies set by the Sitecore instance, ensuring consistency across the user experience.
  2. Cookie Forwarding: Implement proxy settings that forward cookies from the rendering host to the Sitecore instance. This approach enables the Sitecore instance to track user behavior and maintain personalization preferences even when requests originate from the rendering host.
  3. X-Forwarded-For Header: Utilize the X-Forwarded-For header to provide the visitor’s IP address to the Sitecore instance. This information is crucial for accurate analytics and personalization, especially when proxy servers or load balancers are involved.
  4. Cookie Synchronization: Implement a mechanism to synchronize cookies between the rendering host and the Sitecore instance. This ensures that both systems maintain a consistent view of the user’s cookie state, preventing inconsistencies and potential errors.

Compliance Considerations

As with any cookie-handling practice, adhering to privacy regulations is paramount. The rendering host should implement appropriate mechanisms to inform users about cookie usage, obtain consent whenever necessary, and provide options for managing cookie preferences.

Technical Challenges & Workarounds

Within some circumstances it is not “practical” to share custom application cookies between the rendering host and Sitecore Pipelines / Content Resolvers therefore what you would “traditionally” manage with an application Cookie on the Content Delivery in the headless paradigma you would need to split the logic across the rendering host and the Sitecore Pipeline.

Within this code example I show you how to pass value between a cookie on the rendering host and an http header within the Sitecore pipeline.

SitecoreLayoutRequest sitecoreLayoutRequest = _requestMapper.Map(httpContext.Request);
string cookieValueFromContext = _httpContextAccessor.HttpContext.Request.Cookies["_XXXX_trk"];
if (!string.IsNullOrEmpty(cookieValueFromContext))
{
sitecoreLayoutRequest.AddHeader("XXXX", new string[] { cookieValueFromContext });
}
and once you are in the Sitecore Pipeline you can read your code from the header and use it to run IdentifyAs or any other Sitecore XDB related code….
if (HttpContext.Current?.Request?.Headers.AllKeys == null)
{
return null;
}
var customCookie = HttpContext.Current?.Request?.Headers["XXX"];
var result = ContactIdentificationManager.IdentifyAs(new KnownContactIdentifier("Salesforce.ContactId", customCookie));

Sitecore 404 handling in an Headless .net core application

Within a .net core Sitecore Headless implementation 404 is an important topic and probably any project require some code to handle it, within this post I am explaining how to implement in a simple way…

There are two elements to configure and to pay attention:

  1. Rendering Host customisation

you will need to create a Sitecore controller that’s a catch all controller for all the request to the layout service, here you can find a simple implementation

namespace XXXX.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Sitecore.AspNet.RenderingEngine;
using Sitecore.LayoutService.Client.Exceptions;
public class SitecoreController : Controller
{
public IActionResult Index()
{
var request = HttpContext.GetSitecoreRenderingContext();
if (request == null || request.Response == null)
{
return new EmptyResult();
}
//Check if layout service call return any error
if (request.Response.HasErrors)
{
//loop through errors
foreach (var error in request.Response.Errors)
{
switch (error)
{
//this is the case of 404 errors
case ItemNotFoundSitecoreLayoutServiceClientException _:
Response.Redirect("/404");
break;
}
}
}
return View();
}
}
}

2. Content Delivery / Layout service configuration

Here no customisation is required and if you are happy to have a simple implementation, nothing to do to customise it the rendering host, obviously in most sitecore implementation you may have Redirects / Item Resolvers and custom errors pages on SXA to play a role…

3. Content authoring

Within most of Sitecore implementation you will have an item called 404 defined on the root of the site so that Conent Editors can customise the 404 page to serve to customers…

Limitations

this code sinppets is very simple and may not suit you well within your implementation, as an example not all the customer want to call the Page not found 404 and this approach will support the same name for page not found item for multiple site…. Just to clarify if you have 10 sites, all the 10 sites will be forced to call the PageNotFound page as 404 but obviously each site could have different content to show for 404 page….

Other extension to this code could be to set the status code of 404 page, some SEO expert reccomend do it, others do not consider it mandatory…