Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Turbolinks Bundle Laravel Package

helthe/turbolinks-bundle

Helthe Turbolinks Bundle integrates Turbolinks with Symfony, replacing full page reloads with faster AJAX navigation. It adds the required JavaScript, response headers, and Twig helpers so your Symfony app feels quicker with minimal changes.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: helthe/turbolinks-bundle is a Symfony bundle (Helthe\Bundle\TurbolinksBundle), not a Laravel package. You cannot composer require it into a Laravel app, and with 0 dependents, a Symfony 2-era structure, and no commits in years, you shouldn't try. What has value here is the behavior the bundle packages up — Turbolinks request detection and redirect rewriting — which you can reproduce in Laravel in about 60 lines.

Read these two files first in the repo, because they are the entire idea:

  1. EventListener/TurbolinksListener.php — inspects the response on kernel.response. If the request is a Turbolinks visit and the response is a redirect, it strips the 302 body and replaces it with the Turbolinks-Location header so the client can history.pushState correctly.
  2. Turbolinks/Turbolinks.php (the helthe.turbolinks service) — the request detector (isTurbolinksRequest()) and helpers for setting/reading the redirect location.

Then look at the Twig extension — it exists only to expose turbolinks_location() to templates, which in Laravel becomes a Blade directive or just a response header.

Minimal Laravel setup (your actual first use case):

npm install turbolinks

resources/js/app.js:

import Turbolinks from 'turbolinks'
Turbolinks.start()

resources/views/layouts/app.blade.php:

<head>
  <meta name="csrf-token" content="{{ csrf_token() }}" data-turbolinks-track="reload">
  <script src="{{ mix('js/app.js') }}" data-turbolinks-track="reload" defer></script>
</head>

First real win: every internal link in your app now swaps <body> without a full reload, and your existing Laravel redirects (return redirect()->route('posts.index')->with('status', 'Saved')) keep working because Turbolinks 5 follows redirects via fetch and reads the final response.url. You do not need the Symfony bundle's header-rewriting hack for the common case — that was largely a Turbolinks Classic (2.x) workaround.


Implementation Patterns

1. Request detection middleware

The bundle's isTurbolinksRequest() equivalent. Useful for analytics exclusion, conditional fragment rendering, and skipping layout on partial visits.

// app/Http/Middleware/DetectTurbolinks.php
public function handle($request, Closure $next)
{
    $isTurbolinks = str_contains($request->userAgent() ?? '', 'Turbolinks');

    $request->attributes->set('turbolinks', $isTurbolinks);
    view()->share('turbolinks', $isTurbolinks);

    return $next($request);
}

Turbolinks 5 appends Turbolinks to the User-Agent and sends a Turbolinks-Referrer header on history restoration (back/forward). Use that header when you need to know the visit came from a restoration rather than a click.

2. The redirect-location pattern (the bundle's core trick)

Reimplement the listener as a terminable middleware. This matters when you redirect to a URL that differs from what the client can infer — redirect()->back(), redirect()->intended(), or a redirect after a DELETE where the referer is stale.

// app/Http/Middleware/TurbolinksRedirects.php
public function handle($request, Closure $next)
{
    $response = $next($request);

    if ($response->isRedirection()
        && str_contains($request->userAgent() ?? '', 'Turbolinks')) {
        $response->headers->set('Turbolinks-Location', $response->headers->get('Location'));
    }

    return $response;
}

Register it globally in app/Http/Kernel.php after StartSession so flash data survives.

3. JS lifecycle hooks — the actual day-to-day work

This is where 90% of your integration bugs live. DOMContentLoaded fires once; turbolinks:load fires on every visit.

document.addEventListener('turbolinks:load', () => {
  // re-init dropdowns, date pickers, Select2, charts
  initTooltips()
})

document.addEventListener('turbolinks:before-cache', () => {
  // tear down anything you don't want frozen into the snapshot
  document.querySelectorAll('.tooltip').forEach(el => el.remove())
  if (window.Chart) window.Chart.instances.forEach(c => c.destroy())
})

document.addEventListener('turbolinks:before-visit', (e) => {
  if (formIsDirty()) e.preventDefault()
})

Alpine.js and Livewire both need care: Alpine's x-data re-initializes on turbolinks:load fine, but Livewire components must be re-registered or the page will look interactive and do nothing. If your app is Livewire-heavy, consider Turbo Drive instead or scope Turbolinks to specific sections with data-turbolinks="false" on the Livewire routes.

4. Forms, validation, and flash messages

Turbolinks intercepts non-GET form submissions the same way it intercepts links. Laravel's standard validate() → 302 back with $errors works unchanged, because Turbolinks follows the redirect and the 200 response contains your re-rendered Blade with the error bags.

The one real hazard is inertia-less partial rendering: don't return a bare view (no layout) from a controller that a Turbolinks form hits — the client replaces the whole <body> with your fragment. Always return full pages, or gate on turbolinks:

return $request->attributes->get('turbolinks')
    ? view('posts._list', compact('posts'))
    : view('posts.index', compact('posts'));

Only do this if you're explicitly driving Turbolinks.visit from JS — otherwise keep it simple and always render the full page.

5. Asset tracking

The bundle had no equivalent because Symfony's asset pipeline was already cache-friendly. In Laravel, put data-turbolinks-track="reload" on your mix()/vite() script and CSS tags. Turbolinks compares the tracking value on every visit and does a full reload if it changed — this is your deploy cache-busting mechanism and it's essential if you use long-lived CDN caching.


Gotchas and Tips

CSRF tokens go stale in the cache. Turbolinks snapshots the DOM. A user who leaves a tab open past the session lifetime, then navigates back, will submit an old _token and get a 419. Fix: put data-turbolinks-track="reload" on the <meta name="csrf-token"> tag so a changed token forces a reload, or refresh from /csrf-token on turbolinks:load.

Never cache authenticated-sensitive pages by accident. Turbolinks keeps the last snapshot in memory and on back/forward shows it without hitting the server. If you log out, the browser back button will still display the previous user's page. Turbolinks has no built-in logout handling — you must call Turbolinks.clearCache() before Turbolinks.visit('/logout'), or send Cache-Control: no-store on pages where it matters.

302 after POST is technically wrong; 303 is correct. Laravel's redirect() uses 302, which browsers historically convert to GET anyway. Turbolinks handles it, but if you're debugging "the form submitted twice" or "the redirect came back as POST," use return redirect()->route(...)->setStatusCode(303).

Downloads and external links need escape hatches. Any <a> pointing off-domain or to a file download should be data-turbolinks="false" (or you should call Turbolinks.visit(url, { action: 'advance' }) deliberately). Turbolinks will happily fetch a PDF into your <body> if you let it.

Vary: Turbolinks matters if you use full-page HTTP caching. If a response differs based on the request being a Turbolinks visit, your CDN/Varnish will serve the wrong variant. Set the Vary header from the middleware, or don't differentiate responses at all — which is the sane default.

Debugging: enable Turbolinks.setProgressBarDelay(0) in dev to see every request, and watch turbolinks:request-start / turbolinks:load in the console. The Chrome extension "Turbolinks 5" shows the current visit object and cache state. On the server side, tail the log for the Turbolinks UA string to confirm what's actually being sent — the middleware check is easy to get wrong with a strpos vs str_contains mismatch on an empty UA.

Extension points, if you want to port more of the bundle: the listener is a good place to add Turbolinks-Location on redirect()->back() only (the common failure case), rather than on every redirect — rewriting all redirect headers is more invasive than needed and can confuse non-Turbolinks clients behind a shared proxy.

The migration path is Turbo, not this package. Turbolinks 5 was renamed and superseded by Hotwire Turbo (Turbo Drive), which is actively maintained and has an official @hotwired/turbo package. The event names change (turbolinks:load → turbo:load) but every pattern above maps 1:1. If you're starting fresh, start with Turbo and skip Turbolinks entirely — the only thing you'd lose from helthe/turbolinks-bundle is nothing, because it was written for a version of Turbolinks that no longer exists.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels