strontium/pjax-bundle
PJAX support for Symfony applications. This bundle adds middleware and Twig helpers to load page fragments via AJAX, update the URL and browser history, and enable faster navigation without a full SPA. Includes configurable selectors, request headers, and progressive enhancement for non-JS users.
First, the honest framing: this is a Symfony 2 bundle, not a Laravel package. There is no service provider, no facade, no config/pjax.php. Composer will happily install it, but StrontiumPjaxBundle extends Symfony\Component\HttpKernel\Bundle\Bundle and expects AppKernel registration, so it will do nothing inside a Laravel app. What you are actually evaluating is defunkt/jquery-pjax — roughly 400 lines of jQuery — plus a thin Symfony wrapper. Treat the bundle as documentation of a contract, not as a dependency.
Read in this order:
Resources/views/ and DependencyInjection/ directories — that's where any Twig helpers or request-listener logic lives, and it's a small surface area.composer.json — you'll find Symfony 2.x and PHP 5.x constraints. That tells you the maintenance status faster than the README does.If you are actually on Symfony 2 (legacy app, which is the only place this belongs):
// app/AppKernel.php
new Strontium\PjaxBundle\StrontiumPjaxBundle(),
then publish the bundle assets and include the jQuery plugin in your layout after jQuery.
If you are on Laravel (the realistic case), skip the bundle and reproduce the contract in ~30 lines:
npm install jquery-pjax
{{-- layouts/app.blade.php --}}
<div id="pjax-container">
@yield('content')
</div>
<script src="/js/jquery.js"></script>
<script src="/js/jquery.pjax.js"></script>
<script>
$(document).pjax('a[data-pjax]', '#pjax-container', { timeout: 2000 });
</script>
// app/Http/Middleware/DetectPjax.php
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->header('X-PJAX')) {
$response->headers->set('X-PJAX-Version', config('app.asset_version'));
}
return $response;
}
First use case: a content site where the header/footer/sidebar are expensive to re-render and the user mostly clicks article links. Wrap the article body in #pjax-container, opt links in with data-pjax, and return only that fragment when X-PJAX is present. If your app is a stateful dashboard, stop here — this is the wrong tool.
Full-page vs. fragment rendering. The server must return the whole document for a cold load and only the container's contents for a pjax load. In Blade, that's a single conditional at the top of the controller or a response macro:
$view = $request->header('X-PJAX')
? view('articles._body', compact('article'))
: view('articles.show', compact('article'));
return response($view);
Keep the fragment view and the full view sharing the same partial, or the two will drift and you'll get layout flashing on pjax navigation.
Middleware as the single detection point. Don't scatter header('X-PJAX') checks through controllers. Set a request attribute once and let views/controllers ask a question:
$request->attributes->set('pjax', $request->header('X-PJAX') !== null);
// Blade: @if(request()->attributes->get('pjax'))
A Request::macro('pjax', ...) is cleaner still and mirrors what the Symfony bundle's listener does.
Version header for cache busting. jquery-pjax sends and honors X-PJAX-Version. If the version in the response differs from the client's, pjax forces a full reload. Wire it to a build hash so deploys invalidate stale fragments automatically:
$response->headers->set('X-PJAX-Version', mix('js/app.js')->versionId ?? '1');
Re-initializing JavaScript. The classic pattern is to bind everything inside a pjax:end handler, or better, use event delegation from document for anything click-driven. Anything that must run per-page (date pickers, syntax highlighters, charts) goes in:
$(document).on('pjax:end', function () {
hljs.highlightAll();
initCharts();
});
Forms and non-GET. pjax only intercepts plain GET links. For forms, hook submit manually and use $.pjax({ url, type: 'POST', data, container }), and make sure your CSRF token is in the payload — the swapped fragment will not carry a fresh token.
Selective opt-in. Use a[data-pjax] rather than a as the selector. Blanket interception quietly breaks logout links, external links, download links, and anything with target="_blank".
Progressive enhancement discipline. Every pjax-enabled URL must also work as a plain HTTP request returning a full page. If it doesn't, you've built a JS-only app with extra steps.
#pjax-container is missing from the fragment, jquery-pjax silently falls back to a full page load and you'll spend an hour wondering why nothing is fast..html() strips <script> tags' execution semantics. Move all JS to pjax:end handlers, or use the $.pjax.defaults fragment-parsing path deliberately.<title> tag in the fragment; scroll position does not, unless you set scrollTo. Back-button restoration needs a pjax:popstate handler.pjax:end fires on every navigation; if your analytics snippet runs on page load and you also push a virtual pageview, you'll count twice. Use pjax:end as the only pageview trigger, or exclude the initial load.X-PJAX-Version or Cache-Control: no-cache on pjax responses until you're confident the fragments are versioned.X-PJAX: true; confirm the response is the fragment, not the layout; confirm the fragment contains the container selector; check the console for pjax:error and pjax:timeout events. Those two events are where 90% of failures surface.How can I help you explore Laravel packages today?