--- title: "Content caching with Statamic" description: "Cachin' checks, breakin' (bottle)necks." pubDate: 2025-05-11 heroImage: "/images/blog/img_9206.jpeg" tags: [laravel] --- So this past week, I rewrote my website again (pause for audible gasp). I've had many different versions of my website over the years, each a reflection of my current technology obsession at the time. From Hugo, to Next.js, to Astro, back to Next.js, then Nuxt, then Svelte, then back to Nuxt... you get the picture. I think I've spent more time rebuilding my own personalized blogging engine than actually writing meaningful content. That's boring though, and neither here nor there. I've landed on the final iteration of it (for real this time, I swear) with Laravel and Statamic. I've been working a lot with Statamic lately building websites for some clients and have quickly placed it as my go to CMS. It's approachable, extremely flexible, and offers a good balance of customization and ease of use with the built in utilities it provides. I took the past week to migrate my hacked up blogging engine that was essentially markdown files manually/programmatically parsed with [CommonMark for PHP](https://commonmark.thephpleague.com/) that heavily relied on [shiki](https://github.com/shikijs/shiki) for code highlighting (by way of Spatie's [shiki-php](https://github.com/spatie/shiki-php)) and shoved in a SQLite database. It was nice being in full control of the pipeline (I even [wrote](/blog/content-driven-websites-with-php-and-laravel) about it!), but these days, I just want to focus on writing and content rather than building yet another blog engine (Y.A.B.E.) to scratch my own itch. That's where [Statamic](https://statamic.com/) comes in. With it, I centralize all of my content in flat files and focus on writing. I use a lot of code examples in all my writing, so naturally I wanted to bring over Shiki for highlighting. Luckily, Statamic makes it easy to hook into their markdown parsing process, providing a [sensible](https://github.com/statamic/cms/blob/5.x/src/Markdown/Parser.php#L79) set of defaults for parsing said markdown with the PHP League's CommonMark package. All that's needed is one additional bit of configuration in my `AppServiceProvider.php` to highlight code with Shiki, again with the help of Statie's shiki-php: ```php final class AppServiceProvider extends ServiceProvider { public function boot(): void { self::configureMarkdown(); // Other stuff... } private function configureMarkdown(): void { Markdown::addExtensions(function () { return [ new HighlightCodeExtension(theme: 'one-dark-pro'), ]; }); } // Other stuff... } ``` This works™️, but unfortunately Shiki is quite resource intensive on a browser page due to all the custom styling that goes into each section of highlighted code. Throw in fancy things like blurred or focused lines, and you can easily weigh down a page's load time pretty significantly. An easy way to get around this is using Statamic's [cache tag](https://statamic.dev/tags/cache#tag-parameters-and-contents). For a blog page, my [Antler's](https://statamic.dev/antlers) file (Statamic's custom DSL for markup) looks like this: ```html

{{ title }}

{{ topics limit="1" }} {{ title }} {{ /topics }} {{ date format="M j, Y" }}
{{ image }} {{ alt }} {{ /image }}
{{ cache :key="slug" }} {{ content }} {{ /cache }}
``` The `:key` here (no pun intended) for the cached content being the slug of the blog page. Now since I'm not providing a TTL for the cached content, the content of my blog posts will be cached forever, until the end of time, long after all matter in the known universe has decayed to iron-56. To get around this, we can programmatically bust the cache. Now, the great benefit of Statamic being a CMS is that I can update content as I please without the need to physically deploy any new code. By default, Statamic uses flat files to store content alongside some metadata about page the content is associated to. Even _better_, Statamic will also fire events anytime the content is updated. Statamic will store cached content in your cache store of choice with keys along the lines of `statamic_cache_{{key}}` that you specify, which in my case, are the slugs to the blog itself. Laravel makes this _way_ too easy to then hook into the content saved event and bust the cached value like so: ```php final class AppServiceProvider extends ServiceProvider { public function boot(): void { // Other stuff... self::configureEvents(); } private function configureEvents(): void { Event::listen(function (EntrySaved $event) { /** @var array{slug: string} $entry */ $entry = $event->entry; if (Cache::has($entry['slug'])) { Cache::forget($entry['slug']); } }); } // Other stuff... } ``` I register this event listener in my `AppServiceProvider.php` primarily because I'm incredibly lazy and it's the only event I'm concerned about on my website. Now, anytime I update my content like fixing a typo on an old post, I'll bust that particular cache entry and re-cache the updated value when the page is first visited. Pretty sweet! There's one other issue. Remember how I said Shiki is resource intensive? Well... that's because it is. If you have a lot of code being highlighted, it'll take awhile to load your page - not a great user experience. For instance, here's what the above highlighted code looks like once Shiki does its magic: ```html
	
		
			final
			
			class
			
			AppServiceProvider
			
			extends
			
			ServiceProvider
		
		
			{
		
		
			
			public
			
			function
			
			boot
			(): 
			void
		
		
			    {
		
		
			
			// Other stuff...
		
		
			
			self
			::
			configureEvents
			();
		
		
			    }
		
		
		
			
			private
			
			function
			
			configureEvents
			(): 
			void
		
		
			    {
		
		
			
			Event
			::
			listen
			(
			function
			 (
			EntrySaved
			
			$event
			) {
		
		
			
			/** 
			@var
			
			array{slug:
			 string} $entry */
		
		
			
			$entry
			
			=
			
			$event
			->
			entry
			;
		
		
		
			
			if
			 (
			Cache
			::
			has
			(
			$entry
			[
			'slug'
			])) {
		
		
			
			Cache
			::
			forget
			(
			$entry
			[
			'slug'
			]);
		
		
			            }
		
		
			        });
		
		
			    }
		
		
			
		
		
			
			// Other stuff...
		
		
			}
		
		
	
``` Not to mention the fact I'm not tree shaking themes or languages that I'm not using either. I probably could... but I'll save that for a rainy day. So that's great that we cache the content, but we need _someone_ to trigger the caching in the first place. And that sucks. I don't know about you, but I'm not waiting 20 seconds for a blog post to load so I can read it. So how do we get around that? [Commands](https://laravel.com/docs/12.x/artisan) to rescue! With Statamic, while templating with Antlers offers a seamless experience for pulling content out of storage onto the page, it also gives us the ability to [query content](https://statamic.dev/content-queries) in an Eloquent-like fashion. Using this approach, I'm able to spin up a simple command to run on deployment whenever I push new content: ```php final class WarmBlogCacheCommand extends Command { /** * @var string */ protected $signature = 'app:warm-blog-cache'; /** * @var string|null */ protected $description = 'Warms the Statamic cache with shiki content for the blog.'; public function handle(): void { /** @var EntryCollection $entries */ $entries = Entry::query() // @phpstan-ignore-line ->where('collection', 'blogs') ->where('published', true) ->get(['content']); $this->info("Warming cache for {$entries->count()} blog entries..."); /** @var \Statamic\Entries\Entry $entry */ foreach ($entries as $entry) { /** @var string $key */ $key = $entry->slug(); $this->info("Caching: $key"); /** @var string $content */ $content = $entry->get('content'); Cache::rememberForever($key, fn () => Markdown::parser('default')->parse($content)); $this->info("Cached: $key"); } /** @var \Statamic\Entries\Entry $entry */ foreach ($entries as $entry) { /** @var string $key */ $key = $entry->slug(); if (! Cache::has($key)) { $this->error("Cache missing for: $key"); } } $this->info('Blog cache warming complete!'); } } ``` Nothing fancy in the above. I pull all of the entries in my blog collection, run them through the markdown parser that's using Spatie's Shiki extension for code highlighting with the use of `Markdown::parser('default')`, and voilà - no one has to pay the price for waiting for content to be parsed! I've gotta fire this command _somewhere_ though. My site lives on a Hetzner VPS that's provisioned by Forge (as the good Lord intended). To re-cache all the entries, I'm able to plop this command right into my deploy script: ```bash cd /home/forge/joeymckenzie.tech git pull origin $FORGE_SITE_BRANCH $FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader # Prevent concurrent php-fpm reloads touch /tmp/fpmlock 2>/dev/null || true ( flock -w 10 9 || exit 1 echo 'Reloading PHP FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9 Deployment Complete! ``` Unfortunately, _someone_ still has to pay the price when I bust the cached entries when I'm editing blog content in production, though I try to make sure that's always myself. The beauty of Statamic storing content in flat files is that my locally developed changes are always in sync with what's deployed. So a quick fix here or there, deploy, re-cache the content, and boom. All in a day's work. Well, that's it for now. And if you haven't tried Statamic yet, this is your sign. Until next time, friends!