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

Inky Laravel Package

hampe/inky

Laravel package for Inky, Zurb Foundation's email templating language. Build responsive HTML emails with Inky components and compile them into email-client-friendly markup, making transactional and marketing emails easier to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

hampe/inky is a framework-agnostic port of ZURB's Inky parser: you feed it Foundation-for-Emails markup (<container>, <row>, <columns>, <button>, <callout>, <spacer>) and it returns the table-based HTML that actually renders in Outlook and Gmail. There is no service provider, no config file, and no facade — it's one class you instantiate. That's the whole integration surface.

composer require hampe/inky:^1.3.6.2

Pin to 1.3.6.2 or later specifically. The 1.3.6.x line has been carrying real regressions: 1.3.6.0 broke nested <spacer> elements, and style-tag stripping was a long-standing bug (#7) only closed in this release. A bare composer require hampe/inky on a stale lockfile can land you on either.

Before you write a line of code, open vendor/hampe/inky/ and confirm four things:

  1. The entry point — src/Inky.php, almost certainly exposing releaseTheKraken(string $html): string (the method name is inherited from the Node original).
  2. The require block in composer.json — this release bumps the underlying parser to PHPHtmlParser 1.7.0. That's the real engine, and it's what determines how your markup survives serialization. Check the constraint resolves cleanly on your PHP version; older minors frequently need --ignore-platform-reqs or a constraint bump in a fork.
  3. tests/ fixtures — the expected-output HTML files are the most reliable documentation of what this version of Inky actually supports. Read them instead of the ZURB docs, because the ZURB docs describe a newer Inky than this package implements.
  4. That <style> blocks survive — compile a snippet with a <style> element inside it and confirm it comes out the other side. This was broken for most of the package's life and is only fixed as of 1.3.6.2.

First use case — compile a template once, eyeball the output:

use Hampe\Inky\Inky;

$html = (new Inky())->releaseTheKraken(
    view('emails.welcome', ['user' => $user])->render()
);

file_put_contents('/tmp/welcome.html', $html);

If you see <table class="row"> and <th class="columns"> where your <row> and <columns> were, it works. If the custom tags survive untouched, the parser didn't match them and you're on the wrong syntax generation. If your <style> block is missing, you're not actually on 1.3.6.2 — recheck the lockfile.

Implementation Patterns

Bind it as a singleton. The parser is stateless but the object is reusable, and binding it gives you a seam for tests and for swapping in a subclass later.

// AppServiceProvider::register()
$this->app->singleton(\Hampe\Inky\Inky::class, fn () => new \Hampe\Inky\Inky());

The @inky Blade directive. This is the pattern most teams land on, because it keeps templates as ordinary .blade.php files that designers can open in a browser and that your existing view() cache handles.

// AppServiceProvider::boot()
Blade::directive('inky', fn () => '<?php ob_start(); ?>');
Blade::directive('endinky', fn () => '<?php echo app(\Hampe\Inky\Inky::class)->releaseTheKraken(ob_get_clean()); ?>');
{{-- resources/views/emails/welcome.blade.php --}}
@inky
<style>
  /* foundation-emails.css contents */
</style>
<container>
  <row>
    <columns small="12" large="6">Hello {{ $user->name }}</columns>
    <columns small="12" large="6"><button href="{{ $url }}">Confirm</button></columns>
  </row>
  <spacer size="16"></spacer>
  <row>
    <columns><spacer size="8"></spacer></columns>
  </row>
</container>
@endinky

Since 1.3.6.2 the <style> block can live inside the @inky region — previously it was stripped by the parser and had to be concatenated afterwards. Keeping everything in one place is simpler, but the nesting test above is worth running once: nested <spacer> elements are the exact thing that broke in 1.3.6.0.

A base Mailable. If you'd rather not touch Blade internals, make compilation part of the mail pipeline so every Mailable gets it for free:

abstract class InkyMailable extends Mailable
{
    public function build()
    {
        $view = $this->view ?? 'emails.' . Str::kebab(class_basename($this));

        $compiled = app(Inky::class)->releaseTheKraken(
            view($view, $this->buildViewData())->render()
        );

        return $this->html($compiled)->subject($this->subject ?? '');
    }
}

Note the ordering constraint: compile first, then inline CSS. Laravel's CssToInlineStyles (used by Markdown mailables) operates on the final HTML, and the selectors Foundation ships — .row, .columns, .small-6 — only exist after Inky has emitted them. If you inline before compiling you get unstyled tables.

Caching. releaseTheKraken() re-parses the DOM every call. For a transactional email that's irrelevant. For a 50k-recipient campaign it's worth memoizing per view+mtime:

$key = 'inky:' . $view . ':' . filemtime($path);
return Cache::remember($key, now()->addDay(), fn () => $inky->releaseTheKraken(...));

Be careful to key on the compiled Blade output, not just the source file — if your template depends on request data, skip the cache entirely. Add the package version to the key so a composer update invalidates stale compiled markup:

$key = 'inky:v1.3.6.2:' . $view . ':' . filemtime($path);

Preview route. A signed, auth-gated /emails/{name}/preview route that dumps both the raw Inky source and the compiled output side by side pays for itself the first time a stakeholder asks why the button is off-centre in Outlook.

Testing. Assert on structure, not on exact strings — the serializer normalizes attributes and whitespace between versions:

$out = (new Inky())->releaseTheKraken('<container><row><columns>Hi</columns></row></container>');

$this->assertStringContainsString('<table', $out);
$this->assertStringNotContainsString('<columns', $out); // tags were consumed

Add two regression tests that mirror the release notes, so a future downgrade or lockfile accident fails loudly:

// style blocks survive (fixed in 1.3.6.2)
$this->assertStringContainsString('<style>', (new Inky())->releaseTheKraken('<style>.a{color:red}</style><container></container>'));

// nested spacers survive (broken in 1.3.6.0, fixed in 1.3.6.2)
$nested = '<container><spacer size="16"></spacer><row><columns><spacer size="8"></spacer></columns></row></container>';
$out = (new Inky())->releaseTheKraken($nested);
$this->assertSame(2, substr_count($out, 'spacer'));

Gotchas and Tips

Pin the patch version, and care about it. 1.3.6.x has shipped meaningful behaviour changes in patch releases: 1.3.6.0 broke nested spacers, 1.3.6.2 fixed them and fixed style-tag stripping. Constrain to ^1.3.6.2 and treat any further patch as something to smoke-test rather than assume safe.

Style tags: fixed, but only recently. Before 1.3.6.2 the parser stripped <style> elements out of your markup silently. If you inherited a template that concatenates a stylesheet after compilation, that workaround is now redundant but harmless — and if you're on a mixed fleet of app versions, keep the concatenation until everything is on ^1.3.6.2. This bug is also the answer to a large share of "the inliner did nothing" reports.

You are still on Inky 1.x. Patch-level maintenance of a 1.x line is not the same as feature parity with ZURB's current Inky. <spacer> and nesting thereof are supported — the 1.3.6.2 notes prove that much — but components from Inky 2.x (<wrapper>, <menu>, <center>, newer <button> variants) will pass through as unknown tags and break your layout silently. Verify every component against the package's own test fixtures before designing with it, and plan to hand-write the ghost-table equivalents for anything missing.

Structure only, never styles. Inky emits the table scaffolding; it does not ship or inject Foundation's CSS. You still need the foundation-emails stylesheet in a <style> block (for clients that support it) and an inliner for the rest. Half of "Inky doesn't work" reports are really "no CSS was ever applied" — the other half were the style-stripping bug above.

The parser is PHPHtmlParser, now on 1.7.0. It wraps DOMDocument, which brings its own encoding assumptions. Non-ASCII content — accented names, emoji in subject-adjacent copy, currency symbols — can come out as é. Test with real internationalized data early, and if you see mojibake the usual workaround is prepending <?xml encoding="UTF-8"> or running input through mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') before compiling. The 1.7.0 bump may change how this manifests versus older versions, so re-verify rather than trusting a note from a previous project.

Unknown attributes get dropped. The DOM round-trip re-serializes elements, so custom data-* attributes on your custom tags and hand-written conditional comments may not survive exactly as written. If you rely on Outlook <!--[if mso]> blocks, confirm they come out the other side intact — and if they don't, keep them outside the @inky block. Given that serialization just changed under you, re-run this check on upgrade even if it passed before.

It is not a sanitizer. releaseTheKraken() will happily parse whatever you give it. Never run it over user-supplied HTML; only over templates you control. This is a build-time transform, not a security boundary.

Debugging is a diff, not a stack trace. Nothing throws on malformed Inky — you just get your tag back verbatim. When a component isn't compiling, write the smallest possible snippet to artisan tinker, compile it, and compare against the fixtures. Element dispatch lives in src/Element/, so that's where to look to confirm a tag name is actually registered.

Extension point: subclass and register. Adding a missing element is feasible — extend Inky, register a custom element class modelled on an existing one in src/Element/ (and look at the spacer implementation for nesting behaviour), and rebind your subclass in the container. Given there's no 2.x on the horizon, this is realistically how you close feature gaps.

Seriously consider the alternative. If your build already has Node, running the current inky CLI as a Mix/CI step gets you Inky 2.x, active maintenance, and no PHP-version friction — at the cost of a build dependency. hampe/inky is the right call only when you need runtime compilation driven by dynamic PHP data. Audit that assumption before you commit, and if you do stay, budget for the occasional patch-release regression.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package