给 Email 写样式真的很难

虽然 IE 退出了浏览器的历史舞台,但是 Windows 平台上的 Outlook 依然继承了 IE 的衣钵,继续在邮件客户端界发光发热。

This ranks email clients based on their support among the 298 HTML and CSS features listed on Can I email.

最好用内联样式

很多情况下,使用外部样式表并不靠谱,比如 GMail 以及 Outlook 就会在特定情况下移除邮件 HTML 中的 style 标签,一个更保守的做法是使用元素上的内联样式。对于简单的通知邮件来说,完全够用了。

如果嫌手搓样式很麻烦,可以先用 TailWind CSS 编写,然后让 ChatGPT 帮忙转换成内联样式。

避免使用 base64 内联图片

虽然 Base64 图片 URL 很简单易用,但是很多邮件客户端会把它们当作外部图片一样默认屏蔽。

一个对展示效果更加友好的做法是通过 cid 引用内联图片,这时,图片会被作为邮件的附件一起发送给收件人,如果不希望正文图片被客户端识别为可下载的附件,则需要将附件的 Content-Disposition 设置为 inline,同时,还需要设置正确的 Content-Type

Quick summary

  1. Inline only attachments: use multipart/related
  2. Non-inline only attachments: use multipart/mixed
  3. Inline and non-inline attachments use multipart/mixed and multipart/related
mime – HTML-Email with inline attachments and non-inline attachments – Stack Overflow

我的 Email 模板工作流

考虑到 EMail HTML 本身的复杂性,为了简化 Email 模板的开发,我采用了下面的工作流:

首先,使用 degit 初始化一个集成了 TailWind CSS 跟 Vite 的项目模板:

npx degit kometolabs/vite-tailwind-nojs-starter email-template

修改其中的 vite.config.mjs:

import { resolve } from "path";
import { defineConfig } from "vite";
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";

function optimizeSvgPlugin() {
  if (!!process.env.OPTIMIZE_IMAGES) {
    return ViteImageOptimizer({
      svgo: {
        plugins: [{ removeViewBox: false }, { removeDimensions: true }],
      },
    });
  }
}

export default defineConfig(() => ({
  build: {
    outDir: "../dist",
    emptyOutDir: true,
    // avoid inlining images
    assetsInlineLimit: () => false,
    rollupOptions: {
      input: {
        page1: resolve(__dirname, "src", "page1.html"),
        page2: resolve(__dirname, "src", "page2.html"),
      },
      output: {
        // avoid hash in assets filename
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split(".");
          let extType = info[info.length - 1];

          // Explicitly handle image files
          if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
            extType = "img";
            // Return the original filename without hashing
            return `assets/${extType}/[name][extname]`;
          }

          // For other asset types, you can keep the default hashing behavior
          return `assets/${extType}/[name]-[hash][extname]`;
        },
      },
    },
  },
  root: "src",
  plugins: [optimizeSvgPlugin()],
}));

在编写 HTML 时需要手动创建一个引入 TailWind CSS 的 style 标签:

<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

接下来就可以按照常规的方法用 HTML 画页面了,不过需要注意的是,大部分邮件客户端对 flex-box 的支持都比较差,在编写样式时需要注意。

完成了 HTML 页面以及样式后,就可以借助 ChatGPT 将 TailWind 转换成内联样式的代码。最后,借助下面的脚本,即可将构建出来的 HTML 文件转换成 ejs 模板:

// convert all `{{ foo }}` to `<%= foo %>`

function convertToEjs(content: string) {
  return content.replace(/{{\s*(\w+?)\s*}}/g, "<%= $1 %>");
}

/**
 * Convert /assets/img/foo-bar.svg to cid:foo-bar
 * @param content
 */
function toCidReference(content: string): string {
  return content.replace(/\/assets\/img\/([a-zA-Z0-9-)]+?)\.(svg|png|jpg|jpeg)/g, "cid:$1");
}

const files = await glob(dir + "/**/*.html");

for (const filename of files) {
  const content = await fs.readFile(filename, "utf-8");
  const dirOfFile = path.dirname(filename);
  const newContent = toCidReference(convertToEjs(content));
  const newFilename = path.join(
    dirOfFile,
    path.basename(filename, ".html") + ".ejs",
  );

  await fs.writeFile(newFilename, newContent);
  echo(chalk.green(`Converted ${filename} to ${newFilename}`));
}

用来生成邮件模板的命令行操作如下:

#!/usr/bin/bash
OPTIMIZE_IMAGES=true yarn build
cp ./dist/assets/img/* ./assets/img/
npx tsx ./scripts/to-ejs-template.ts --dir ./dist

参考链接

How to Embed Images in Your Emails (CID, HTML Inline & More) | SendGrid

mime – HTML-Email with inline attachments and non-inline attachments – Stack Overflow

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注


Index