So what we want to do is attach our selves to the MovingContent event, get a hold of the old URL and the content GUID, store that data, and in our custom 404 controller check for it so that we might perform a redirect (otherwise we display our normal 404 page).
using EPiServer.Core; using EPiServer.Framework; using EPiServer.Framework.Initialization; using EPiServer.ServiceLocation; using EPiServer.Web.Routing; namespace SEO.UrlKeeper { [InitializableModule] [ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class InitializationModule : IInitializableModule { private static bool _initialized; public void Initialize(InitializationEngine context) { if (_initialized) { return; } var contentEvents = ServiceLocator.Current.GetInstance(); contentEvents.MovingContent += contentEvents_MovingContent; _initialized = true; } private void contentEvents_MovingContent(object sender, EPiServer.ContentEventArgs e) { var urlResolver = ServiceLocator.Current.GetInstance(); var repository = ServiceLocator.Current.GetInstance(); var item = new UrlKeeperItem { ContentGuid = e.Content.ContentGuid, OldPath = urlResolver.GetUrl(e.ContentLink) }; repository.Save(item); } public void Preload(string[] parameters) { } public void Uninitialize(InitializationEngine context) { var contentEvents = ServiceLocator.Current.GetInstance(); contentEvents.MovingContent -= contentEvents_MovingContent; } } }
To persist the data we use EPiServer's Dynamic Data Store. The repository is quite simple.
using System; using System.Linq; using EPiServer.Data; using EPiServer.Data.Dynamic; using EPiServer.ServiceLocation; namespace SEO.UrlKeeper { [ServiceConfiguration(typeof(IUrlKeeperRepository))] public class UrlKeeperRepository : IUrlKeeperRepository { private static DynamicDataStore Store { get { return typeof(UrlKeeperItem).GetOrCreateStore(); } } public UrlKeeperItem GetByOldPath(string path) { return this.GetAll().FirstOrDefault(item => item.OldPath == path); } private UrlKeeperItem GetByContentGuid(Guid contentGuid) { return this.GetAll().FirstOrDefault(item => item.ContentGuid == contentGuid); } private void Delete(Identity id) { Store.Delete(id); } public IQueryable GetAll() { return Store.Items(); } public Identity Save(UrlKeeperItem item) { if (item == null || string.IsNullOrEmpty(item.OldPath)) { return null; } // check if the old path already exists var oldPathItem = this.GetByOldPath(item.OldPath); // check if contentGuid is already there var existingItem = this.GetByContentGuid(item.ContentGuid); if (oldPathItem != null) { // since we don't want more than one item with the same old path. this.Delete(oldPathItem.Id); } if (existingItem != null) { // let's replace the existing item with our new on (we only want one per contentGuid) item.Id = existingItem.Id; } item.Modified = DateTime.UtcNow; return Store.Save(item); } } }
using System.Linq; using EPiServer.Data; namespace SEO.UrlKeeper { public interface IUrlKeeperRepository { IQueryable GetAll(); Identity Save(UrlKeeperItem item); UrlKeeperItem GetByOldPath(string path); } }
using System; using EPiServer.Data; using EPiServer.Data.Dynamic; namespace SEO.UrlKeeper { [EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)] public class UrlKeeperItem { public Identity Id { get; set; } [EPiServerDataIndex] public Guid ContentGuid { get; set; } [EPiServerDataIndex] public string OldPath { get; set; } public DateTime Modified { get; set; } } }
Our 404 controller is just as simple.
using System.Web.Mvc; using EPiServer.Web; using EPiServer.Web.Routing; using SeoPlayground.SEO.UrlKeeper; namespace SeoPlayground.Controllers { public class NotFoundController : Controller { private readonly IUrlKeeperRepository _urlKeeperRepository; private readonly UrlResolver _urlResolver; public NotFoundController(IUrlKeeperRepository urlKeeperRepository, UrlResolver urlResolver) { this._urlKeeperRepository = urlKeeperRepository; this._urlResolver = urlResolver; } public ActionResult Index() { var item = this._urlKeeperRepository.GetByOldPath(Request.RawUrl); if (item != null) { try { var redirectUrl = this._urlResolver.GetUrl(PermanentLinkUtility.FindContentReference(item.ContentGuid)); if (redirectUrl != null) { Response.RedirectPermanent(redirectUrl, true); } } catch { } } Response.TrySkipIisCustomErrors = true; Response.StatusCode = 404; return View(); } } }
Last step is just updating web.config to use our custom 404 controller.
<system.webServer> <httpErrors errorMode="Custom">
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" prefixLanguageFilePath="" path="/NotFound" responseMode="ExecuteURL" />
</httpErrors>
</system.webServer>
Next time an editor moves a page, Search Crawlers and people who use the old URL will automatically be redirected to the new URL instead of getting a 404 error back.