<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: KAM Sie Philippe ANgelo</title>
    <description>The latest articles on DEV Community by KAM Sie Philippe ANgelo (@ouranos27).</description>
    <link>https://dev.to/ouranos27</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4031975%2Ffb5c7775-e9d1-4320-8fcc-b487bfef1559.jpg</url>
      <title>DEV Community: KAM Sie Philippe ANgelo</title>
      <link>https://dev.to/ouranos27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ouranos27"/>
    <language>en</language>
    <item>
      <title>Lint your React Email templates in CI (before Gmail strips half your CSS)</title>
      <dc:creator>KAM Sie Philippe ANgelo</dc:creator>
      <pubDate>Thu, 16 Jul 2026 11:10:02 +0000</pubDate>
      <link>https://dev.to/ouranos27/lint-your-react-email-templates-in-ci-before-gmail-strips-half-your-css-1d27</link>
      <guid>https://dev.to/ouranos27/lint-your-react-email-templates-in-ci-before-gmail-strips-half-your-css-1d27</guid>
      <description>&lt;p&gt;Your email looks perfect in the React Email preview. Then Gmail strips half the CSS, and Outlook on Windows renders it with Word's engine from 2007. Nobody notices until a customer replies with a screenshot.&lt;/p&gt;

&lt;p&gt;Frontend developers solved this problem years ago: we lint, we type-check, we run visual tests in CI. Email developers mostly still send a test to themselves and squint at it on their phone. This post shows how to close that gap for React Email projects: compile your templates in CI, lint the output against real email client support data, and fail the PR when someone introduces CSS that Outlook will eat.&lt;/p&gt;

&lt;p&gt;Full disclosure up front: I built the linter used below, &lt;a href="https://emailens.dev" rel="noopener noreferrer"&gt;Emailens&lt;/a&gt;. The engine is open source (MIT), the free tier covers this workflow, and the approach works even if you swap in your own checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why linting beats "send a test email"
&lt;/h2&gt;

&lt;p&gt;Render-testing services like Litmus screenshot your email in real clients. That's valuable, but it's slow, costs per test, and happens late. Linting is the complement: static analysis of your compiled HTML against a database of what each client actually supports (think caniemail.com, but as a build step). It runs in seconds, costs nothing per run, and catches the boring 80% of breakage: unsupported CSS properties, missing MSO conditionals, dark mode surprises, oversized HTML that Gmail clips at 102KB.&lt;/p&gt;

&lt;p&gt;The React Email abstraction makes this more important, not less. JSX components hide the compiled output from you. You never see the HTML that ships, which means you never see the &lt;code&gt;border-radius&lt;/code&gt; that silently does nothing in Outlook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: compile your templates to HTML
&lt;/h2&gt;

&lt;p&gt;React Email templates are React components, so first render them to static HTML. The react-email CLI does this in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx react-email &lt;span class="nb"&gt;export&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes plain HTML files to an &lt;code&gt;out/&lt;/code&gt; directory, one per template. That compiled output is what your users' inboxes receive, and it's what we lint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: lint the compiled output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @emailens/cli lint out/&lt;span class="k"&gt;*&lt;/span&gt;.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks like a familiar code linter, except the rules come from email client support data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;out/welcome.html
  error  outlook-windows     border-radius           Not supported in Outlook Windows
  warn   spam                caps-ratio              20%+ of words are ALL CAPS

out/newsletter.html
  pass   No issues found

2 files | 1 error | 1 warning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood it flattens several audits into one issue list: CSS compatibility across 15 clients, accessibility, link validation, image checks, spam signals, size limits, and leftover template variables (the &lt;code&gt;{{first_name}}&lt;/code&gt; that escaped into production, we've all been there).&lt;/p&gt;

&lt;p&gt;The exit codes are designed for CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; clean&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt; errors found&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt; warnings only, when you pass &lt;code&gt;--fail-on-warning&lt;/code&gt; or exceed &lt;code&gt;--max-warnings&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also lint JSX directly with &lt;code&gt;--format jsx&lt;/code&gt; if you'd rather skip the export step, but linting the compiled HTML is closer to what actually ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: fail the PR in GitHub Actions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email lint&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emails/**"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint-emails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compile React Email templates&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx react-email export&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint compiled emails&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @emailens/cli lint out/*.html --max-warnings &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. A PR that adds &lt;code&gt;position: sticky&lt;/code&gt; to an email now fails with a named client and a named property instead of a confused customer three weeks later.&lt;/p&gt;

&lt;p&gt;Two useful refinements once the basics work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope the noisy checks.&lt;/strong&gt; Early on, spam heuristics on a transactional receipt can be noise. Skip categories you don't care about yet: &lt;code&gt;--skip spam,inboxPreview&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get machine-readable output.&lt;/strong&gt; &lt;code&gt;--json&lt;/code&gt; gives you structured results if you want to post a summary comment on the PR or track score trends over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the issues a linter can't fix?
&lt;/h2&gt;

&lt;p&gt;Some Outlook problems aren't one-property fixes; they need structural changes like table-based layouts or VML fallbacks. For those there's an AI-assisted mode (&lt;code&gt;emailens fix email.html&lt;/code&gt;, bring your own Anthropic API key) that rewrites the structure and prints the diff. I treat it like a code-mod: useful suggestion, human reviews before merge.&lt;/p&gt;

&lt;p&gt;And linting doesn't replace screenshots entirely. My workflow is: lint on every PR (fast, free, catches most regressions), screenshot-test before big campaign sends (slow, thorough, catches the weird stuff). The linter's per-client compatibility score, 0 to 100 per client, is also a decent early-warning metric: when the Outlook score drops ten points in one PR, someone should look at that diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger point
&lt;/h2&gt;

&lt;p&gt;Email HTML is the last corner of frontend where "works on my machine" is still the default QA strategy. It doesn't have to be. Your email templates are code; treat them like code. Compile them, lint them, gate merges on the result, and save the manual eyeballing for the things machines can't judge, like whether your copy is any good.&lt;/p&gt;

&lt;p&gt;Links, for the curious: &lt;a href="https://emailens.dev" rel="noopener noreferrer"&gt;Emailens&lt;/a&gt; (free tier: 30 previews/day), the &lt;a href="https://github.com/emailens/engine" rel="noopener noreferrer"&gt;open-source engine&lt;/a&gt;, the &lt;a href="https://www.npmjs.com/package/@emailens/cli" rel="noopener noreferrer"&gt;CLI on npm&lt;/a&gt;, and an &lt;a href="https://github.com/emailens/mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; if you want Claude or Cursor to run these checks for you.&lt;/p&gt;

&lt;p&gt;Questions or war stories about emails breaking in production? I'd love to hear them in the comments.&lt;/p&gt;

</description>
      <category>react</category>
      <category>cicd</category>
      <category>email</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
