stoffel.io

Statamic Tip: Showing Random Entries with Static Caching

Statamic's full-measure static caching feature can give your website a huge speed boost. But since it serves HTML straight from the web server without ever touching your app, it does present some challenges when trying to use certain features.

One such functionality is the ability to output randomized or frequently changing data. A common use case is adding random read more links underneath each of your blog posts. Or maybe you want to display the recipe of the day on your cooking page, or always load a random sponsor banner into your sidebar.

None of these are possible when full-measure static caching is activated. The site will only be generated once, and whichever random entries were selected on that initial load will then be cached and served to every user until you empty the cache.

But there are ways to get the best of both worlds: random output and static caching. Here are two approaches using a read more blog posts feature as an example.

Approach 1: Caching your Entire Collection

Since the HTML output is being cached, we cannot rely on Antlers/PHP to do our randomization and selection for us. Instead, we need to find a way to always have all posts available and then output a random selection using JavaScript.

To get the data of all posts (except the one currently displayed) we can reach for the collections tag to get all posts, filter out the current one, and store them into a variable called posts. In the next step we can pass that data as a JSON object into AlpineJS.

Finally AlpineJS' x-for directive will turn this data into a list of linked entries. By randomly sorting the array of posts with sort() and then only retaining the first three entries using slice(), we get three random posts each time the page is reloaded.

{{ collection:posts id:not=id as="posts" }}
  <ul x-data="{ posts : {{ posts | to_json | sanitize:true }} }">
    <template x-for="post in posts.sort(() => .5 - Math.random()).slice(0,3)" :key="post.id">
      <li>
        <a x-bind:href="post.url" x-text="post.title"></a>
      </li>
    </template>
  </ul>
{{ /collection:posts }}
This snippet gets all Statamic posts except the current one, loads them into AlpineJS and displays three entries at random.

This works great for small data sets. However, once you have dozens or hundreds of posts, storing the entire collection in every single posts output can become a bit unwieldy.

Another caveat are the limitations of the collection tag when it comes to filtering and sorting entries. If you want any custom functionality, this solution quickly becomes limiting.

Approach 2: Serving your Random Data via a Custom Controller

In order to not store everything in the HTML and get a bit more flexibility, we need to add a different way to fetch the data from our server on each page load.

If you're already using the REST or GraphQL API, this would probably best be solved by creating a custom endpoint that returns this data. Since I'm not using an API and don't feel like setting it up for such a small feature, I'm going to go the custom controller route instead.

Starting out we need to create a new route that will be used to request the data. Let's name it /readmore and pass along the id of the current post so we can skip that one when selecting our posts.

/routes/web.php
<?php use Illuminate\Support\Facades\Route; use App\Http\Controllers\ReadMoreController; Route::get('/readmore/{current}', [ReadMoreController::class, 'morePosts']);
The new route pointing to a morePosts() function in our custom controller.

Now that the controller is accessible, let's actually create it with php artisan make:controller ReadMoreController. Then we add the morePosts() function that accepts a post id as a parameter.

The content of the function might look like a lot, but it's pretty easy to read:

  1. get entries

  2. that are in the collection posts

  3. that are published

  4. that don't have the id of the current post

  5. return results

  6. randomly shuffle their order

  7. take the first three entries

  8. turn them into an array with only the data we need

  9. return the array as JSON

The only tricky part is having to use toAugmentedArray() instead of toArray(). The reason is simple: id and url are augmented values, meaning they aren't stored in the entry itself and aren't available without augmentation.

/app/Http/Controllers/ReadMoreController.php
<?php namespace App\Http\Controllers; use Statamic\Facades\Entry; class ReadMoreController extends Controller { public function morePosts($current) { $entries = Entry::query() ->where('collection', 'posts') ->where('status', 'published') ->where('id', '!=', $current) ->get() ->shuffle() ->limit(3) ->toAugmentedArray(['id', 'title', 'url']); return json_encode($entries); } }
The custom contoller creating the requested selection of posts and returning it as JSON.

Now that we have a route and a controller returning the required data, we can set up AlpineJS to fetch the posts on each page load.

Instead of using Antler's collection tag, we simply add a getMorePosts() function to AlpineJS which fetches the data and saves it to our posts variable. The actual output is the same as in the first approach.

<ul x-data="readMoreData()" x-init="getMorePosts();">
  <template x-for="post in posts" :key="post.id">
    <li>
      <a x-bind:href="post.url" x-text="post.title"></a>
    </li>
  </template>
</ul>
<script>
  function readMoreData() {
    return {
      posts: {},
      getMorePosts() {
        fetch('https://stoffel.io/readmore/{{ id }}')
        .then(res => res.json())
        .then(data => {
          this.posts = data;
        });
      }
   }
}
</script>
In our template file we add a function to AlpineJS that fetches the information provided by the controller.

By calling it within x-init, the function is automatically called as soon as AlpineJS is initialized.

Both these examples could of course also use some error handling to account for an empty collection or the fetch returning an error message. Feel free to expand on them for your own use case.

More posts: