lenorix / laravel-early-hints
Tell to client what will need later
Fund package maintenance!
Requires
- php: ^8.4
- illuminate/contracts: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0||^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
Lets the client know as early as possible what resources it will need, so it can start loading them sooner. This package provides a facade to add Link HTTP headers (e.g. preconnect) to a response.
Support us
We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.
Installation
You can install the package via composer:
composer require lenorix/laravel-early-hints
Usage
Use the EarlyHints facade to add a Link header to a response.
addLinkHeader
Adds a single URI to the Link header.
use Lenorix\LaravelEarlyHints\Facades\EarlyHints; EarlyHints::addLinkHeader($response, 'https://one.example.com'); EarlyHints::addLinkHeader($response, 'https://fonts.example.com/font.woff2', rel: 'preload', as: 'font', crossorigin: 'anonymous', type: 'font/woff2', fetchpriority: 'high');
addLinkHeaders
Same as addLinkHeader, but accepts an array of URIs.
use Lenorix\LaravelEarlyHints\Facades\EarlyHints; EarlyHints::addLinkHeaders($response, [ 'https://one.example.com', 'https://two.example.com', ]);
Both methods default rel to preconnect, and also accept crossorigin, imagesrcset, imagesizes, media and type, all omitted from the header unless provided. Any existing Link header on the response is preserved and the new entries are appended to it.
Streamed responses
The automatic middleware skips StreamedResponse entirely, since it can't inspect a body that doesn't exist yet as a string. If you know in advance what a streamed response will reference (e.g. the images inside a JSON body sent via response()->streamJson(...)), call the facade yourself before returning it:
use Lenorix\LaravelEarlyHints\Facades\EarlyHints; use Symfony\Component\HttpFoundation\StreamedResponse; public function show() { $response = response()->streamJson([ 'title' => 'A blog post', 'cover_image' => 'https://cdn.example.com/cover.jpg', 'gallery' => [ 'https://cdn.example.com/img1.jpg', 'https://cdn.example.com/img2.jpg', ], ]); EarlyHints::addLinkHeaders($response, [ 'https://cdn.example.com/cover.jpg', 'https://cdn.example.com/img1.jpg', 'https://cdn.example.com/img2.jpg', ], rel: 'preload', as: 'image'); return $response; }
This works because headers on a StreamedResponse (a subclass of Symfony's Response) are only sent once, right before the streaming callback runs. As long as addLinkHeaders is called before the response is returned, the Link header goes out with the rest of the headers in that first flush — adding it from inside the streaming callback itself would be too late, since headers have already been sent by then.
Automatic mode
The early-hints middleware inspects the response and adds the Link header for you. It is opt-in, so add it wherever you want it to run:
Route::get('/dashboard', DashboardController::class)->middleware('early-hints');
It detects, on HTML responses:
- stylesheets (
<link rel="stylesheet">) asrel="preload"; as="style", carrying overcrossorigin,mediaandfetchpriorityfrom the tag when present - scripts (
<script src>) asrel="preload"; as="script"(orrel="modulepreload"fortype="module"), carrying overcrossoriginandfetchpriority; anomodulescript is skipped - images (
<img>/<source>tags and CSSurl(...)in astyleattribute or<style>block), by extension (bmp,gif,jpg,jpeg,png,svg,tiff,webp,avif) asrel="preload"; as="image", carrying overfetchpriority; a responsive<img srcset>becomes a single link withimagesrcset/imagesizesmirroring the tag; an image markedloading="lazy"is skipped - fonts declared in an inline
<style>block's@font-facerule, asrel="preload"; as="font"; crossorigin="anonymous"with atypematching the extension (woff2,woff,ttf,otf); fonts referenced from an external stylesheet cannot be detected, since linked CSS is never fetched
And on JSON responses:
- images, the same way as in HTML (but matched anywhere in the body, since JSON has no tags to inspect)
- pagination links, from the
next_page_url,next(asrel="next") andprev_page_url,previous,prev(asrel="prev") root properties, when they are notnull
A resource on a different origin than the request gets rel="preconnect" to that origin instead of a full preload — cheaper, still helpful, and several resources from the same third party collapse into a single preconnect.
URIs are detected in every form: full URLs, protocol relative URLs, absolute paths and relative paths, with or without a signed query string. Streamed responses, downloads and redirects are left untouched.
Auto-detected images are capped, in document order, to images_limit (20 by default); an image with fetchpriority="high" always counts regardless of the cap.
Detected links are added from the shortest URI to the longest, and stop before the Link header grows past max_header_bytes (4096 bytes by default), which is where web servers usually start rejecting headers. This budget covers the whole wire line, so it also applies when a link is added manually through the EarlyHints facade: a link that would push the header past the limit is silently skipped instead of being emitted.
Each detection can be turned off in the published config file:
php artisan vendor:publish --tag="laravel-early-hints-config"
return [ 'styles_and_scripts' => true, 'images' => true, 'json_pagination' => true, 'images_limit' => 20, 'fonts' => true, 'max_header_bytes' => 4096, ];
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The Unlicense. Please see License File for more information.