ruwork/frujax-bundle
The Ruwork Frujax Bundle integrates Frujax with Symfony to simplify AJAX-powered interactions in server-rendered applications. It helps handle asynchronous requests, update page fragments, and keep controllers and templates in control without full-page reloads.
Frujax is a Symfony bundle for "friendly AJAX": the same route/render pipeline serves either a full page or a fragment, decided by a request marker, so forms and links keep working without JavaScript. It is not a Laravel package — it targets Symfony 3.x/4.0 (last release July 2018, repo archived). In a Laravel codebase you are porting the pattern, not installing the dependency.
Minimal steps to understand it:
Resources/config/services.* first. The whole feature is usually three small services: a request matcher (decides "is this a frujax request?"), a kernel listener (injects state / adjusts the response), and a Twig extension (a helper like frujax_url() or a {% if frujax %} test).X-Frujax: 1, sometimes a _frujax route default. That marker is the contract your JavaScript must honor — nothing works unless the client sends it.fetch() the same URL with the marker and swap the returned fragment into the DOM. One controller, one action, two renderings.If you want this in Laravel today, composer require ruwork/frujax-bundle is the wrong move — copy the idea: a middleware reading a custom header plus a Blade layout that yields either @yield('content') or just the section.
Opt-in per action, not global. Don't make every request frujax-aware. Mark the handful of endpoints (form submit, "load more", modal fetch) explicitly — via route defaults or a controller annotation — so the matcher stays cheap and predictable. A global listener that inspects every response is the classic source of "why did my redirect break" bugs.
Split by template, not by branching logic. The idiomatic pattern is: controller always renders the same logical template name; the bundle's Twig loader/listener appends a suffix (e.g. page.frujax.html.twig) when the marker is present, falling back to page.html.twig. This keeps fragments next to their full pages and avoids if ($request->isXmlHttpRequest()) { return $this->render(...partial...) } littering controllers.
Progressive enhancement for forms. The flow that pays off daily:
submit, sends the same payload with the frujax header.frujax-redirect instruction the JS honors.Redirect handling. POST/redirect/GET is the main friction point. Either the bundle's response listener converts a 302 into a JSON envelope ({"redirect": "..."}) or you special-case it. Decide early and standardize, because every form will hit this.
Compose with Symfony Form and Twig. The Twig extension is where day-to-day value lives: {{ frujax_url(path('order_submit')) }} builds the same URL with the marker, so templates never hardcode query strings. Add a global frujax boolean to the Twig context from the listener and gate chrome (nav, footer) on it in the base layout.
Laravel port sketch. Middleware sets view()->share('frujax', $request->header('X-Frujax') === '1'); a layout chooses full vs bare; a response macro converts redirects to JSON. Same three concerns, no bundle.
Symfony\Contracts changes will bite. Read it as reference source, not a dependency.AppKernel, no service tags, no bundle registration. Budget time to port, and don't expect the Twig loader tricks to map 1:1 onto Blade.Vary. If a shared cache or CDN sits in front, the same URL returns different bodies for frujax vs normal requests. You must emit Vary: X-Frujax (or whatever the marker is), or users get fragments as full pages. This is the single most common production failure with this pattern.X-* headers. Detect the marker leniently (header OR query param) during rollout, then tighten.X-Requested-With is not the same marker. Many bundles piggyback on X-Requested-With: XMLHttpRequest, which jQuery sets automatically but fetch() does not. If you mix axios/fetch/jQuery, be explicit about which header you send or half your requests will render full pages.curl -H 'X-Frujax: 1' and diff against a plain curl. If identical, the matcher isn't firing — check listener priority and that the bundle is registered above your app's response listeners.Accept: text/fragment negotiation) and the response listener (add your redirect-envelope logic there rather than in every controller). Those two are the seams worth customizing; everything else is thin glue.How can I help you explore Laravel packages today?