lenorix/laravel-early-hints

Tell to client what will need later

Maintainers

Package info

github.com/lenorix/laravel-early-hints

pkg:composer/lenorix/laravel-early-hints

Transparency log

Fund package maintenance!

lenorix

Statistics

Installs: 50

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.4.0 2026-08-26 08:55 UTC

This package is auto-updated.

Last update: 2026-08-26 08:56:25 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

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">) as rel="preload"; as="style", carrying over crossorigin, media and fetchpriority from the tag when present
  • scripts (<script src>) as rel="preload"; as="script" (or rel="modulepreload" for type="module"), carrying over crossorigin and fetchpriority; a nomodule script is skipped
  • images (<img>/<source> tags and CSS url(...) in a style attribute or <style> block), by extension (bmp, gif, jpg, jpeg, png, svg, tiff, webp, avif) as rel="preload"; as="image", carrying over fetchpriority; a responsive <img srcset> becomes a single link with imagesrcset/imagesizes mirroring the tag; an image marked loading="lazy" is skipped
  • fonts declared in an inline <style> block's @font-face rule, as rel="preload"; as="font"; crossorigin="anonymous" with a type matching 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 (as rel="next") and prev_page_url, previous, prev (as rel="prev") root properties, when they are not null

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.