<?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: Jayesh Bansal</title>
    <description>The latest articles on DEV Community by Jayesh Bansal (@jayeshbansal).</description>
    <link>https://dev.to/jayeshbansal</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3979309%2F6e06f4b9-d0e7-4c36-8d8f-ac6a0916f674.jpeg</url>
      <title>DEV Community: Jayesh Bansal</title>
      <link>https://dev.to/jayeshbansal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jayeshbansal"/>
    <language>en</language>
    <item>
      <title>Why your AI writes HTML email that breaks in Outlook (and how I fixed it)</title>
      <dc:creator>Jayesh Bansal</dc:creator>
      <pubDate>Thu, 11 Jun 2026 10:38:16 +0000</pubDate>
      <link>https://dev.to/jayeshbansal/why-your-ai-writes-html-email-that-breaks-in-outlook-and-how-i-fixed-it-5447</link>
      <guid>https://dev.to/jayeshbansal/why-your-ai-writes-html-email-that-breaks-in-outlook-and-how-i-fixed-it-5447</guid>
      <description>&lt;p&gt;Ask any AI coding assistant for an email template and you'll get something beautiful: flexbox layout, &lt;code&gt;border-radius&lt;/code&gt;, &lt;code&gt;rgba()&lt;/code&gt; colors, a clean &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;. Paste it into your ESP, send a test, open it in Gmail on your phone — perfect.&lt;/p&gt;

&lt;p&gt;Then a customer opens it in Outlook on Windows and it looks like a ransom note.&lt;/p&gt;

&lt;p&gt;This isn't the AI being dumb. It's the AI being &lt;em&gt;too smart&lt;/em&gt; — applying everything it knows about modern web development to a medium where most of that knowledge is actively wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email clients are not browsers
&lt;/h2&gt;

&lt;p&gt;Here's the fact that breaks everyone's mental model: &lt;strong&gt;Outlook for Windows renders HTML email with Microsoft Word's engine.&lt;/strong&gt; Not a browser engine. Word. The same thing that renders your .docx files.&lt;/p&gt;

&lt;p&gt;That one fact cascades into a pile of constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No flexbox, no grid, no floats.&lt;/strong&gt; Layout has to be done with &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; elements, like it's 2005.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;max-width&lt;/code&gt;.&lt;/strong&gt; Your responsive container doesn't constrain anything in Outlook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;border-radius&lt;/code&gt;.&lt;/strong&gt; Your rounded button is a sharp rectangle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No background images&lt;/strong&gt; without special VML markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;padding&lt;/code&gt; on a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; is ignored&lt;/strong&gt; — only &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt; padding is reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rgba()&lt;/code&gt; and 3-digit hex fail.&lt;/strong&gt; Six-digit hex only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And Outlook isn't even the only landmine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gmail clips your email at 102KB&lt;/strong&gt; of HTML. Everything past the cut — including your unsubscribe link and tracking pixel — just vanishes behind a "[Message clipped]" link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gmail drops your entire &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block&lt;/strong&gt; if a single CSS rule has a syntax error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gmail and Outlook mobile force dark mode&lt;/strong&gt; by inverting your colors, ignoring your media queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images are blocked by default&lt;/strong&gt; in Outlook — so your email has to communicate as styled text first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is in the training data's "best practices for HTML/CSS," because none of it is true for browsers. So the AI confidently generates browser-correct code that fails in the one place corporate email actually gets read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patterns that actually work
&lt;/h2&gt;

&lt;p&gt;Email developers have spent fifteen years figuring out the workarounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bulletproof buttons
&lt;/h3&gt;

&lt;p&gt;A CSS button with &lt;code&gt;border-radius&lt;/code&gt; is square in Outlook. The fix is a hybrid: a normal anchor for modern clients, plus a VML &lt;code&gt;&amp;lt;v:roundrect&amp;gt;&lt;/code&gt; that &lt;em&gt;only&lt;/em&gt; Outlook sees, hidden from everyone else inside conditional comments. Rounded corners in Outlook, clean anchor everywhere else, and it works with images blocked because it's real text.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hybrid (spongy) layout
&lt;/h3&gt;

&lt;p&gt;You want fluid width on modern clients but Outlook needs a fixed pixel width. The trick is a "ghost table" wrapped in &lt;code&gt;&amp;lt;!--[if mso]&amp;gt;&lt;/code&gt; that only Outlook sees. Modern clients honor &lt;code&gt;max-width&lt;/code&gt; and go fluid; Outlook reads the fixed ghost table and renders correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dark mode survival
&lt;/h3&gt;

&lt;p&gt;You can't stop Gmail/Outlook mobile from force-inverting colors, but you can design palettes that survive it, and target Outlook.com's dark-mode rewrite with its proprietary &lt;code&gt;[data-ogsc]&lt;/code&gt; selectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  I packaged all of it into a Claude skill
&lt;/h2&gt;

&lt;p&gt;I kept re-explaining these rules to Claude on every email task, so I turned them into a Claude skill — a reusable instruction pack the agent loads automatically when it detects an email task.&lt;/p&gt;

&lt;p&gt;It's just a &lt;code&gt;SKILL.md&lt;/code&gt; with the non-negotiable rules plus data files: a CSS support matrix for 25+ clients grouped by rendering engine, copy-paste bulletproof patterns (the button, hybrid layout, dark-mode kit, preheader, compliance footer), a pre-send QA checklist, and production templates.&lt;/p&gt;

&lt;p&gt;Install is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/jayesh-bansal/email-pro-max.git ~/.claude/skills/email-pro-max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, when you ask Claude for an email, the first draft uses table layout, VML buttons, styled alt text, and dark-mode-safe colors — instead of browser-correct code that breaks where it matters.&lt;/p&gt;

&lt;p&gt;It's MIT, zero dependencies, just markdown. Repo here, with a before/after Outlook comparison in the README:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/jayesh-bansal/email-pro-max" rel="noopener noreferrer"&gt;https://github.com/jayesh-bansal/email-pro-max&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you do email development professionally, I'd genuinely love a second pair of eyes on the client-support matrix — open an issue if you catch something it gets wrong.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>html</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Generate Zod schemas from any JSON API — no OpenAPI spec required</title>
      <dc:creator>Jayesh Bansal</dc:creator>
      <pubDate>Thu, 11 Jun 2026 10:20:45 +0000</pubDate>
      <link>https://dev.to/jayeshbansal/show-hn-apitype-generate-zod-schemas-from-any-json-api-no-spec-required-576d</link>
      <guid>https://dev.to/jayeshbansal/show-hn-apitype-generate-zod-schemas-from-any-json-api-no-spec-required-576d</guid>
      <description>&lt;p&gt;I built this after the nth time I hand-typed a third-party API response into a Zod schema, squinting at the JSON trying to figure out which fields were nullable.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://orval.dev/" rel="noopener noreferrer"&gt;orval&lt;/a&gt; and &lt;a href="https://github.com/openapi-ts/openapi-typescript" rel="noopener noreferrer"&gt;openapi-typescript&lt;/a&gt; are great — &lt;em&gt;if&lt;/em&gt; the API has an OpenAPI spec. But most third-party APIs, internal services, and quick prototypes don't. So I built &lt;strong&gt;apitype&lt;/strong&gt;: point it at any URL or JSON and it infers the full schema from the actual data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @jayeshbansal/apitype https://api.github.com/users/octocat &lt;span class="nt"&gt;--name&lt;/span&gt; GithubUser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;githubUserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonnegative&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;public_repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonnegative&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;githubUserSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What makes it more than &lt;code&gt;JSON.parse&lt;/code&gt; + guesswork
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Format detection.&lt;/strong&gt; It recognizes 14 string formats from the values — UUID, email, URL, ISO datetime, IP, JWT, CUID, NanoID, base64, semver, hex color — and emits the matching Zod validator (&lt;code&gt;.uuid()&lt;/code&gt;, &lt;code&gt;.email()&lt;/code&gt;, &lt;code&gt;.datetime()&lt;/code&gt;) instead of a bare &lt;code&gt;z.string()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nullable/optional inference by sampling.&lt;/strong&gt; A single response can't tell you whether &lt;code&gt;bio&lt;/code&gt; is &lt;em&gt;always&lt;/em&gt; null or &lt;em&gt;sometimes&lt;/em&gt; a string. So apitype can fetch an endpoint N times and merge the samples: a key that's missing in some responses becomes &lt;code&gt;.optional()&lt;/code&gt;, a key that's null in some becomes &lt;code&gt;.nullable()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @jayeshbansal/apitype https://api.example.com/posts/random &lt;span class="nt"&gt;--samples&lt;/span&gt; 5 &lt;span class="nt"&gt;--name&lt;/span&gt; Post
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Four output formats.&lt;/strong&gt; Same engine, your choice of target:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;zod&lt;/code&gt; — Zod schema + inferred type&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;typebox&lt;/code&gt; — TypeBox with &lt;code&gt;Static&amp;lt;&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;typescript&lt;/code&gt; — plain &lt;code&gt;export type&lt;/code&gt;, zero runtime&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonschema&lt;/code&gt; — JSON Schema draft-07&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part I actually care about: it's an MCP server
&lt;/h2&gt;

&lt;p&gt;The same inference engine runs as a &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server, so AI coding assistants (Claude, Cursor, Windsurf) can call apitype as a tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"apitype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@jayeshbansal/apitype"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you ask your assistant to "type this API response," it doesn't hallucinate a schema — it calls a deterministic tool that fetched the real data and detected the real formats.&lt;/p&gt;

&lt;p&gt;There's also a Vite plugin (regenerate types on dev-server start), a batch config mode (keep a whole API's types in sync in CI), and a programmatic API (&lt;code&gt;import { fromUrl } from '@jayeshbansal/apitype'&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should &lt;em&gt;not&lt;/em&gt; use this
&lt;/h2&gt;

&lt;p&gt;Honesty matters: if your API ships an OpenAPI/Swagger spec, use a spec-based generator. The spec is the source of truth — inference is only as good as the sample responses you feed it. apitype is for the (very common) case where no spec exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @jayeshbansal/apitype &amp;lt;url|file&amp;gt;           &lt;span class="c"&gt;# one-off, no install&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @jayeshbansal/apitype           &lt;span class="c"&gt;# project dependency&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/jayesh-bansal/apitype" rel="noopener noreferrer"&gt;https://github.com/jayesh-bansal/apitype&lt;/a&gt;&lt;/strong&gt; · MIT, zero runtime deps.&lt;/p&gt;

&lt;p&gt;If you hit an API whose shape it gets wrong, open an issue with the response — those are the best test cases.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>zod</category>
      <category>api</category>
    </item>
  </channel>
</rss>
