Statamic Tip: Using Forms with Static Caching

Update: While I'm keeping this article here for people using older versions of Statamic, the approach detailed here is no longer needed. Since Statamic v3.4 CSRF tokens are automatically refreshed once a cached site is loaded.


I've talked about it before: one of my favorite Statamic features is static caching. This way every page or your website is only generated once, saved as an HTML file, and then served to each following visitor with lightning speed.

The site you're currently on is fully cached. So are almost all sites I build for clients. It's simply too good to pass up. But there are a few issues with static caching: certain features just won't work. This post is about one of those.

Why Forms Don't Work with Static Caching

Statamic comes with a very capable form builder out of the box. You can create any form you need right in the backend, show it on your site and have it work straight away including validation and data handling. Neat!

But once you activate static caching, your forms will quickly stop working. Instead, you will only see an error page: 419 - Page Expired

The reason behind this error is the CSRF token - a security feature by Laravel, which prevents cross-site request forgery attacks.

error-419-page-expired.jpg

The Laravel 419 error page displayed when submitting a form without a valid CSRF token.

Whenever you load a page, Laravel assigns that request a token which is valid for 15 minutes only. Any post request from that page (e.g. when submitting a form) has to include this security token which will be checked by the server.

But since that token is part of the cached HTML returned to every user, every request for that page is now being served the exact same token - which will be expired starting 15 minutes after the page was cached. The server therefore classifies every single one of those post requests as invalid - and your form no longer works.

The simple fix would be to exclude all sites containing forms from caching. But that can be cumbersome if you have a lot of them, and of course you wouldn't be able to benefit from the desired performance boost on those pages.

But not to worry, there is a simple way to use both the form builder and static caching, and it's not complicated at all.

Solution: The Dynamic Token Plugin

When I first ran into this issue I was stumped. It took a bit of research to understand the issue and find a simple and reusable solution for all of our projects. After taking inspiration from the popular Peak starter kit I set up a small plugin, which you can find for free in the Marketplace: Dynamic Token

After installing it, all you have to do is to include the antlers tag {{ dynamic_token }} anywhere on every page which includes a form. I usually just add it to my layout view or my form component, that way I don't have to worry about forgetting to include it on a page.

This will add a simple JavaScript function to your page which refreshes the CSRF token on page load, and then again every 15 minutes, meaning you never have an expired security token again.

Just like that the server accepts your form submissions again.

Even Better: Submitting Forms via AJAX

But we can do better. When the form gets submitted, we ideally want to show some sort of feedback to the user. At the very least any failed submits should trigger some sort of warning, letting them know why the request failed.

Once again this is not possible with static caching - if you submit the form the page refreshes and, no matter what happened with the form data validation, you're just shown the same document again.

So we can't just process the form with PHP alone anymore, instead we need to send the data to the server as an asynchronous request, meaning outside of a standard page request. This can easily be achieved utilizing JavaScript.

{{ form:create handle="myform" x-data="formSetup()" x-on:submit.prevent="getResults($el)" }}
	// your form output        
{{ /form:create }}
<script>
	function formSetup() {
		return {
			formResult: false,
			getResults(formElement) {
				let context = this;
				let data = new FormData(formElement);
				let xhr = new XMLHttpRequest();
				xhr.open(formElement.method, formElement.action);
				xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhr.send(data);
				xhr.onload = function() {
					if (xhr.readyState === xhr.DONE) {
						context.formResult = JSON.parse(xhr.response);
					}
				};
			}
		};
	}
</script>
An easy way to submit a Statamic form via Ajax, using AlpineJS.

This example shows how simple it is using AlpineJS. The form instance is given a formResult variable as well as a getResults() function. When the user submits the form, x-on:submit.prevent stops the default submission and instead calls on the function to send the form data to the endpoint using AJAX.

The server handles the submission and returns either a success message or a list of errors. This return data is stored in the formResult variable, from which you can show it to the user with JavaScript.

Not only does this mean the form works perfectly fine, it's a also a better user experience than refreshing the page on submit.

So there you have it: user friendly forms and super speedy websites. Hope this helps you with future Statamic projects.

More Posts