helthe/turbolinks
Laravel integration for Turbolinks, the JavaScript library that speeds navigation by swapping page bodies instead of full reloads. Adds middleware, helpers, and Blade directives to handle Turbolinks visits, redirects, and asset tracking in Laravel apps.
helthe/turbolinks is a Symfony component/bundle, not a Laravel package — there is no service provider, no publishable config, and the Twig helpers are irrelevant to Blade. In a Laravel app you will use it for exactly one thing: the server-side contract Turbolinks expects, above all the Turbolinks-Location header on redirects after non-GET requests. Everything else is frontend JavaScript.
Minimal steps:
Install the JS side (this is where 90% of the value is):
yarn add turbolinks
// resources/js/app.js
import Turbolinks from 'turbolinks'
Turbolinks.start()
If you actually pull the PHP package (composer require helthe/turbolinks), look first at TurbolinksListener — it is the only class worth reading. It inspects the response, and when a redirect follows a non-GET request it stamps the Turbolinks-Location header with the redirect target. The HeltheTurbolinksBundle DI wiring, Twig extension, and Turbolinks helper are Symfony-specific and will not load in Laravel.
Reproduce that listener as Laravel middleware rather than fighting HttpFoundation↔Illuminate type conversions:
// app/Http/Middleware/TurbolinksLocation.php
public function handle($request, Closure $next)
{
$response = $next($request);
if (! $request->isMethod('GET') && $response->isRedirect()) {
$response->headers->set('Turbolinks-Location', $response->headers->get('Location'));
}
return $response;
}
Register it in the web group of app/Http/Kernel.php, after StartSession (it needs the final response, session middleware included).
Wire the layout so Turbolinks can see asset versions:
<link rel="stylesheet" href="{{ mix('css/app.css') }}" data-turbolinks-track="reload">
<script src="{{ mix('js/app.js') }}" data-turbolinks-track="reload" defer></script>
First real use case: a POST /posts that does return redirect()->route('posts.index'). Without the header Turbolinks follows the redirect with an extra GET; with it, the history entry is corrected and there is one fewer round trip.
Redirect-after-write is the core pattern. Every store/update/destroy that redirects should produce a response carrying Turbolinks-Location. Middleware is cleaner than touching 40 controllers; if you need per-action control, a RedirectResponse macro called from controllers works too.
Keep the layout a single Turbolinks-managed shell. Turbolinks replaces <body> and merges <head>; put your nav, flash container, and footer in layouts/app.blade.php and never conditionally render a different document shell per page.
Flash messages need explicit lifecycle handling. Snapshots are cached, so a flash rendered on page A can reappear when the user navigates back to A. Prefer removing it before the snapshot is taken:
@if (session('status'))
<div class="alert alert-success" data-turbolinks-clear>{{ session('status') }}</div>
@endif
document.addEventListener('turbolinks:before-cache', () => {
document.querySelectorAll('[data-turbolinks-clear]').forEach(el => el.remove())
})
Re-initialize third-party JS on turbolinks:load, not DOMContentLoaded. Datepickers, select2, tooltips, charts — anything that binds to DOM nodes — must be re-bound each visit. Guard against double-initialization with a data attribute or destroy() before init().
CSRF and the meta tag. Blade @csrf inside forms works untouched, but if you use axios/fetch, the csrf-token meta tag is replaced on each visit, so set defaults on turbolinks:load:
document.addEventListener('turbolinks:load', () => {
const token = document.querySelector('meta[name="csrf-token"]')?.content
if (token && window.axios) window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token
})
Opt out surgically. data-turbolinks="false" on a link or form for file uploads, OAuth callbacks, or full-page redirects to third parties. Use Turbolinks.visit(url, { action: 'replace' }) when you want to avoid pushing a history entry (e.g. after a login redirect).
Testing. PHPUnit feature tests do not simulate a Turbolinks client, so assert the contract directly:
$this->post('/posts', $data)
->assertRedirect('/posts')
->assertHeader('Turbolinks-Location', url('/posts'));
When not to use it at all. If the app is Livewire- or Inertia-heavy, Turbolinks is redundant or actively harmful — Inertia is its spiritual successor, and Livewire fights body-swapping. Skip the package entirely in those cases.
HeltheTurbolinksBundle or the Twig extension. Port the six-line listener into middleware and move on; the rest of the package is dead weight.Turbolinks-Location, absolute URLs, no data-turbolinks-temporary). If you load Turbolinks 5 from npm, expect to write the middleware yourself — the bundle's assumptions may not match.Turbo-Location instead).FormRequest plus redirect()->back()->withErrors() keeps the flow intact; returning JSON or a raw error page from a non-GET breaks the expected navigation model.Turbolinks-Location should be an absolute URL. url()->to(...) or $response->headers->get('Location') resolved against the app URL. Relative paths work in most clients but not universally.data-turbolinks-track="reload" only works with fingerprinted asset URLs. That means mix() / @vite — a plain /css/app.css has no digest, Turbolinks can't detect the change, and users keep stale assets until a hard refresh.turbolinks:load to refresh, and call Turbolinks.clearCache() after logout so the authenticated shell doesn't get restored on back-navigation.app.js (e.g. one in layout and one in a partial) means Turbolinks.start() runs twice and turbolinks:load fires twice. Check for duplicate <script> tags before debugging anything else.Turbolinks.supported === false in the console means the browser fell back to full navigation — your header is irrelevant. In the Network panel, Turbolinks-initiated requests are initiated by turbolinks.js and carry the Turbolinks-Referrer / pjax headers depending on version; if you see none, the JS isn't loaded.VerifyCsrfToken so a CSRF failure's redirect also gets stamped.symfony/http-foundation and symfony/http-kernel. Requiring it into a Laravel 10/11 app is likely to produce dependency conflicts for zero runtime benefit. If you only want the header behavior, don't install the package — copy the logic.Vary headers), a Blade partial/component for the flash container, and the turbolinks:before-cache / turbolinks:load events on the JS side, which are the only reliable hooks for teardown and setup of per-page behavior.How can I help you explore Laravel packages today?