Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • David Armitage 243 posts 923 karma points
    Jan 20, 2020 @ 11:45
    David Armitage
    0

    Umbraco 8 | Hijack Deleted Pages | Redirect to Friendly Error Page

    Hi Guys,

    So what I am trying to achieve is...

    I have a load of pages (job pages) to be more specific. These pages come and go quite often. So every day a new job is either added or deleted. And it add. These pages are not trashed. They are hard deleted. There is just too much content to leave them in the trash. I want to keep the data very clean.

    So the problem is this leaves a load of broken links and jobs are removed.

    I understand I can set up a generic (standard .net) 404 redirect to a custom error page. This is ok but not the best just doesn't quite fit the user experience I am after.

    So for all these types of removed pages I want to be able to re-direct them their their own custom error. Either a 'this job has expired' page or just to the main job list page instead.

    All the jobs pages share the same url structure /browse-jobs/the-job-title here/

    What I am thinking might be possible is. (and this is what I need help with)

    1. Hijack the page load.
    2. Look at the url structure
    3. If the url structure contains /browse-jobs/ then check a page has been found.
    4. If so server that page.
    5. If not then redirect to my custom error page.
    6. The the page does not contain /browse-jobs/ then carry on doing waht it normally does.

    I did think of another idea. Hook in to the deleted event and create a redirect rule on the fly for that specific url. I decided this method is not suitable either because it's very likely that there will be a new job page posted with the exact same name at a later date and this might get caught up in that existing redirect rule. I could in theory hook into the published even and check / remove if a redirect exists but I just don't think that's the way to go.

    Anyone got any ideas here?

    Thanks in advanced.

    David

  • Marc Goodson 1118 posts 7507 karma points MVP 4x c-trib
    Jan 20, 2020 @ 20:12
    Marc Goodson
    101

    Hi David

    The neat thing here I think would be to display a custom 'sorry this job has expired' page that mentions the job title of the job has expired... (possibly with search results of 'similar jobs'... that might still be available on the site.

    The problem being the details of the original job that was available on the Url has been unpublished and deleted from Umbraco.

    So my thinking is 'on unpublish' for the job, read the jobtitle, the Url it was published under, and any other details you might want to display on the expired page, and insert them into a custom database table (along with the date of insertion),,,

    Then create a custom IContentFinder called FindExpiredJobsContentFinder and add it to the Umbraco pipeline 'after' the ContentFinderByNiceUrl content finder that Umbraco uses to match to Urls to published content, (it's important to be after this in the queue of content finders, so that future Umbraco pages on the same Url take precedence over the expired page).

    https://our.umbraco.com/Documentation/Reference/Routing/Request-Pipeline/IContentFinder

    The FindExpiredJobsContentFinder will then be able to read the requested url eg /jobs/browse-jobs/removed-job-title

    and use this to query the custom database table for the details of the expired job

    If a job isn't found the ContentFinder can return false, and the generic 404 is served - if an expired job is found, then you can create a fake IPublishedContent item with the details from the database table, and return true - displaying a custom page, with the neat message

    'removed job title' has been removed, closing date was: 24/01/2020

    or whatever the message should be...

    Periodically delete expired jobs from the database table after a month? so the table doesn't build exponentially...

    regards

    Marc

  • Nik 1213 posts 5076 karma points MVP 2x c-trib
    Jan 21, 2020 @ 18:54
    Nik
    0

    This is a really nice solution Marc!

  • David Armitage 243 posts 923 karma points
    Jan 22, 2020 @ 01:09
    David Armitage
    0

    Hi Marc,

    I agree this seems like a great solutions.

    I don't particularly need to display any data related to the job so I didn't consider storing anything in the database. That said though this isn't difficult and would mean that I can at least provide some SEO friendly content and a much better user experience if I did have a little job information to go along with the landing page.

    I am going to attempt this solution over the next few days so I will let you know how I get on.

    It's the content finder I was looking for. I used it in Umbraco 7 but forgot about it for Umbraco 8. After reading the link you sent I've learnt you can set an order the when the content finders get applied. I didn't realise you could do this so thanks for pointing me in the right direction.

    Kind Regards

    David

  • David Armitage 243 posts 923 karma points
    Jan 22, 2020 @ 13:29
    David Armitage
    0

    Hi Marc,

    Thanks. I implemented your solution exactly as you put it. It worked a treat.

    For anyone interested are is a bit of code that might help you do the same.

    My Content Finder

    using Umbraco.Core;
    using Umbraco.Web.Routing;
    using Website.Core.Helpers;
    
    namespace Website.Core.Events
    {
        public class ContentFinder : IContentFinder
        {
            private readonly ISiteHelper _siteHelper;
    
            public ContentFinder(ISiteHelper siteHelper)
            {
                _siteHelper = siteHelper;
            }
    
            public bool TryFindContent(PublishedRequest contentRequest)
            {
                var navigationSettings = _siteHelper.GetNavigationSettings();
    
                if (navigationSettings.JobListPage != null && navigationSettings.JobExpiredPage != null)
                {
                    var path = contentRequest.Uri.GetAbsolutePathDecoded();
                    string jobListPageUrl = navigationSettings.JobListPage.Url;
    
                    if (path.StartsWith(jobListPageUrl))
                    {
                        var content = contentRequest.UmbracoContext.Content.GetById(navigationSettings.JobExpiredPage.Id);
                        if (content != null)
                        {
                            contentRequest.PublishedContent = content;
                            return true;
                        }
                    }
                }
    
                return false;
            }
        }
    }
    

    My Composer required to tell Umbraco about my content finder.

    using Umbraco.Core;
    using Umbraco.Core.Composing;
    using Umbraco.Web;
    using Umbraco.Web.Routing;
    
    namespace Website.Core.Events
    {
        [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
        public class UpdateContentFindersComposer : IUserComposer
        {
            public void Compose(Composition composition)
            {
                composition.ContentFinders().InsertAfter<ContentFinderByUrl, ContentFinder>();
            }
        }
    }
    

    My Trashing Event - It seems I can only get access to the node URL in trashing which I used to insert and query my custom database table.

    using Umbraco.Core.Composing;
    using Umbraco.Core.Scoping;
    using Umbraco.Core.Services.Implement;
    using Umbraco.Core.Services;
    using Umbraco.Core.Events;
    using Umbraco.Core.Models;
    using Website.Core.Services;
    using Website.Core.Helpers;
    
    namespace Website.Core.Events
    {
        public class TrashedEvent : IComponent
        {
            private IScopeProvider _scopeProvider;
            private IJobHelper _jobHelper;
            private IJobService _jobService;
    
            public TrashedEvent(IScopeProvider scopeProvider, IJobHelper jobHelper,  IJobService jobService)
            {
                _scopeProvider = scopeProvider;
                _jobHelper = jobHelper;
                _jobService = jobService;
            }
    
            public void Initialize()
            {
                ContentService.Trashed += ContentService_Trashed;
            }
            public void Terminate()
            {
    
            }
    
            private void ContentService_Trashed(IContentService sender, MoveEventArgs<IContent> args)
            {
                foreach (var node in args.MoveInfoCollection)
                {
                    if (node.Entity.ContentType.Alias == "jobDetailsPage")
                    {
                        _jobService.DeleteJob(node.Entity.Id);
                    }
                }
            }
        }
    }
    

    TrashingComposer - To tell umbraco about my trashing event.

    using Umbraco.Core;
    using Umbraco.Core.Composing;
    
    namespace Website.Core.Events
    {
        [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
        public class TrashingComposer : ComponentComposer<TrashingEvent>
        {
            // nothing needed to be done here!
        }
    }
    

    And here is some database work.

    My Insert Code

    public void InsertExpiredJob(string url, string title, string summary)
            {
                try
                {
                    PocoExpiredJob expiredJob = new PocoExpiredJob
                    {
                        Url = url,
                        Title = title,
                        Summary = summary,
                        CreatedDate = DateTime.Now
                    };
    
                    using (var scope = _scopeProvider.CreateScope())
                    {
                        _ = scope.Database.Insert(expiredJob);
                        scope.Complete();
                    }
                }
                catch (Exception ex)
                {
    
                }
            }
    

    My Select Code

     public PocoExpiredJob GetJobExpiredByUrl(string url)
            {
                PocoExpiredJob pocoExpiredJob = null;
                using (var scope = _scopeProvider.CreateScope())
                {
                    pocoExpiredJob = scope.Database.FirstOrDefault<PocoExpiredJob>("SELECT * FROM RDExpiredJob WHERE Url=@0 ORDER BY CreatedDate DESC", url);
                    scope.Complete();
                }
    
                return pocoExpiredJob;
            }
    

    I'm sure there will be bits of code in here someone will find useful at some point.

    Thanks again

    David

  • Marc Goodson 1118 posts 7507 karma points MVP 4x c-trib
    Jan 23, 2020 @ 06:56
    Marc Goodson
    0

    Awesome!

    And now you can build a dashboard for the Umbraco backoffice, querying the db table and showing editors a report of 'recently expired jobs' ... :-P

    regards

    Marc

  • David Armitage 243 posts 923 karma points
    Jan 23, 2020 @ 07:00
    David Armitage
    0

    Actually it leads me onto a content app I have planned.

    I just remembered I have another similar forum post open which pretty much gets answered by this too.

    Just a simple internal analytics app which logs specific page views. I will probably go ahead and create this in a couple of weeks when I get all my projects finished up.

    Cheers for your help.

Please Sign in or register to post replies

Write your reply to:

Draft