Jump to content
joshuag

Routes and rewriting URLS

Recommended Posts

Hi all,

I have a blog setup in PW like this

/blog/

cat1/

article1

article2

cat2/

article3

article4

Here is what I want.

I want to have all articles at the URL

/blog/article/

instead of

/blog/cat/article/

even though the articles are in categories and organized in the page tree like so.

Here is the challenge I am having. I still want to be able to use $page->url when I am pointing to an article.

currently, I made it so that /blog/ accepts urlSegments then, I am either loading a list of articles, or, if there is urlSegment1 and it's not a category, I am including a single post template file as an include.

The sucks for a lot of reasons.

I was just going to rewrite the urls with mod rewrite, but then my $page->url is still going to include the category in the URLS in the HTML.

Right now, I am essentially creating a new links to articles in the templates like this:

$pages->get(/blog/)->url.$article->name.'/';

This sucks. I have had to manage this a million times every time I am outputting a list of pages or links to blog articles. Now my templates are loaded with conditionals looking for links to blog posts and putting in my hacky URLS. :(

Is there a way to do routing with PW. I have run into this a few times, I am sure that I am not the only one. I know that I could have placed all my blog articles in /blog/ and made the categories as it's own tree of pages like /blog/categories/ and placed all the categories there.... buuuuuut.... I found that it is a million times easier for my client to publish and manage their articles with the categories and articles in the same page tree. Maybe I am mistaken, but now it would be almost crazy for me to go back and change the way I did it.

Any suggestions?

Thanks in advance.

Share this post


Link to post
Share on other sites

i.am first

you can puut this in your head.inc tamplate or some includeded file before.u are doing $page->url

$pages->addHookAfter('Page::path', null, 'hookPagePath');
function hookPagePath(HookEvent $e) {
 $page = $e->object;
 if($page->template == 'article') $e->return = "/blog/$page->name/";
}
  • Like 20

Share this post


Link to post
Share on other sites

Wow - that's a good question and a stellar answer. Didn't even know you could do that!

Share this post


Link to post
Share on other sites

If WillyC's suggestion is a viable solution and applicable for more use cases it would be great if stuff like this is documented outside of the forum. I, and i'm sure more people would have a hard time figuring this stuff out.

  • Like 2

Share this post


Link to post
Share on other sites

I knew it, I was just keeping it to myself.

Noooooooooooooooooot.

(That was so terribly 1990's I know).

  • Like 2

Share this post


Link to post
Share on other sites

Thanks for the answer. this looks like an ok solution... but I would love to see how more people are dealing with this type of thing.

I want to know more about PW and how to take advantage of these hooks etc. Awesome example WillyC Thanks.

Share this post


Link to post
Share on other sites
If WillyC's suggestion is a viable solution and applicable for more use cases it would be great if stuff like this is documented outside of the forum. I, and i'm sure more people would have a hard time figuring this stuff out.
Thanks for the answer. this looks like an ok solution... but I would love to see how more people are dealing with this type of thing.

WillyC's solution is a good way to go if you have this need. But want to mention that the whole idea of multiple routes to a page kind of goes against the grain of the ProcessWire philosophy. By design, there is only one URL to any given page. This is different from systems that disconnect their data from URLs (Drupal, EE, etc.). ProcessWire considers pages like files on a file system. We are trying to embrace the way that the web is addressed rather than counter it.

I understand the desire to make a shorter URL for a given page, and that's a fine reason to implement a solution like this (and I've done it myself too). But the reason you don't see things like this outlined in the documentation is because I don't think it's a best practice, whether in ProcessWire or on the web in general. So if someone uses multiple routes, I would suggest it only to solve a specific need after a site is produced… not as something to build around from the start.

  • Like 9

Share this post


Link to post
Share on other sites

Thanks for the reply Ryan, and I understand your position and agree. But yeah, sometimes I need to do this. hahaha

Share this post


Link to post
Share on other sites

joshuag: and why can't you change the tree, so all the articles are children of /articles/? There may be a different take on this.

Share this post


Link to post
Share on other sites

@adamkiss, well, great question, but this is just one example where I have needed this type of thing. Usually easily solved by mod rewrite and some careful templates. But gets to be a real pain on bigger websites where there is a lot of dynamic content.

  • Like 1

Share this post


Link to post
Share on other sites

Joshua, I am in no way 'expert' when it comes to PW, but one thing I've learned so far (and one to keep in mind, maybe I should make a nice print ;)) is that developers often apply traditional CMS thinking (for things like tags, categories, etc.), and that gets in the way of the flexibility of the PW Tree.

You often need to step back, visualize the target outcome and try to create tree that gives you the outcome.

  • Like 4

Share this post


Link to post
Share on other sites

joshuag: and why can't you change the tree, so all the articles are children of /articles/? There may be a different take on this.

I'm actually planning to do this on one website with a few hundred articles and use the page history plugin to handle the redirects.

What happened was I was forced into a structure of /articles/categories/title in the previous CMS, and only had 4 strict categories. As I want to add more categories, and there is now a chance that articles can apply to multiple categories I'm considering shifting them to articles/title and simply referencing them to those categories for search purposes.

Essentially the front-end and the list of categories will remain the same, but the flexibility is far superior and only some URLs will change which can be fixed with a 301 redirect :)

  • Like 1

Share this post


Link to post
Share on other sites

Is it possible to remove parts of urls (like "cat/" in joshuag example) without messy hacking every time you use it? so that you could use modules and be sure that urls are this way everywhere? In above example I would want "cat/" part to be non-existent at all and don't need multiple routes but organization in the backend is important. Maybe similar "hack" could be done somewhere in the core?

Or maybe organization in the backend could be done differently? Adding some dividers/labels in the tree? Or some kind of second "virtual tree" - so ie when FooterNav field was checked, it would be displayed in second tree. I guess labels or different formatting may be the answer here.

I want to create multiple menus.

Share this post


Link to post
Share on other sites
Is it possible to remove parts of urls (like "cat/" in joshuag example) without messy hacking every time you use it?

WillyC's example above would be the way to do it. The core is specifically built to support that hook exactly the way that he used it, so this is not a hack, this is part of the API (combined with using urlSegments). But maybe somebody will come up with a module that enables it to be automated from the admin at some point. I'm of the opinion that you will see the greatest benefits by using a structure that is consistent with the navigation. So if one is having to redefine URLs outside of the structure, that's a good reason to reconsider whether the chosen structure is ideal.

Or maybe organization in the backend could be done differently? Adding some dividers/labels in the tree? Or some kind of second "virtual tree" - so ie when FooterNav field was checked, it would be displayed in second tree. I guess labels or different formatting may be the answer here.

You've got an infinitely nest-able tree (in a manner of speaking). Every branch is its own tree if you want it to be. It's just a question of how you perceive and use it. Regarding labels, take a look at mindplay.dk's Template Badges module, which might have some of what you are talking about.

  • Like 2

Share this post


Link to post
Share on other sites

Thanks ryan.

So I didn't understand WillyC's code. Sorry for messing. I have to learn how hooks work. I'll probably ask very stupid question, but... does it mean that using this code anywhere "overrides" all the urls for every page and mod rewrite is not needed?

mindplay.dk's Template Badges module seems to be exactly what I need. I thought about modyfing admin theme for this purpose but with this I probably will not need to do it.

Thanks

Share this post


Link to post
Share on other sites
So I didn't understand WillyC's code. Sorry for messing. I have to learn how hooks work. I'll probably ask very stupid question, but... does it mean that using this code anywhere "overrides" all the urls for every page and mod rewrite is not needed?

WillyC's example just modifies the output of the $page->path() function (which is also used by $page->url). His intention there is to change what URL gets sent out when you call $page->url. It's really no different than making your own function and using it instead, except by using a hook like this, you can continue to use ProcessWire's API and get the result out of it that you want. Ultimately it's just a syntax convenience.

While his hook example handles the URLs that get output in your markup, you still have to have something to look for and capture those URLs. Perhaps you could use mod rewrite, but typically you would use the "URL segments" feature that you see on the "URLs" tab when editing any template in PW admin.

Lets use a simple example of your homepage template (home) with filename home.php. And lets say you want to make it so that when someone access domain.com/contact/ (a URL that doesn't currently exist) you want it to display the page that lives at domain.com/about/contact/. So you are setting up your site to respond to the shorter URL of /contact/ (rather than /about/contact/, where the page actually lives).

Go to Setup > Templates > home > URLs. Click the checkbox to "allow URL segments", and save. Now edit your /site/templates/home.php file, and do something like this at the top:

if($input->urlSegment1 == 'contact') {
 // render the /about/contact/ page
 echo $pages->get('/about/contact/')->render();
 return;

} else if($input->urlSegment1) {
 // throw a 404
 throw new Wire404Exception();
}

// otherwise continue normally and display your homepage.
  • Like 7

Share this post


Link to post
Share on other sites

Thank you for your patience ryan. That is extremely useful post for me. When 404 will be displayed in above example?

Share this post


Link to post
Share on other sites

Thank you for your patience ryan. That is extremely useful post for me. When 404 will be displayed in above example?

When you enable url segments on the home template it will also render, ehm...non-existing pages

from the docs:

Given a page that exists at the URL /path/to/page/, a URL of /path/to/page/aaa/ will return a 404 Page Not Found error by default. But if you enable URL segments for the page's template, then it will instead render the page and populate $input->urlSegment1 with "aaa".

So we only allow urlSegment with the value "contact" and throw 404 when we try to go to any other non-existent page e.g.

www.mysite.com/hahanothinghere/

  • Like 3

Share this post


Link to post
Share on other sites

Sorry to hijack this topic, but how would one do it other way around? I have page 'posts' and all articles are in there. But I want my url to be /posts/category/article/ instead of just /posts/article/. 'Category' is Page reference field.

I am happy as it is, but my old site which I am converting from (made in Drupal) had it that way, so to avoid 404 errors, would like to know this. Thanks.

Share this post


Link to post
Share on other sites

turn url segments on your category pages, and then something like this code (browser written, untested):

 <?php
   $post_name = $input->urlSegment(1);
   $post = $pages->get("/posts/{$post_name}");
   if ($post->id) {
     // render post
   } else {
     throw new Wire404Exception();
   }
  • Like 2

Share this post


Link to post
Share on other sites

Since it appears that each post can have only one category, you could easily solve this by making your actual site tree represent the /posts/category-name/post-name/ structure. If the one-category-per-post status will remain for the future, then this would be the best approach.

But if you want to stick with your current structure then here are a couple options:

Option 1:

For your /posts/ page template, turn on URL segments like Adam mentioned. But since you want the category to be included in the URL, that's going to be your first URL segment (not the post_name). it sounds like you want the post_name to be the second URL segment. So something like this should work:

if($input->urlSegment1 && $input->urlSegment2) {
 // display the post
 $postName = $sanitizer->pageName($input->urlSegment2); 
 $post = $page->child("name=$postName"); 
 if(!$post->id) throw new Wire404Exception();
 echo $post->render(); 
} else if($input->urlSegment1) {
 // display posts in category, or a 404 if you don't want it
} else {
 // display regular content for your /posts/ page
}

If there is potential for post and category name collisions, you might want to precede the category part of your URLs with something like "cat-[category name]" for URLs like /posts/cat-widgets/post-name/.

Option 2:

If the goal is to avoid 404s, then I would take a different approach: accept the new URLs as they are in ProcessWire and perform redirects when one of the old URLs is hit. This would be more efficient than option 1, and you'd benefit from the newer shorter URLs. You'd do this in a similar manner to above, enabling URL segments for your posts template. But you'd take a different action:

if($input->urlSegment1 && $input->urlSegment2) {
 // URL has /category-name/post-name/
 $postName = $sanitizer->pageName($input->urlSegment2); 
 $post = $page->child("name=$postName"); 
 if(!$post->id) throw new Wire404Exception(); 
 $session->redirect($post->url); 
} else if($input->urlSegment1) {
 // URL has /category-name/
 $categoryName = $sanitizer->pageName($input->urlSegment1); 
 $category = $pages->get("/categories/$categoryName/");
 if(!$category->id) throw new Wire404Exception(); 
 $session->redirect($category->url); 
} 

// if this point is reached, display regular content for your /posts/ page
  • Like 1

Share this post


Link to post
Share on other sites

I definitely prefer friendlier shorter urls, so I will go with your second option, I think that is exactly what I need. I prefer to keep things simple, so when editor hits 'new post' he will choose (apart from title/body part) category name, author, and issue no. I was contemplating for a long time should my posts go under category, author or issue, which one is more important. So decided just to go under 'posts' instead, keeping it more real and not having to choose between the three. Thanks to ProcessWire and its great community support, I am free to choose other approach down the line if I have to.

Only one more tip: as I am using blog template as a starting point which already uses the segments for the rss feeds, I am not sure how to incorporate the code above (second one, 404 avoiding) to the existing one, keeping rss as well:

include_once("./blog.inc");
if($input->urlSegment1) {
// check for rss feed
if($input->urlSegment1 != 'rss') throw new Wire404Exception();
$homepage = $pages->get('/');
renderRSS($page->children("limit=10"), $homepage->get('headline|title'), $homepage->get('summary|meta_description'));

} else {
// display paginated blog list
$headline = $page->get('headline|title');
$content = renderPosts("limit=10", true);
include("./main.inc");
}

Help very appreciated.

Share this post


Link to post
Share on other sites

I think you'd probably want this, or something like it:

if($input->urlSegment1 && $input->urlSegment2) {
 // URL has /category-name/post-name/
 $postName = $sanitizer->pageName($input->urlSegment2); 
 $post = $page->child("name=$postName"); 
 if(!$post->id) throw new Wire404Exception(); 
 $session->redirect($post->url); 

} else if($input->urlSegment1 === 'rss') {
 $homepage = $pages->get('/');
 renderRSS($page->children("limit=10"), $homepage->get('headline|title'), $homepage->get('summary|meta_description'));

} else if($input->urlSegment1) {
 // URL has /category-name/
 $categoryName = $sanitizer->pageName($input->urlSegment1); 
 $category = $pages->get("/categories/$categoryName/");
 if(!$category->id) throw new Wire404Exception(); 
 $session->redirect($category->url); 

} else {
 // display paginated blog list
 $headline = $page->get('headline|title');
 $content = renderPosts("limit=10", true);
 include("./main.inc");
}

Share this post


Link to post
Share on other sites

Hi all,

I have a blog setup in PW like this

/blog/

cat1/

article1

article2

cat2/

article3

article4

Here is what I want.

I want to have all articles at the URL

/blog/article/

instead of

/blog/cat/article/

Joshua, couldn't you have resolved this using segments?

Share this post


Link to post
Share on other sites

Just been thinking about joshuag's original aim and wonder if it could be tackled from a different angle. I know I often have the instinct to categorize things in the page tree to make them easier to find when editing, like in his example:

/blog/
  cat1/
    article1
    article2
  cat2/
    article3
    article4

I usually refrain because I want the simpler URL structure on the front-end and it can be hard to know what category is really the most important when an article belongs in multiple categories, but it can make browsing through pages tedious when you have 100's or 1000's of articles.

What I wondering is if there might be a way to group and sort the pages into categories in the tree view based on a chosen (and easily changeable) field value (most likely an ASM field that points to a list of selectable categories). I see this as being somewhat analogous to the way we can add tags to fields to have them appear grouped on the Setup > Fields page. 

Not sure the best way to implement this - maybe it could be a module - I'll think about it more soon - maybe even put together a mockup of what I think it could look like.

Imagine being able to instantly group and sort the page tree by year and month, and then switch to subject, keywords, categories, or whatever other field you want.

Any thoughts?

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...