Adding estimated read time to posts in Jigsaw cover image

Adding estimated read time to posts in Jigsaw

7 min readJanuary 19, 2019

jigsaw tutorials php

I really like the feature on sites like Medium where you can see how long it most likely will take to read an article. It helps me decide at a glance if I can read it right now or should save for later.

So earlier this week I decided to implement this feature to the blog section of this website. In this article I'll go over the steps I took to get that feature implemented.

What we will make

As blog posts can be shown wherever you want, our ideal scenario would be to be able to add a new property to the blog posts in some way. After adding that property we just have to modify the blade templates to show it wherever we need it on the website.

This article assumes you have installed Jigsaw by Tighten and the blog default theme (with your own modifications of course).

Looking for solutions

First thing you do when you want to implement something like read time, you check if anyone has done it before of course. It's always nice if you don't have to re-invent the wheel. As I didn't feel like this warranted an extra php package I just looked for a simple enough function to use and modify to my needs. I didn't even have to look for it that long. This post by Kevin Perrine filled all my needs for this function.

Now I just needed a way to add a new property to a blog post. So what to do next? Go through the Jigsaw documentation of course! There you can find that there are some events you can listen for. One of those events, afterCollection, fires after Jigsaw goes through your files but before it actually outputs any html. Perfect! That's exactly what we are looking for here.

Time for code

Creating a helpers file

First let's create a new folder _php in the source folder of our Jigsaw application. Inside that folder create a helpers.php file and copy paste the following code, giving credit where credit is due.

<?php

/**
 * Originally found on @link https://coderwall.com/p/y1yhwg/estimate-reading-time
 *
 * Returns an estimated reading time in a string
 * idea from @link http://briancray.com/posts/estimated-reading-time-web-design/
 * @param  string $content the content to be read
 * @param int $wordsPerMinute
 * @param bool $minutesOnly
 * @param string $suffix
 * @return string  estimated read time eg. 1 minute, 30 seconds / 1 min read / ...
 */
function estimate_reading_time($content, $wordsPerMinute=200, $minutesOnly=false, $abbreviated=true, $suffix='read' ) {
    $wordCount = str_word_count(strip_tags($content));

    $wordsPerMinute = (int) $wordsPerMinute;
    if( $wordsPerMinute <= 0 ){
        $wordsPerMinute = 200;
    }

    $minutes = floor($wordCount / $wordsPerMinute);
    $seconds = floor($wordCount % $wordsPerMinute / ($wordsPerMinute / 60));

    if( $minutesOnly === true && $minutes > 0 ){
        if( $seconds >=30 ){
            $minutes++;
        }
    }

    if( $abbreviated === true ){
        $strMinutes = 'min';
        $strSeconds = 'sec';
    } else {
        $strMinutes = ($minutes == 1) ? "minute" : "minutes";
        $strSeconds = ($seconds == 1) ? "second" : "seconds";
    }

    if ($minutes == 0) {
        $returnString = "{$seconds} {$strSeconds}";
    } elseif( $minutesOnly === true ) {
        $returnString = "{$minutes} {$strMinutes}";
    } else {
        $returnString = "{$minutes} {$strMinutes}, {$seconds} {$strSeconds}";
    }

    if( is_string( $suffix ) && !empty( trim( $suffix ) ) ){
        $returnString.= ' ' . trim( $suffix );
    }

    return $returnString;
}

The return value of the original function was 2 minutes, 20 seconds. I actually wanted my estimated read time to resemble the format Medium uses (2 min read) so I added some extra parameters $minutesOnly, abbreviated and $suffix.

Loading the helpers file

Now that we have our functions file, we need to make sure it is known inside our Jigsaw application. In your composer.json you should see the following.

{
    "require": {
        "tightenco/jigsaw": "^1.3",
        "tightenco/jigsaw-blog-template": "^1.0",
        "samdark/sitemap": "^2.2"
    },
    "autoload": {
        "psr-4": {
            "App\\Listeners\\": "listeners/"
        }
    }
}

As we just need to load our file we can use composers autoload feature. In your composer.json add the following lines to the autoload property.

        "files": [
                "source/_php/helpers.php"
        ],

You should now have a composer.json looking similar to the following contents.

{
    "require": {
        "tightenco/jigsaw": "^1.3",
        "tightenco/jigsaw-blog-template": "^1.0",
        "samdark/sitemap": "^2.2"
    },
    "autoload": {
        "files": [
            "source/_php/helpers.php"
        ],
        "psr-4": {
            "App\\Listeners\\": "listeners/"
        }
    }
}

Now run the command composer update in your terminal and your function should be available for you to use.

Adding a new listener

In Jigsaw listeners are registered in the bootstrap.php file in the root of your project. We do however first need to create a new listener before we can modify the bootstrap.php file. So let's do that.

In your listeners folder create a new class file GenerateEstimateReadingTime.php and copy paste the following contents.

<?php

namespace App\Listeners;

use TightenCo\Jigsaw\Jigsaw;

class GenerateEstimateReadingTime
{
    public function handle(Jigsaw $jigsaw)
    {
        $jigsaw->getCollection('posts')->map(function($post){
            $post->estimate_reading_time = estimate_reading_time($post->getContent(), 200, true, true, 'read');
        });
    }
}

This code will fetch the posts collection which contains our blog posts and use the map function to go through all the posts and add our new property estimate_reading_time using our new function, aptly named estimate_reading_time().

After creating your new listener you can update the bootstrap.php file.

Add the following code before the afterBuild calls.

$events->afterCollections(App\Listeners\GenerateEstimateReadingTime::class);

Great! You have now registered your new listener and have access to the new property estimate_read_time within your posts collection. We are almost there.

Updating the views

Now that you have your new property in place it's time to add it to the views.

In the source folder find the file index.blade.php and open it. This is our home page. On the home page you have 2 sections, the featured posts and the other posts.

In the featured posts loop add the following code where you would like to display the estimated read time.

<span class="inline-block bg-grey-light hover:bg-grey-lighter leading-loose tracking-wide text-grey-darkest uppercase text-xs font-semibold rounded mt-2 md:ml-2 px-3 pt-px">{{ $featuredPost->estimate_reading_time }}</span>

I put my estimated read time after the title but still within the h2 tag. The featured post part of my blade file looks like the following code.

@foreach ($posts->where('featured', true) as $featuredPost)
        <div class="flex flex-col w-full mb-6">
            @if ($featuredPost->cover_image)
                <img src="{{ $featuredPost->cover_image }}" alt="{{ $featuredPost->title }} cover image" class="mb-6">
            @endif

            <p class="text-grey-darker font-medium my-2">
                {{ $featuredPost->getDate()->format('F j, Y') }} <span class="icon-star-full" title="Featured item"></span>
            </p>

            <h2 class="text-3xl mt-0">
                <a href="{{ $featuredPost->getUrl() }}" title="Read {{ $featuredPost->title }}" class="text-black font-extrabold">
                    {{ $featuredPost->title }}
                </a>
                <span class="inline-block bg-grey-light hover:bg-grey-lighter leading-loose tracking-wide text-grey-darkest uppercase text-xs font-semibold rounded mt-2 md:ml-2 px-3 pt-px">{{ $featuredPost->estimate_reading_time }}</span>
            </h2>

            <p class="mt-0 mb-4">{!! $featuredPost->excerpt() !!}</p>

            <a href="{{ $featuredPost->getUrl() }}" title="Read - {{ $featuredPost->title }}" class="btn btn-black w-24">
                Read
            </a>
        </div>

        @if (! $loop->last)
            <hr class="border-b border-grey my-6">
        @endif
    @endforeach

The other posts section contains an include file located in the folder source/_components named post-preview-inline.blade.php . In that file I did the same thing, add the estimated read time inside the h2 tag right after the title.

<span class="inline-block bg-grey-light leading-loose tracking-wide text-grey-darkest uppercase text-xs font-semibold rounded mt-2 md:ml-2 px-3 pt-px">
    {{ $post->estimate_reading_time }}
</span>

Now that the home page is done we can move to the detail view of the post. This view is located in the folder source/_layouts and is named post.blade.php . In that file you can copy paste the following code inside the p tag of the date but before the date itself.

<span class="inline-block bg-grey-light leading-loose tracking-wide text-grey-darkest uppercase text-xs font-semibold rounded mr-4 px-3 pt-px">
    {{ $page->estimate_reading_time }}
</span>

With that you normally have an estimated read time on all pages where blog posts are shown.

Beware of the variable names inside each blade file you modify. On the home page the post is called $featuredPost, in the include file it's called $post and in the detail view of the post it's actually called $page

Conclusion

This article showed just a way to add new properties to items within Jigsaw using the afterCollection event. This also showed that there are some nice little features in Jigsaw that can help you manipulate things in just the right way. Now you just need some ideas to experiment with it.

Things to do if you want

You could add a class .read-estimate to your source/_assets/sass/_base.scss file which would group common classes with the @apply directive.

.read-estimate {
    @apply inline-block;
    @apply bg-grey-light;
    @apply leading-loose; 
    @apply tracking-wide;
    @apply text-grey-darkest; 
    @apply uppercase; 
    @apply text-xs; 
    @apply font-semibold; 
    @apply rounded;
}

After which you can replace those classes on the span tags in your blade files.