How to Modify Slug When Creating Content Item

Topics: Writing modules
Apr 11, 2011 at 9:24 PM

We are writing a module that creates a content type called "Market". What we would like to have happen is that every time a new market is created, the slug has "markets/" appended to it. We have tried to emulate some of the techniques in the blog module and are coming up dry.

Ideas?

Apr 11, 2011 at 9:45 PM

I assume you mean prepended? ;)

You should be able to replicate functionality from Orchard.Blogs but it's quite complicated how it all works there and has to be integrated with the Routable system; and in fact the dev team will be refactoring Blogs in the future to use the containers/containables system which already supports this concept.

So what you can do is create a "container" type and item and give it a slug of markets; then add ContainablePart to your market type; and every time you create a market item you can add it to that container. This will achieve what you want.

For something more automated it could get a bit more tricky. This was actually on my list of things to do if I get time; basically a content part that lets you specify a base slug for a given content type.

May 18, 2011 at 5:03 AM

Did anyone get anywhere with this?  I'd like to create content items with a custom prefix. Any ideas how to do this with Orchard 1.1.30.0?

May 18, 2011 at 2:54 PM

I'm planning to write a custom routing part to do this (and some other things). It'll let you use content-type as a URL base, or configure a custom URL base for a content type, or hook in code to determine routes on an item-by-item basis. There are  couple of other ideas I've got, for instance "filtering" routes that can filter a sub-collection of items. I'm planning this for the 1.0 release of Mechanics, so that'll be next after the current version I'm working on.

May 20, 2011 at 1:03 AM

RandomPete, I'm trying to create a custom routing part that will factor the contentType into the url base as well.  Can you provide some insight into how I can make the module aware of the content type being used to construct the url without the module having a reference to the projects containing the types?  I'm sure there is some plugin that I can use, but nothing is jumping out at me right now.

May 20, 2011 at 1:00 PM

Hi, I'm actually working on this right now, it involved a lot of custom implementation of the entire routing system so it's certainly not trivial. If you want to work with me on this, I could certainly use some help getting it polished and documented.

Accessing the content type is as easy as ContentItem.ContentType - that gives you the string name of the content type.

May 21, 2011 at 2:31 AM

Hi RandomPete, I started off by trying to customize the RoutableByDate part that smeyers was gracious enough to post.  My ultimate goal is to create a routable part that does not require user input for url creation (hiding the permalink markup; you and I have had a couple of small discussions about this in the past).  Another goal is to have the part automatically prepend all routes with the content type (ex: maps/blah-blah or blogs/blah-blah).  I feel like I'm close.  I just need to gain a better understanding of containers since the prepender is based on the container type, which is the content type.  I tried to set the container of the route part in the Create method of the admin controller for my content type like so:

myType.ContentItem.Parts
                .Single(x => x.PartDefinition.Name == "SmartRoutePart")
                .ContentItem.As<ICommonPart>().Container = myType;

 

SmartRoutePart is the name of my custom route part.  This doesn't work since I eventually get an "Invalid Container" error in my CreatePost method.  This code seems like it's doing a few things in an ill-advised manner anyway.  Do you know the right way to specify the container of the route part? Have you taken a look at the RoutableByDate module? Thanks again!

May 21, 2011 at 5:54 AM

The thing is, "container" has to be a content item, you can't just set any old value. Have you looked at how the entire routing system works, with separate implementations in both Routing and Containers? There are quite a lot of different pieces to the puzzle and it really isn't that simple. I've pretty much already done what you're trying to do and I basically had to write a completely custom implementation of everything; AFAIK RoutableByDate had to do the same. Routable just doesn't have the extension points to customize it in this kind of way (although I'm adding some extension points into my system to make it a bit more flexible).

What I've also done with mine, is separate the TitlePart out from the routing component. So for your situation, you could just hide the route edit shape and leave the title; slugs will still be generated on publish.

May 21, 2011 at 6:36 AM

     I switched my above code to the following to reflect what you said about the container needing to be a ContentItem instance but I still get the invalid container error:

myType.ContentItem.Parts
                .Single(x => x.PartDefinition.Name == "SmartRoutePart")
                .ContentItem.As<ICommonPart>().Container = myType.ContentItem;

  Overall, I've got pretty much the same code base as Routable in my module.  Thus, I have a completely custom implementation as well.  I'm just not sure where the correct extension point is.  Is your implementation included in your Science Project code base?

May 21, 2011 at 6:50 AM

What you've done there is set a content item to be it's own container if I'm reading that code right? That would of course produce an infinitely recurring Url so "invalid container" is the correct response there ;)

The thing is the only extension points for routes is an IRouteProvider; then you need a path constraint which in the case of IRoutablePathConstraint is updated by a background service. There just didn't seem to be anywhere to hook in any customize the routes in any significant way. The only thing approaching that is ISlugEventHandler; which would be perfect, unfortunately the content item isn't passed into that event so it's useless for this purpose.

Anyway; my code is currently in a fork of Science Project. The content type URLs aren't fully working yet, most of the implementation is there but I've been concentrating on getting nested URLs working first. The thing is my routing works off the whole Mechanics system so it has a very different concept of parent/child (i.e. not Containers, instead any connectors you define can also produce child routes). I'll let you know when it's working.

Jun 5, 2011 at 12:48 AM

  Hey RandomPete, I noticed you made some updates to the Science Project.  Did you happen to make any updates regarding slug modifications?  Thanks!

Jun 5, 2011 at 7:40 PM

Yes - it's a feature called Plumbing. It's not 100% working yet but the basic functions are there. Need to get it finished ASAP so I can move on and get the websites finished that I'm building all of this for :)

The features it provides are as follows:

- Specify a base URL for content. There's an option to automatically base it on the content type name, or you can specify a custom base URL.

- Create nested URLs based on connections between content.

To illustrate these features, imagine you have a content type called Category, and a content type called Product. Products are then placed in Categories via a connector content type called CategoryToProduct. That's all done with the base Mechanics feature.

Then by using the Plumbing parts, you can make it so your products will be on URLs like this: /categories/books/watership-down. So that's a base URL for Category content type, then a Category called Books, then a Product called "Watership Down".

It can get more complicated than this. You could create a multi-level categories hierarchy, using a further connector which we'll call CategoryChild. This links categories to categories with a child relationship. Here we can also use a Plumbing part to create deeper nested URLs. Now we can have /categories/books/fiction/watership-down. There are practically infinite permutations of the ways you can build URLs with these parts, and even then I have a couple of event hooks in case you need further control.

Current issues with the system are:

- Home pages are broken. I had it working but there was a regression after the latest load of refactoring.

- Nested URLs won't all be updated to reflect a change in the slug of an item, or when an item is deleted - although on site restart, all URLs will get rebuilt.

- Changing the Part Settings will also not update URLs. This is a more complex issue. I can handle this in most cases. But if you remove one of the parts, the URLs will carry on working until the site restart. If you add a part, I might not be able to create URLs unless you also save the setting. I haven't yet found any event where I can handle a part getting added or removed; so I don't think there's anything I can do about this - although it seems like a good candidate to raise a workitem, I can imagine a lot of situations where you'd want to perform work on add/remove part.

- There are some other odd bugs, untested scenarios, and missing settings. Quite frankly the code is diabolical; but I needed to just get certain things working to be able to complete a couple of websites, and I'm afraid coding standards went out the window. I'll be refactoring to improve and optimize things later.

- There's no way right now to migrate existing titles and slugs to the new routing. This is a pain and something I want to support; but since I'm using this on brand new websites, I don't need it myself right now, and time constraints mean it'll have to wait (but if anyone else wants to look into it, shouldn't be too hard)

There's no documentation so I'll quickly explain the relevant Parts. Again I can't write any docs until I've got these two sites live. This is an area where I'd really appreciate any help - producing detailed documentation and examples will just further delay me finishing features and working on new stuff :)

- TitlePart. I wanted a title storage mechanism that was completely independent of routing. So this is it; it'll render a title and URL, same as RoutePart currently does, but it'll just take whatever View route is available in metadata, whichever routing system that comes from. You can also implement ITitledAspect on your own part, and that will be used for slug generation instead.

- PipeRoutePart. This gives you the options for base URLs. It also lets you choose whether that content type will be a "RootRoute" - meaning whether it's available at the top level of the URL tree, or whether it has to be accessed as a child URL of another content item.

- DrillRoutePart. You create nested URLs by adding this to a Connector content type. So it has to be on a content type that also has ConnectorPart. It can also act as a "filter" route - where the parent content item will still be the main detail display, but a particular connector type will get filtered across the route. That probably sounds very confusing because it is! But it's something I needed and it'll make a lot more sense with demonstration.

If anyone needs any specific help using this then please contact me. PipeRoutePart as it stands is pretty straightforward; it's DrillRoutePart that I know will cause real problems :)

 

Jul 12, 2011 at 8:29 PM

Hi Guys,

Pete's doing great work, and I hope to have time to dig into it soon.  In the mean time, I've been re-thinking my RoutableByDate module and had an "AH HA" moment today.  The Routable module provides event hooks for FillingSlugFromTitle and FilledSlugFromTitle if you create a new class that inherits from Orchard.Core.Routable.Events.ISlugEventHandler.  Thanks to usagi for the example in his Japanese Slug module at http://www.orchardproject.net/gallery/List/Modules/Orchard.Module.JapaneseSlug. 

So, to keep things simple, looking at the original question on this thread, you can add a single file to a new module called MarketSlug.cs with this content:

using System.Text.RegularExpressions;
using System;

namespace Orchard.Core.Routable.Events
{
    public class MarketSlugEventHandler : ISlugEventHandler
    {
        IOrchardServices _services;

        public MarketSlugEventHandler(IOrchardServices services)
        {
            _services = services;
        }

        public void FillingSlugFromTitle(FillSlugContext context)
        {

        }

        public void FilledSlugFromTitle(FillSlugContext context)
        {
            if (_services.WorkContext.HttpContext.Request["contentType"] == "Market")
            {
                context.Slug = "market/" + context.Slug;
                
                // Processing is done. Don't process further.
                context.Adjusted = true;
            }
        }
    }
}

Once enabled, you should be able to see the AJAX call to Slugify activate both methods in your new module.

In my testing so far (albeit only a couple of hours), I don't see any major issues with this technique and it's much simpler than trying to recreate the entire Routable module.  From here, you can take it anywhere you want.  For instance, I'm modifying the BlogPost slug to include the current date in the slug.

Anyone have any thoughts on this approach?

Thanks!

Coordinator
Jul 12, 2011 at 8:40 PM

Sounds great. Only thought I'd have is that this is going to get a lot easier in Orchard 2.0 :)

Sep 23, 2011 at 3:43 PM

Hi, I've tried the code smezers wrote and it doesn't work for this situation because of "/" character. Through the process the method GetEffectiveSlug is called and it splits the slug string by slash and returns only the first part (in this case "market"). But still I think this is very useful.

Jan 19, 2012 at 5:56 PM

Anyone know how to modify smezers' code to prefix a slug with a value that includes a forward slash? or is there now some  other simple method of accomplishing this? I basically want to have a prefix depending on the content Type. So for my Shoe Content Items I want URL's like /shoes/nikes, /shoes/loafers, etc. 

 

As janchvojka said, smezers' method works but the route ends up getting saved with just the part before the first forward slash, e.g., my shoe types get the route "/shoes". 

Jan 19, 2012 at 6:07 PM

Basically, you need Autoroute. This is what Bertrand was referring to with "Orchard 2.0" but actually it's going to be in Orchard 1.4. But you don't even have to wait that long ... in another few hours it'll be feature complete and available on a fork of 1.x ... if you're comfortable with using pre-release code for the time being!

(Also, the Plumbing feature I mentioned earlier in this post could achieve this too, and it's available in my Mechanics module on the gallery; but Autoroute is far, far better, and I can't wholeheartedly recommend Plumbing any more with Autoroute just around the corner...)

Jan 19, 2012 at 6:15 PM

Cool. Is there a description of what Autoroute is/does that I can read somewhere? Is it a replacement of Orchard's routing system, or will I have to go out of my way to make my parts use AutoRoute? 

I am already using 1.x branch for development so that won't be a problem. 

 

Jan 19, 2012 at 6:24 PM
Edited Jan 19, 2012 at 6:24 PM

Bertrand's post explains the concept: http://weblogs.asp.net/bleroy/archive/2011/07/30/future-orchard-part-3-autoroute.aspx

It's intended to completely replace Orchard's routing, although the current plan for 1.4 is to leave RoutePart working so as not to break any modules that depend on it, with an option to upgrade content types to Autoroute when ready. A new installation will have Autoroute by default for all the core content types.

The current development fork, however, has no RoutePart at all so it could break some 3rd party modules, and if you need to upgrade an existing site it could be tricky, although there's a straightforward SQL script to copy titles over from the RoutePart table.

Using it is as easy as adding AutoroutePart to a content type, and configuring the Url patterns to whatever you want. Some default patterns will be provided.