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

Emogrifier Laravel Package

pelago/emogrifier

Emogrifier converts CSS styles into inline style attributes in your HTML, ensuring reliable display in email clients and mobile readers that lack stylesheet support. Useful for Outlook 2007 and Gmail quirks. Install via Composer: pelago/emogrifier.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation (v8.3.0 or later):

    composer require pelago/emogrifier:^8.3
    
  2. Check your platform:

    • PHP 8.6 is now supported; PHP 7.3 is deprecated as of 8.3.0 — target PHP 7.4+ and plan the upgrade path.
    • v8.3.0 requires sabberworm/php-css-parser >= 9.5.0. If any other package pins an older version, run composer why sabberworm/php-css-parser before upgrading.
    • thecodingmachine/safe is no longer a runtime dependency — if your code (or another package) relied on it being installed transitively, add it explicitly.
  3. Basic inlining:

    use Pelago\Emogrifier\CssInliner;
    
    $html = '<html><body><p class="email-text">Hello</p></body></html>';
    $css  = '.email-text { color: red; }';
    
    $inlinedHtml = CssInliner::fromHtml($html)->inlineCss($css)->render();
    // <p style="color: red;">Hello</p>
    
  4. No external CSS? Omit the argument to parse <style> blocks in the document:

    $inlinedHtml = CssInliner::fromHtml($html)->inlineCss()->render();
    
  5. First use case: transactional email templates — convert a styled Blade/HTML template into inline CSS so Outlook and Gmail render it consistently.


Implementation Patterns

Core Workflow

  1. Preprocess / normalize (adds missing <head>/<body>, fixes nesting):

    use Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer;
    
    $normalizedHtml = HtmlNormalizer::fromHtml($rawHtml)->render();
    
  2. Inline — pass external CSS as a string, or omit to consume <style> blocks. Chain options before inlineCss():

    $inliner = CssInliner::fromHtml($html)
        ->setDebug(false)                      // see Gotchas: exception vs. trigger_error
        ->addExcludedCssSelector('.ad-banner')
        ->addAllowedMediaType('handheld')
        ->inlineCss($css);
    
  3. Post-process — prune hidden nodes and redundant classes, then convert visual CSS to HTML attributes:

    use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
    use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
    
    $pruned = HtmlPruner::fromDomDocument($inliner->getDomDocument())
        ->removeElementsWithDisplayNone()
        ->removeRedundantClassesAfterCssInlined($inliner)
        ->render();
    
    $visual = CssToAttributeConverter::fromDomDocument($inliner->getDomDocument())
        ->convertCssToVisualAttributes()
        ->render();
    
  4. CSS variables — resolve var(--x) into concrete values:

    $evaluated = CssVariableEvaluator::fromDomDocument($dom)->evaluateVariables()->render();
    

Laravel Integration

Wrap it in your own service, not a singleton of CssInliner. CssInliner is constructed via the static fromHtml()/fromDomDocument() factories and carries per-document state — binding it as a singleton in a container is a footgun (shared DOM across requests, stale option flags):

// app/Services/EmailInliner.php
namespace App\Services;

use Pelago\Emogrifier\CssInliner;

class EmailInliner
{
    public function inline(string $html, ?string $css = null): string
    {
        $inliner = CssInliner::fromHtml($html)
            ->addExcludedCssSelector('.ad-banner')
            ->addAllowedMediaType('handheld');

        foreach (config('emogrifier.excluded_selectors', []) as $selector) {
            $inliner->addExcludedCssSelector($selector);
        }

        return $inliner->inlineCss($css)->render();
    }
}
// app/Providers/AppServiceProvider.php
$this->app->singleton(EmailInliner::class);

Use it from a Mailable, a Blade directive, or a queued job:

public function handle(EmailInliner $inliner): void
{
    $html = $inliner->inline($this->template, $this->styles);
    Mail::html($html, fn ($m) => $m->to($this->recipient)->subject($this->subject));
}

Testing

Use renderBodyContent() to assert on the body without HTML document noise:

$body = CssInliner::fromHtml($html)->inlineCss()->renderBodyContent();
$this->assertStringContainsString('style="color: red;"', $body);

Error Handling Pattern (new in 8.3.0)

render() now throws on internal DOMDocument errors instead of returning an empty string. That is a good change — the old behaviour could silently mail a blank body — but it means every render path needs a guard:

try {
    $html = $inliner->inline($template, $css);
} catch (\Throwable $e) {
    report($e);
    $html = $template; // ship unstyled rather than blank
}

Gotchas and Tips

Pitfalls

  1. render() no longer fails silently (8.3.0) — any code that treated an empty string as "something went wrong" is now dead code, and any unguarded render() call can throw mid-request or mid-job. Audit mail-sending paths before upgrading; a try/catch with a raw-HTML fallback is the minimum.

  2. Regex blowout behaviour changed (8.3.0) — on an unexpected PCRE backtrack limit:

    • Production: trigger_error() is called and the original string is returned unmodified (graceful degradation, restoring 8.1.0 behaviour).
    • With the debug option enabled: an exception is still thrown.
    • Practical implication: in production you get an un-inlined email and a PHP notice instead of a 500. Turn those notices into log entries (a set_error_handler or your handler config) so you actually learn about it, and set setDebug(true) in local/staging so tests fail loudly.
  3. PHP version drift: PHP 8.6 is supported, PHP 7.3 is deprecated. Don't upgrade composer on a 7.3 host without checking CI.

  4. Selector specificity: addExcludedCssSelector('.example') does not match p .example. Broaden with .example, * .example. Pseudo-classes (:hover, :active) are not inlined at all.

  5. Media queries: allowed types default to all, screen, print — add more with addAllowedMediaType(). Outlook ignores @media entirely, so exclude critical rules explicitly.

  6. HTML structure: HtmlNormalizer will insert <head>/<body> and can restructure malformed markup. Deeply nested HTML is fixed in 8.3.0, but still test your worst template.

  7. <style> blocks are removed after inlining by default — disableStyleBlocksParsing() if you need them preserved (e.g. for clients that do support them).

  8. CSS variables: must be defined on :root or an ancestor; unresolved var(--x) stays literal in the output.

Debugging Tips

  1. Dump intermediate DOM:

    file_put_contents('debug.html', $cssInliner->getDomDocument()->saveHTML());
    
  2. Test selectors in isolation with a two-line fixture before blaming the template.

  3. Catch the new exceptions by type and log the offending HTML — a DOMDocument internal error is almost always malformed markup or invalid UTF-8 (run mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') if needed).

  4. Batch memory: 8.3.0 clears all previous caches on each invocation, which fixes the memory build-up reported in earlier 8.x releases — long-running queue workers can now loop without the old ceiling. Still reuse a single normalizer per batch where practical, and restart workers on a schedule as hygiene.

Extension Points

  1. Custom processor — extend AbstractHtmlProcessor for extra steps (sanitization, tracking-pixel injection), returning $this for chaining.

  2. Config file — centralize options and feed them into your own service (see above):

    // config/emogrifier.php
    return [
        'allowed_media'      => ['all', 'screen', 'print', 'handheld'],
        'excluded_selectors' => ['.ad-banner', '.ad-banner *'],
    ];
    
  3. Post-inlining DOM hooks — getDomDocument() plus DOMXPath for rewrites that CSS can't express:

    foreach ((new DOMXPath($cssInliner->getDomDocument()))->query('//img') as $img) {
        $img->setAttribute('alt', 'Fallback');
    }
    
  4. Caching — cache inlined output for static templates:

    $html = Cache::remember('email:'.md5($template.$css), now()->addHour(), fn () => $inliner->inline($template, $css));
    

    Note the cache key must include the package version if you rely on output stability across upgrades.

Pro Tips

  1. Table layout + !important on critical declarations still wins over email-client defaults; Emogrifier does not strip !important from inline output.
  2. Always ship a fallback path — since bad markup now throws and PCRE blowouts now return raw HTML, a "send unstyled" branch beats a failed send or an empty email.
  3. Pin and test the upgrade: 8.2 → 8.3 touches render error semantics, a runtime dependency, and a transitive CSS parser floor. Run your email snapshot tests against 8.3.0 before rolling to production.
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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log