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

Turbo Laravel Laravel Package

tonysm/turbo-laravel

Turbo Laravel brings Hotwire Turbo to Laravel, letting you build fast, SPA-like applications with server-rendered HTML and minimal JavaScript. It provides Turbo Drive, Frames, and Streams integration, view helpers, and conventions for reactive pages without a full frontend framework.

View on GitHub
Deep Wiki
Context7

Getting Started

hotwired-laravel/turbo-laravel is Tony Messias' integration layer between Hotwired Turbo and Laravel. It exists to solve the two things Turbo gets wrong out of the box in a Laravel app: redirect semantics after form submissions and validation error rendering (422).

Install:

composer require hotwired-laravel/turbo-laravel
php artisan turbo:install
npm install && npm run dev

turbo:install publishes config/turbo-laravel.php, adds the @hotwired/turbo-laravel import to resources/js/app.js, and registers HotwiredLaravel\TurboLaravel\Http\Middleware\TurboMiddleware in the web middleware group. Verify the middleware landed in bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php — if it's missing, redirects and validation will silently behave wrong and nothing else in the package will make sense.

First use case — the one that justifies the package on day one: a resourceful form.

public function store(StorePostRequest $request)
{
    $post = Post::create($request->validated());

    return Turbo::redirectTo(route('posts.show', $post));
}
<x-turbo-frame id="post_form">
    <form method="POST" action="{{ route('posts.store') }}">
        @csrf
        ...
    </form>
</x-turbo-frame>

With the middleware in place, a failing StorePostRequest returns a 422 with a turbo-stream that renders errors into the page — no custom error handling, no @error gymnastics. A successful POST gets a 303 See Other, so Turbo doesn't re-submit the form on back/refresh.

Where to look first: the published config/turbo-laravel.php, then Turbo facade autocompletion (HotwiredLaravel\TurboLaravel\Facades\Turbo). Skim the package's Http/Middleware and TurboStream classes to understand what the magic actually emits — that's what you'll debug against.

2.6.0 note: this release is purely Laravel 13.x compatibility (PR #159). No API changes vs 2.5.1. If you're on Laravel 13, require ^2.6; otherwise 2.5.x behaves identically.

Implementation Patterns

Turbo Streams from controllers. Return the stream directly; the package sets the correct Content-Type: text/vnd.turbo-stream.html.

return turbo_stream()
    ->append('posts', view('posts._post', compact('post'))->render())
    ->prepend('flash', view('shared._toast', ['message' => 'Saved'])->render());

Available actions mirror the Turbo spec: append, prepend, replace, update, remove, plus redirectTo and refresh. A turbo_stream() response is also what you return for 422 responses you build by hand.

Frame-scoped partial rendering. A frame request only needs the frame's content, but Turbo still expects a valid document. The cleanest pattern is a dedicated layout or a Blade conditional, not two separate controllers:

{{-- resources/views/layouts/app.blade.php --}}
@if (request()->header('Turbo-Frame'))
    @yield('content')
@else
    <html>... @yield('content') ...</html>
@endif

Pair this with lazy frames (src on <x-turbo-frame>) for expensive widgets so the page paints immediately.

Flash messages + redirects. Turbo::redirectTo() is the right call for any non-GET action. Keep using normal redirect() for GET requests. If you already have controllers sprinkled with redirect()->route(), the middleware makes them mostly work, but you lose the explicit 303 and will occasionally hit resubmit-on-refresh bugs.

Broadcasting to other clients. The package bridges turbo_stream() into Laravel broadcasting, so an event can push a stream over your websocket driver:

broadcast(turbo_stream()->append('posts', view('posts._post', compact('post'))->render()))->toOthers();

Treat this as the integration point for "live page" features (comment feeds, notifications) and keep the payload rendering in a single Blade partial shared with the HTTP path.

Turbo Native / mobile shells. For native wrappers, use Turbo::visit() and Turbo::refresh() so navigation goes through the Turbo Native adapter rather than a plain redirect. Gate these on the request being a Turbo Native visit; don't let them leak into web responses.

Progressive enhancement boundary. Put interactivity inside frames/streams and leave plain links and full-page forms outside them. When a form genuinely shouldn't be Turbo-driven (file uploads with progress bars, third-party payment iframes, OAuth redirects), opt out explicitly with data-turbo="false" rather than fighting the client.

Gotchas and Tips

The middleware is the whole package. A missing TurboMiddleware produces the two classic symptoms: (1) validation errors vanish because Turbo follows the 302 and drops the error bag, (2) stale PUT/DELETE re-submissions on refresh. If something feels broken, check middleware registration before anything else.

422 is a contract, not a status code. Turbo only renders form errors in place when the response is 422 and carries a turbo-stream body. Returning 422 with JSON or a redirect will fail silently in the browser but pass your HTTP tests — test the rendered markup, not just the status.

Return a 303 for non-GET redirects. This is the actual fix Turbo::redirectTo provides. Plain redirect() after a POST gives 302, which some clients/proxies handle differently and which can resurrect the request on refresh.

Turbo Drive caching will bite your JavaScript. DOMContentLoaded fires once; use turbo:load for init and turbo:before-cache to tear down anything stateful (maps, editors, open modals). Flash/toast elements rendered server-side get cached into the preview — clear them in turbo:before-cache or the user sees the same success message on every back-navigation.

Frame responses must still contain the frame element. Returning a bare fragment with no matching <turbo-frame id="..."> makes Turbo silently discard the response. Keep the frame wrapper in the partial (or in the layout conditional) and match the id exactly.

Duplicate element IDs across frames. Two frames rendering the same id inside a stream target is the most common "why did the wrong thing update" bug. Keep IDs unique per rendered unit and target by container, not by the item.

Config quirks. The published config governs broadcasting setup and Turbo Native detection; re-publish after major upgrades to diff new keys (php artisan vendor:publish --tag=turbo-laravel-config --force into a scratch dir first, don't blind-overwrite).

Debugging. In DevTools, filter XHR by text/vnd.turbo-stream.html to see exactly what came back; Turbo silently ignores responses it can't parse. Add data-turbo-action="advance" only when you actually want history entries — accidental history spam is a common frame bug.

Extension points. Custom stream actions via Turbo:: stream helpers, and Blade components (<x-turbo-frame>) you can publish and override into resources/views/components. Prefer overriding the blade component over forking the package when you need different markup.

Testing. Turbo stream responses are plain HTML with <turbo-stream action="..." target="..."> tags, so assertSee/assertSeeInOrder work and are more robust than asserting on status codes alone. Assert the Content-Type contains turbo-stream for stream responses. For form failures, assert 422 and the presence of the target ID in the returned stream.

Upgrade path for 2.6.0. No code changes required from 2.5.1. If you're pinning laravel/framework to ^13.0, bump the constraint to hotwired-laravel/turbo-laravel: ^2.6 and run the suite — the PR is CI-compatibility only, but Laravel 13's middleware/kernel changes make a smoke test of the web group worth doing.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle