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.
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.
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:
get entries
that are in the collection posts
that are published
that don't have the id of the current post
return results
randomly shuffle their order
take the first three entries
turn them into an array with only the data we need
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.
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.
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.