SamC

URL segments vs matching tree path

Recommended Posts

I'm creating a blog which I hope to write PW tutorials for beginners. So, I have the following tree structure:

1.thumb.png.2a405441fe716176e7b72301e8de7aba.png

Paths like:

/blog/ (lists posts)
/categories/processwire/ (will list all posts with that category etc.

I choose the category within the blog post thus:

2.thumb.png.0acc6b86b44bfdc25204b140fba4e31b.png

Path on the category page:

3.thumb.png.536b6ce5ffb7368b906d0a279250f745.png

However, I have a couple of problems. I was going to enable URL segments on category-index template. However:

1) My paths exactly match my anticipated URL segment enabled URLs i.e. /category/processwire/ actually exists as a path therefore taking precedence. Because there is no category-entry.php file, this gives me a 404 i.e.

 

"You can use URL segments on any page where your template settings allow, regardless of whether it has children or not. Should there be a child page that has the same name as a URL segment that the parent page's template is looking for, the pages in the tree always have precedence." https://processwire.com/docs/tutorials/how-to-use-url-segments/page2

 

2) I have no real use for /category/ page other than organizing the child categories in the tree. Going to that URL makes no real sense, what would be shown there?

3) If I decided to NOT use URL segments, but to use a category-entry.php file instead (category-entry template is used to create these pages), the URL /category/processwire/ would work and render whatever was in category-entry.php.

In both scenarios though, how do I handle the /category/ URL? I mean, I could list all blog posts there or something but /blog/ already does that.

 

Any advice would be awesome, thanks :)

Share this post


Link to post
Share on other sites

I'd pluralize category -> categories and display a list of categories with 3-4 posts under each category. Then you can display a category under /categories/processwire or use urlSegments to rename that to /category/processwire.

Or just render category list at /categories with urlSegments and redirect /category to /categories. 

What I normally do is keep categories under /blog/categories/. Everything works just fine with no need for urlSegments.

One handy tool when using urlSegments is to hook into Page::path and modify $event->return, so you won't have to manually building urls everywhere you need. You still need to implement urlSegment logic though, changing Page::path is a passive operation, it doesnt change routing behavior

  • Like 1

Share this post


Link to post
Share on other sites

Thanks @abdus I like the idea of listing the categories with a few posts under each one at /categories/. Is a good use of that page.

Might try the /blog/categories/ too, see which works best for me.

Not got into hooks yet but that's the second time I've read about Page::path tonight. On the bucket list along with writing a module.

  • Like 1

Share this post


Link to post
Share on other sites

I also recommend using built-in InputfieldPageAutoComplete, install it from Modules > Core and change categories field to use autocomplete. It's much easier to seach and pick categories with it. There's also selectize and chosen js alternatives as well

https://modules.processwire.com/modules/inputfield-selectize/
https://modules.processwire.com/modules/inputfield-chosen-select/

  • Like 3

Share this post


Link to post
Share on other sites
7 minutes ago, abdus said:

Also, for 2-way relations, like posts under a certain category vs. categories of a post you can use @Robin S's Connect Page Field module. It's really handy. I use it for ordering categories by number of posts under each category.

https://modules.processwire.com/modules/connect-page-fields/

image.thumb.png.7468c5e4d814983a27679884ff4be9bd.png

Not sure I'm 100% on the point of this module. I find I learn best when I get a problem, then have to find a way to fix it, meaning, I'll see the point once it solves a problem I have! :)

Share this post


Link to post
Share on other sites
2 minutes ago, abdus said:

I also recommend using built-in InputfieldPageAutoComplete, install it from Modules > Core and change categories field to use autocomplete. It's much easier to seach and pick categories with it. There's also chosen and selectize and chosen js alternatives as well

https://modules.processwire.com/modules/inputfield-selectize/
https://modules.processwire.com/modules/inputfield-chosen-select/

Always up for trying new things! Thanks.

Share this post


Link to post
Share on other sites

I understand. No need to overengineer a simple blog. But check out the module description. Maybe not with this project but @Robin S gives a few use cases, which I'm sure you'll find quite useful later on.

  • Like 1

Share this post


Link to post
Share on other sites

So I quickly knocked this up before work. Changed path to /category-index/category-entry/ (have template files for each now) and to render the /categories/ page:

// category-index.php
<?php namespace ProcessWire; ?>

<div class="container">
    <div class="row">

        <?php
            $categories = $pages->get("/categories/")->children;
            foreach ($categories as $category):
        ?>
        <div class="col">
            <h2><a href="<?= $category->url; ?>"><?= $category->title; ?></a></h2>

            <ul class="list-unstyled">
            <?php
                $posts = $pages->find("template=blog-entry, categorySelect=$category");
                foreach ($posts as $post):
            ?>
                <li><a href="<?= $post->url; ?>"><?= $post->title; ?></a></li>
                <?= $post->summary; ?>

            <?php endforeach; ?>
            </ul>

        </div>

        <?php endforeach; ?>

    </div>
</div>

How is this in terms of efficiency? Like, if there were 10,000 posts. I saw code in another post, something like like:

if (! $pages->count("template=blog-entry, categorySelect=$category")) continue; {
	// Do stuff
}

Is this a situation where @Robin S module would come in handy? Currently the page renders lie so:

1.thumb.png.46b0acad4ff5487df8cb25ab400009de.png

(some pages are repeated because they have multiple categories selected but this is fine)

 

  • Like 1

Share this post


Link to post
Share on other sites
28 minutes ago, SamC said:

Like, if there were 10,000 posts.

Don't worry about it. At a rate of one post/day, it takes ~30 years to hit that point. Even at hundreds of post you won't have an issue. If you hit that point, there's $pages->findMany() method for you. 

One thing I've noticed is you're not limiting the amount of posts under a category. What happens when a category has tens of posts while another has only 1-2? With three column layout you'll get a very long column that will push others down quite a bit. (Unless you set up a masonry layout.)  

 

 

Share this post


Link to post
Share on other sites
42 minutes ago, abdus said:

Don't worry about it. At a rate of one post/day, it takes ~30 years to hit that point. Even at hundreds of post you won't have an issue. If you hit that point, there's $pages->findMany() method for you. 

My one 'concern' for want of a better word is the code:

$posts = $pages->find("template=blog-entry, categorySelect=$category");

This is inside a foreach so does this do the find every loop or is the value in memory after the first run?

 

42 minutes ago, abdus said:

One thing I've noticed is you're not limiting the amount of posts under a category. What happens when a category has tens of posts while another has only 1-2? With three column layout you'll get a very long column that will push others down quite a bit. (Unless you set up a masonry layout.)  

This is just a demo using default bootstrap 4, just working on getting the data displayed first. Never liked the way that grid layouts do that when one column is longer. I'll most likely use something like this technique again, works really well and flows nicely:

 

Share this post


Link to post
Share on other sites
5 hours ago, SamC said:

This is inside a foreach so does this do the find every loop or is the value in memory after the first run?

It does run find() on every loop, but PW caches pages as it fetches them from the DB, so subsequent calls to the same page will be made in memory without touching the db again.

From /wire/core/PagesLoaderCache.php

/**
* Cache the given page.
* @param Page $page
* @return void
*/

public function cache(Page $page) {
    if($page->id) $this->pageIdCache[$page->id] = $page;
}

 

  • Like 2

Share this post


Link to post
Share on other sites

Thanks for all the tips. I am aware of limit @mr-fan but thanks anyway. This is just a demo for now but will start writing some real content asap. Looking forward to being able to give something back to the community. Although my guides will probably be nothing new to seasoned users, they will make it easier for people new to PW, or maybe to persuade people choosing a CMS/CMF that PW is super easy to use even for non/beginner coders. Anyway, back from work now so can continue.

-EDIT-

I added a Datetime field to the blog posts with no output so I can choose the post date on each one rather than use the created date i.e. I can backdate them to make it look like I've been doing this for ages... seriously though, now it shows the two latest posts per category and I can manipulate the dates better (for HTML/CSS output) from the field timestamp. I'll probably stick that in a function as the date will be displayed on multiple templates.

// category-index.php
<?php namespace ProcessWire; ?>

<div class="container">
    <div class="row">

        <?php
            $categories = $pages->get("/categories/")->children;
            foreach ($categories as $category):
        ?>
        <div class="col">
            <h2><a href="<?= $category->url; ?>"><?= $category->title; ?></a></h2>

            <ul class="list-unstyled">
            <?php
                $posts = $pages->find("template=blog-entry, categorySelect=$category, sort=-postDate, limit=2");
                foreach ($posts as $post):
            ?>
                <li><a href="<?= $post->url; ?>"><?= $post->title; ?></a></li>
                <?= $post->summary; ?>

            <?php endforeach; ?>
            </ul>

        </div>

        <?php endforeach; ?>

    </div>
</div>

Seems to work ok:

1.thumb.png.16fef9ff52d36b020e214f25a4731ecf.png

Thanks for all the help. Maybe my first post should be "How to make a blog with processwire"... :)

  • Like 2

Share this post


Link to post
Share on other sites

I've installed the page fields connect module and trying to redo the above page with new code. I linked the fields, I can now see blog posts in a page ref in categories templates, and I choose categories in blog posts templates. So it's working in other words. Not 100% sure on the code to use here though:

<?php namespace ProcessWire; ?>

<div class="container">
    <div class="row">

        <?php
            // get PageArray of all categories
            $categories = $pages->get("/categories/")->children;
      	    // loop through them individually
            foreach ($categories as $category):
        ?>

        <?php // OLD CODE - if ($pages->count("template=blog-entry, categorySelect=$category")): ?>

        <?php
            // if single category has any values in the blogPosts page ref field - is correct??
            if (count($category->blogPosts)):
        ?>

        <div class="col-4">

            <h2><a href="<?= $category->url; ?>"><?= $category->title; ?></a></h2>

            <ul class="list-unstyled">
            <?php
                // OLD CODE - $posts = $pages->find("template=blog-entry, categorySelect=$category, sort=-postDate, limit=2");
                // The old way had to get EVERY blog post before doing anything  
              
                // NEW CODE - only gets relevant blog posts to begin with
                // return PageArray of all items in blogPosts ref field in single category
                $posts = $category->blogPosts;
                // sort the PageArray newest first
                $posts->sort("-postDate");
                // only want first 2 items
                $posts = $posts->slice(0, 2);
                // loop through the 2 items
                foreach ($posts as $post):
            ?>
                <li><a href="<?= $post->url; ?>"><?= $post->title; ?></a></li>
                <?= $post->summary; ?>

            <?php endforeach; ?>
            </ul>

        </div>

        <?php
            endif;
            endforeach;
        ?>

    </div>
</div>

Is this how you should use this? With pages->find, you sort/limit etc.. from within the selector. Running:

$posts = $category->blogPosts;

...returns me a PageArray with EVERY blog post in the page reference field from that specific category i.e. all of these:

59c29a7304795_Screenshot2017-09-2017_41_44.thumb.png.27d2dd251930724c6e9c184ca5eb69ad.png

Just not sure if I'm on the right track here. It works ok, but is this the way to do it? I added a bunch of comments to try and show better what I'm doing. Never had to loop or filter through a multiple page reference, only ever used a single page.

I got this info from thumbing through the docs again here:

http://cheatsheet.processwire.com/pagearray-wirearray/sorting-and-filtering/a-sort-property/
http://cheatsheet.processwire.com/pagearray-wirearray/getting-items/a-slice-n-limit/

Share this post


Link to post
Share on other sites
1 minute ago, abdus said:

Yes, you're on the right track. Nothing to worry about

Hi @abdus so the new way of doing things means only relevant posts are returned as a PageArray rather than ALL blog posts (i.e. 'template=blog-entry').

This is why this method is more efficient?

Share this post


Link to post
Share on other sites
13 minutes ago, SamC said:

only relevant posts are returned as a PageArray rather than ALL blog posts

That's right. (you were putting boundaries using selectors though, so, not "all" pages)

13 minutes ago, SamC said:

This is why this method is more efficient?

You're not searching through all pages to check if their template and category matches with your criteria. With page reference fields, all relevant pages are known beforehand (check out field_blogPosts table in your db), and fetched from DB all at once once you call $cat->blogPosts.

image.png.a395e5689fc62edd4936af7caf722060.png

  • Like 1

Share this post


Link to post
Share on other sites

That's a great explanation, thanks.

Using the new page ref field, I managed to get a counter onto the posts pretty easy too:

// category-index.php
<ul class="list-unstyled">

<?php
    $categories = $pages->get("/categories/")->children;
    $categories->sort("name");
    foreach ($categories as $category):
        // how many items are in returned PageArray
        $counter = count($category->blogPosts);
?>

<?php
  // if any posts have this category selected, then render it with counter
  if ($counter): ?>

    <li class="py-1">
        <a href="<?= $category->url; ?>"><?= $category->title; ?> (<?= $counter; ?>)</a>
    </li>

<?php
    endif;
    endforeach;
?>

</ul>

I like this two way relationship stuff! Seem to making progress using PW so much faster than any system I used previously, it's remarkable. Got pretty much everything linking to everything else now:

59c2a32b88c7d_Screenshot2017-09-2018_19_21.thumb.png.9449ffd9d3b72df6ce5ef57b762543e4.png

...and made a little function to output the categories on each post:

// _func.php
/**
 * Render blog post categories
 *
 * @param PageArray $items
 * @return string
 *
 */
function renderCategories($items) {
    $arr = [];
    foreach ($items->categorySelect as $category) {
        array_push($arr, "<a href='$category->url'>{$category->title}</a>");
    }

    $str = implode(" / ", $arr);

    if ($arr) {
        return "Posted in: " . $str;
    }
}

used like:

// blog-entry.php
<p>Posted in: <?= renderCategories($page); ?></p>

And:

59c2a3c25bb88_Screenshot2017-09-2018_20_17.thumb.png.dca3e5a82198f6cf5346b7ad1e5ed267.png

It's the first time I've had such a great time learning a CMS. The positive vibe on this forum is a massive plus to me, keeps me motivated.

  • Like 2

Share this post


Link to post
Share on other sites
23 minutes ago, SamC said:

Using the new page ref field, I managed to get a counter onto the posts pretty easy too:

Protip: you can filter empty categories and specify sort in your selector, and simplify your code like this

<?php namespace ProcessWire;

// get categories that have been referenced by a blog post
$categories = $pages('template=category, blogPost.count>0, sort=name');
// $categories = $pages('parent=/categories/, blogPosts.count>0');

?>
<?php if ($categories->count): ?>
    <ul class="list-unstyled">
        <?php foreach ($categories as $category): ?>
            <li class="category">
                <a href="<?= $category->url ?>"><?= $category->title ?> (<?= $category->blogPosts->count ?>)</a>
            </li>
        <?php endforeach; ?>
    </ul>
<?php else: ?>
    <p>No categories yet.</p>
<?php endif; ?>

 

And using API methods, your post items can be simplified to:

<article class="post">
    <h1 class="post__title">...</h1>
    <p class="post__summary">...</p>
    <p>Posted in: <?= $post->categories->implode('/', function($c){return "<a href='{$c->url}'>{$c->title}</a>";}) ?></p>
</article>

 

https://processwire.com/api/ref/wire-array/#pwapi-methods-retrieval

https://processwire.com/api/ref/wire-array/implode/

  • Like 2

Share this post


Link to post
Share on other sites

Lol, I love it when I'm patting myself on the back and congratulating myself on my code success... and then someone comes along and shows how much easier it can be done :P I just keep climbing that hill. I had to modify the above but works great:
 

<h2>Categories NEW</h2>
<?
// find categories that have been referenced by a blog post
$categories = $pages->find("template=category-entry, blogPosts.count>0, sort=name");
?>

<?php if ($categories->count): ?>
    <ul class="list-unstyled">
        <?php foreach ($categories as $category): ?>
            <li class="category">
                <a href="<?= $category->url ?>"><?= $category->title ?> (<?= $category->blogPosts->count ?>)</a>
            </li>
        <?php endforeach; ?>
    </ul>
<?php else: ?>
    <p>No categories yet.</p>
<?php endif; ?>

RE using the API:

26 minutes ago, abdus said:

And using API methods, your post items can be simplified to:


<article class="post">
    <h1 class="post__title">...</h1>
    <p class="post__summary">...</p>
    <p>Posted in: <?= $post->categories->implode('/', function($c){return "<a href='{$c->url}'>{$c->title}</a>";}) ?></p>
</article>

https://processwire.com/api/ref/wire-array/#pwapi-methods-retrieval

https://processwire.com/api/ref/wire-array/implode/

The post categories need to go on multiple pages though. Would this not go against the DRY principle? That's why I stuck it in a function. They are displayed under each post at '/blog/' and under the individual post at '/blog/my-first-post/'.

Share this post


Link to post
Share on other sites

You can implement it anyway you like, I was just introducing a few api methods that I use quite often. I like being DRY with my code but with templates, I lean towards using template files and wireRenderFile() function over basic functions if the markup does not include too much logic

https://processwire.com/blog/posts/processwire-2.5.2/#new-wirerenderfile-and-wireincludefile-functions

Skim over api methods once or twice to see what's available to you, you'll appreciate PW once more. So much good stuff is buried in the docs (and blog posts) 

  • Like 2

Share this post


Link to post
Share on other sites

Yeah wireRenderFile() is awesome. I use it for random content blocks on my other site. Great to be able to pass in additional data.

I got stuck in to the whys and hows right at the beginning i.e finding out this method returns a PageArray, this one returns something else. What a PageArray actually is etc. I spent a day just print_r()'ing stuff to see the structures of various get/find combos with different selectors. It helped a lot.

But as you say, I'd put £££ on it that I've only scratched the surface!

Share this post


Link to post
Share on other sites
49 minutes ago, abdus said:

I tried to use it before but was so confused with it. I can't remember what I was trying to do, it was in an earlier post of mine a while back.

That said, this has jogged my memory so I'll give it another shot for sure. Maybe my extra experience now will make things clearer.

I know it's hailed as an extremely useful tool in PHP.

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.