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

Html Min Laravel Package

voku/html-min

HtmlMin is a fast, easy-to-use PHP library that minifies HTML5 by stripping extra whitespace, comments, and unneeded characters without breaking structure. It also sorts attributes and CSS class names for better gzip compression, making pages smaller and faster to load.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and sanity-check in under five minutes.

composer require voku/html-min

This pulls in voku/simple_html_dom as a transitive dependency (the DOM-ish parsing layer). There is no Laravel service provider, facade, or config file — it is a plain PHP class you instantiate. Do not wait for auto-discovery; nothing will be registered.

The first thing to read is src/HtmlMin.php, not the README. The README shows the concept; the class file is the authoritative list of do*() optimization flags and the exact defaults baked into minify(). Second stop: the tests/ directory — it is effectively the spec for edge cases (whitespace handling, <pre>, inline scripts), which matters more here than for a typical package because the parser is regex-assisted.

A first use case in a Laravel app:

use voku\helper\HtmlMin;

$html = view('welcome')->render();

$minified = (new HtmlMin())
    ->doRemoveComments()
    ->doSumUpWhitespace()
    ->doRemoveWhitespaceAroundTags()
    ->minify($html);

That is the whole API surface in spirit: build a configured instance, chain flags, call minify(string): string. Flags mutate the instance and return $this, so an instance is stateful — build it once in a container binding rather than per call if you minify many fragments.

Then wire it where Laravel actually benefits: a response middleware, gated to production. See the next section for the middleware; register it in bootstrap/app.php (Laravel 11+) via $middleware->append(...) or in app/Http/Kernel.php $middleware for older apps.

Do not start with a Blade directive. The correct seam is the HTTP response (or the cache layer), because Blade compiles once but renders per request.

Implementation Patterns

Pattern 1 — Response middleware (the default choice).

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use voku\helper\HtmlMin;

class MinifyHtml
{
    public function __construct(private HtmlMin $minifier) {}

    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        if (! $this->shouldMinify($request, $response)) {
            return $response;
        }

        $html = $response->getContent();

        if (! is_string($html) || $html === '') {
            return $response;
        }

        $response->setContent($this->minifier->minify($html));
        $response->headers->remove('Content-Length'); // body changed

        return $response;
    }

    private function shouldMinify(Request $request, Response $response): bool
    {
        if (! config('html-min.enabled')) {
            return false;
        }

        if (! $request->isMethod('GET') || $response->getStatusCode() !== 200) {
            return false;
        }

        if ($response instanceof \Symfony\Component\HttpFoundation\StreamedResponse
            || $response instanceof \Symfony\Component\HttpFoundation\BinaryFileResponse) {
            return false;
        }

        if (! str_contains($response->headers->get('Content-Type', ''), 'text/html')) {
            return false;
        }

        if ($response->headers->has('Content-Encoding')) {
            return false;
        }

        return ! $request->is('telescope*', 'horizon*', '_debugbar*', 'livewire/*');
    }
}

Bind the minifier once so the flag configuration lives in one place:

// AppServiceProvider::register()
$this->app->singleton(HtmlMin::class, function () {
    return (new HtmlMin())
        ->doRemoveComments()
        ->doSumUpWhitespace()
        ->doRemoveWhitespaceAroundTags()
        ->doRemoveSpacesBetweenTags()
        ->doRemoveDefaultAttributes();
});

Everything else is a variation on this. The do*() chain is your policy object — treat it like a config array, one method per risk you've decided to accept.

Pattern 2 — Full-page cache / pre-render. If you use a response cache (spatie/laravel-responsecache or your own Cache::remember around the rendered body), minify before writing to the cache, not on every read. Minification is regex-heavy and, on large pages, is not free. Minifying once per cache warm is the only placement that makes sense.

Pattern 3 — Stored content cleanup. For CMS/settings HTML persisted in the DB, minify on write, not on read:

$model->body_html = app(HtmlMin::class)->minify($request->input('body_html'));

Caveat: this is lossy. If editors round-trip that HTML back into a WYSIWYG, they will see one long line. Keep the authoring source separate from the rendered column, or accept the pain.

Pattern 4 — Mailables / HTML email. Minifying mailable views genuinely helps: Gmail clips at ~102 KB, and inline-style-heavy marketing HTML is bloated. Apply it in a Mailable base class or a Mail::alwaysFrom-adjacent hook. Be conservative here — do not enable doRemoveSpacesBetweenTags() for email, because Outlook/Word rendering is whitespace-sensitive between inline elements.

Pattern 5 — Artisan backfill. For a big table, chunk and minify, keeping an idempotency marker column so you can re-run:

Post::whereNull('minified_at')->chunkById(200, function ($posts) {
    foreach ($posts as $post) {
        $post->forceFill([
            'body_html' => app(HtmlMin::class)->minify($post->body_html),
            'minified_at' => now(),
        ])->saveQuietly();
    }
});

Integration tips. Wrap it behind your own HtmlMinifier interface from day one. This package has a single maintainer, ~180 stars, and no downstream dependents — the blast radius of an abandoned release is entirely yours. A one-method interface costs nothing and lets you swap to voku/html-min → symfony/html-compressor or a Node-based step later. Test the middleware with an HTTP test against a fixture page and assert on a snapshot of output, not on assertSee('Exact String').

Gotchas and Tips

HTML comments are hydration markers. Inertia + Vue SSR emits <!--[-->, <!--]-->, and <!----> as anchors; Livewire does similar. doRemoveComments() will break client-side hydration in ways that look like unrelated JavaScript bugs. If you use Inertia SSR or Livewire, disable comment removal, or scope the middleware to exclude those routes.

Whitespace between tags is rendered whitespace. </a> <a> renders a gap; removing it visually joins the links. So does </span> <strong>. doRemoveSpacesBetweenTags() is the single most common cause of "the design shifted after deploy." Audit nav bars, breadcrumbs, and inline-styled text before enabling it. If you enable it, do it late and diff real pages.

doRemoveOmittedQuotes() and modern attribute syntax. Alpine's x-data="{ open: false }", Vue's :class="[...]", and Livewire's wire:model values are fine in most cases, but unquoted attributes containing > or / can terminate the tag early. Test every framework directive on a page. The same applies to doSortHtmlAttributes() — harmless in the browser, fatal for tests or CSP code that reads attribute order, and confusing in git diffs.

doRemoveOmittedHtmlTags() removes <html>, <head>, <body> when omitted. Browser-spec-legal, but it breaks server-side libraries that scrape your own output, and some CDNs/WAFs that inspect <body>. It also trips up DOM-based test assertions. Skip it unless you're optimizing aggressively.

Guard the response type checks. $response->getContent() returns false for StreamedResponse and BinaryFileResponse; str_replace on false in older code paths silently yields '' and you'll ship a blank download. Check instanceof before touching content, as shown above.

Remove Content-Length when you rewrite the body. Symfony may set it; if you minify a 40 KB page to 32 KB but leave the old header, an upstream proxy or the client can truncate the response. Removing it lets the SAPI/PHP recompute it.

Never minify behind Content-Encoding. If a middleware or server has already gzipped, you'd be regexing binary. Check the header, and keep this middleware ahead of any server-side compression layer.

Don't minify in development. The output is unreadable, breaks line-number-based debugging in browser devtools, and the _debugbar injection often fights the minifier. Gate on config('app.env') === 'production' or an explicit env flag — and make the flag default to false so a fresh clone isn't mysteriously broken.

Debug with a fixture and a diff, not the browser.

$before = file_get_contents(resource_path('fixtures/page.html'));
$after  = app(\voku\helper\HtmlMin::class)->minify($before);
file_put_contents(storage_path('app/minified.html'), $after);

Then diff -u (or open both in a browser tab) and flip flags one at a time. Bisecting the do*() chain is the fastest way to find which optimization broke layout.

Extension points are thin but real. Subclass HtmlMin and override minify() if you need to pre/post-process (e.g., wrap <pre> blocks in placeholders, run the minifier, restore them). Subclassing is the intended escape hatch; there's no middleware pipeline or event system inside the package. Note that older v2 tutorials use the static HtmlMin::minify($html) — v3 is instance-based, so copied snippets from blog posts will fail or silently misbehave.

Pin the version and watch PHP compatibility. Single-maintainer package, no dependents, and pre-8.x PHP deprecations have historically produced noisy E_DEPRECATED output that breaks phpunit with convertDeprecationsToExceptions. Pin a minor version in composer.json, and add a snapshot test so a composer update can't silently change your rendered output.

Measure before you commit to it. Minification typically saves 10–20% of the HTML body, which is 2–5% of a gzipped response. If you're already behind Brotli/gzip on a CDN, the win is small and the risk is real. Do it on the big public pages, skip it on authenticated app shells where the HTML is a few KB and heavily cached anyway.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
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