# Email trong mã nguồn: nhìn rõ, chuyển nền tảng và cải thiện

URL chính tắc: https://brew.new/vi/blog/emails-in-code-visibility-migration-mcp
Tác giả: Thomas Park
Ngày đăng: 2026-10-12
Ngày cập nhật: 2026-10-15

Email trong mã nguồn là những email sản phẩm được định nghĩa ngay trong codebase và được phát hành như mọi tính năng khác. Ở một đội ngũ dùng Resend và React Email, nếu bạn hỏi sản phẩm đang gửi những email nào, câu trả lời thường là một khoảng lặng. Email chào mừng là một component trong `emails/`. Email đặt lại mật khẩu nằm trong bảng điều khiển của nhà cung cấp dịch vụ xác thực. Email báo sắp hết hạn dùng thử là một chuỗi văn bản trong một cron job. Stripe gửi biên lai. Không ai nhớ thứ gì đang gửi email “chúng tôi nhớ bạn”.

Email nào cũng dễ phát hành, nên không ai nắm được toàn bộ bức tranh. Chúng tôi sẽ xử lý việc này theo thứ tự: nắm toàn cảnh, rồi chuyển nền tảng, rồi cải thiện. Agent lập trình bạn đang dùng có thể làm phần lớn việc này khi được kết nối với Brew qua MCP.

## Những điểm chính

- Hãy bắt đầu bằng một bản kiểm kê, chứ không phải một công cụ mới: mọi email, cùng trình kích hoạt, tệp và các biến của từng email.
- Agent lập trình có thể tìm các lệnh gửi, kết xuất từng mẫu và nhập mẫu đó bằng công cụ MCP `import_email`. Việc nhập mang tính tất định, không dùng AI và miễn phí.
- Email đã nhập được đặt trên một canvas cho mỗi nhóm, nên cả đội có thể rà soát chúng cạnh nhau ở giao diện máy tính và di động.
- Khi chuyển nền tảng, một lệnh gửi kiểu Resend trở thành một trình kích hoạt cùng một tự động hóa đã xuất bản, được kích hoạt bằng một lệnh gọi REST. Mã chạy thực tế dùng REST, không dùng MCP.
- Chuyển từng loại email một, đặt sau một cờ tính năng, và chuyển email giao dịch sau cùng.

## Vì sao email được định nghĩa trong mã lại trở nên vô hình

React Email và Resend đã giúp việc làm email tốt hơn hẳn cho lập trình viên: component có kiểu dữ liệu, được rà soát qua pull request. Cái giá phải trả chỉ lộ ra về sau.

**Email nằm rải rác.** Component, HTML viết thẳng trong một tác vụ nền, mẫu trong bảng điều khiển của bên thứ ba, email do một dịch vụ khác gửi thay bạn. Không có tệp nào liệt kê tất cả.

**Bản xem trước không phải email thực tế.** Máy chủ xem trước của React Email hiển thị một mẫu với preview props của mẫu đó, không phải thứ khách hàng đã nhận, và không hiển thị gì cho các mẫu khác.

**Chỉ kỹ sư mới xem được.** Bộ phận sản phẩm, thiết kế và hỗ trợ sẽ phải nhờ một kỹ sư chạy dự án, nên phần lớn thời gian họ chẳng xem.

**Không có gì được rà soát sau khi ra mắt.** Email từ hai năm trước vẫn mang logo cũ và một gói bạn không còn bán nữa. Không ai nhận ra, vì không ai nhìn thấy các email cùng một lúc.

## Bước 1: Nắm toàn cảnh

Mục tiêu là có một góc nhìn chung về mọi email. Không bước nào ở đây động đến môi trường thực tế.

![Ba giai đoạn đưa email ra khỏi mã nguồn: nắm toàn cảnh với import_email và một canvas cho mỗi nhóm, chuyển nền tảng với tên miền, liên hệ, trình kích hoạt và tự động hóa, rồi tối ưu hóa với kiểm tra email, kiểm tra hiển thị và phân tích.](/images/blog/emails-in-code-three-phases.png)

*Giai đoạn nào cũng hữu ích ngay cả khi đứng riêng, và bạn có thể dừng lại sau bất kỳ giai đoạn nào.*

### Kết nối agent lập trình với Brew

Máy chủ MCP của Brew nằm tại `https://brew.new/api/mcp` và dùng OAuth: bạn đăng nhập bằng Brew và chọn thương hiệu mà agent được phép làm việc. Trong Claude Code:

```bash
claude mcp add --transport http brew "https://brew.new/api/mcp"
```

Lệnh thêm máy chủ này chỉ lưu vào cấu hình cục bộ của dự án hiện tại. Thêm `--scope user` để dùng cho mọi dự án. Trước khi dùng các công cụ của Brew, hãy chạy `claude mcp login brew`, hoặc mở `/mcp` rồi xác thực với Brew.

[Tài liệu thiết lập client](https://docs.brew.new/api-reference/mcp/connect-your-client) có hướng dẫn cho Cursor và Codex. Client không hỗ trợ OAuth có thể dùng khóa API dành riêng cho thương hiệu. Sau đó, hãy cho agent gọi `get_brew_capabilities` để nó đọc danh sách công cụ hiện hành thay vì đoán.

### Lập bản kiểm kê

Hãy yêu cầu agent lập danh sách:

```text
Find every email this codebase sends. Search for provider calls
(resend.emails.send, postmark, sendgrid, nodemailer, SES), React Email
components and render() calls, and any email templates referenced from
config or a third-party dashboard.

For each email, record: a name, what triggers it, the file and line that
sends it, the template file, the provider, and the variables it uses.
Write the result as a table in EMAILS.md. List anything you could not
resolve under "Unknown" instead of guessing.
```

Hãy giữ mục “Unknown”. Email xác nhận của dịch vụ xác thực và biên lai Stripe không phải là lệnh gửi trong mã của bạn, nhưng vẫn phải có mặt trong danh sách. Hãy commit `EMAILS.md`. Nếu dừng lại ở đây, bạn vẫn có một danh sách mà hôm qua bạn chưa có.

### Kết xuất từng mẫu với dữ liệu thực tế

Đầu vào tốt nhất để nhập là HTML mà mã của bạn vốn đã tạo ra. [Tiện ích render](https://react.email/docs/utilities/render) của React Email trả về đúng chuỗi HTML đó:

```tsx
import { render } from 'react-email'
import WelcomeEmail from './emails/welcome'

const html = await render(
  <WelcomeEmail firstName="Ana" planName="Pro" trialDays={14} />
)
```

Các dự án cũ hơn nhập `render` từ `@react-email/render`. Hãy dùng dữ liệu mẫu (fixture) sát thực tế, đừng dùng `"test"`, vì cả đội sẽ rà soát những email này. Hãy cho agent viết script kết xuất từng mẫu ra một tệp.

Brew cũng nhập trực tiếp một module React Email với `format: "jsx"`, đọc tĩnh mà không thực thi module đó. Props được phân giải thành giá trị mặc định hoặc `PreviewProps`, component được nhập từ các tệp cục bộ khác sẽ bị bỏ qua hoặc bị từ chối, còn `await` và các cú pháp tương tự sẽ không được chấp nhận. Nếu mẫu không gói gọn trong một tệp độc lập, hãy dùng HTML đã kết xuất.

### Nhập tất cả vào một nơi

Sau đó, nhập từng bản kết xuất bằng `import_email`, mỗi khu vực sản phẩm một `groupName`:

```text
For each rendered email in ./email-renders, call import_email with
format "html", the title and subject line from EMAILS.md, and a
groupName by area: "Auth", "Onboarding", "Billing" or "Lifecycle".
Report the emailId for each one and any warnings in its assetReport.
```

Lập trình viên có lý do chính đáng để hỏi liệu việc nhập có viết lại email của họ không. Câu trả lời là không:

- Đây là một trình biên dịch tất định, không phải mô hình AI, và không tốn tín dụng.
- Công cụ tạo ra một thiết kế Brew có thể chỉnh sửa, trong khi email gửi đi vẫn giữ HTML nguồn gốc cho các hàng đã nhập.
- Công cụ lưu trữ lại các hình ảnh công khai, và `assetReport` liệt kê những ảnh không tải về được.
- Công cụ loại bỏ script, iframe, biểu mẫu và trình xử lý sự kiện, đồng thời vô hiệu hóa các pixel theo dõi lượt mở có sẵn.
- Mỗi nguồn có dung lượng tối đa 5 MB. Không có đầu vào dạng URL: công cụ nhận trực tiếp mã đánh dấu, và `baseUrl` chỉ dùng để phân giải đường dẫn ảnh tương đối.

### Cùng cả đội rà soát email

Mỗi nhóm có một canvas. Mở `brew.new/emails/onboarding`, bạn sẽ thấy toàn bộ chuỗi email nằm cạnh nhau, kèm nút chuyển giữa giao diện máy tính và di động, cùng bình luận trên từng thiết kế. Liên kết canvas dùng được với những người đã đăng nhập vào không gian làm việc Brew của bạn. Không có liên kết chia sẻ công khai, nên hãy mời người rà soát vào.

Đây là lúc các vấn đề lộ ra: hai logo, một nút có liên kết hỏng, ba chân email. Hãy chạy `audit_email` trước khi sửa bất cứ thứ gì. Công cụ này kiểm tra liên kết, hình ảnh, tính tuân thủ, tổng dung lượng tải, mức hỗ trợ trên ứng dụng email, khả năng tiếp cận và nội dung, tốn 5 tín dụng cho mỗi lần kiểm tra đầy đủ, và nhận một `emailId` đã lưu hoặc HTML thô, nên bạn có thể kiểm tra cả những email chưa nhập. [Danh sách kiểm tra chất lượng email trước khi gửi](/vi/blog/pre-send-email-qa-checklist) của chúng tôi giải thích từng mục kiểm tra và ngưỡng.

Giờ bạn đã có bản kiểm kê, mọi email trên canvas và một danh sách vấn đề, trong khi mã của bạn vẫn gửi mọi thứ như trước.

## Bước 2: Chuyển nền tảng

Lúc này, Brew gửi một số email thay cho mã của bạn. Không phải email nào cũng cần chuyển, và tại mọi thời điểm, mỗi loại email chỉ do một hệ thống gửi.

### Đối chiếu các khái niệm

| Trong mã của bạn hiện nay | Trong Brew |
| --- | --- |
| `resend.emails.send({ react: <WelcomeEmail /> })` | Một sự kiện kích hoạt cùng một tự động hóa đã xuất bản có một bước Gửi email |
| Props truyền vào component | Các trường trong payload của trình kích hoạt, dùng làm thẻ cá nhân hóa như `{{ firstName \| there }}` hoặc `{{ trigger.planName }}` |
| Tên miền `from` | Một tên miền gửi đã xác minh trong Brew |
| Bảng người dùng của bạn | Liên hệ, được tạo qua REST API hoặc một tích hợp |
| Một cron job gửi chuỗi email | Một tự động hóa với các bước Chờ, Bộ lọc và Gửi email |

### 1. Thiết lập tên miền gửi

Agent gọi `create_domain` để lấy các bản ghi DNS, rồi gọi `verify_domain` khi các bản ghi đã có hiệu lực. Tên miền tạo qua MCP và API là tên miền tiếp thị. Với email đặt lại mật khẩu và biên lai, hãy thiết lập một tên miền dành cho email giao dịch trong ứng dụng Brew: tên miền này không chèn chân email hủy đăng ký và vẫn gửi đến các liên hệ đã hủy đăng ký. [Xác minh tên miền gửi](https://docs.brew.new/get-started/verify-your-sending-domain) trình bày chi tiết các bản ghi.

### 2. Đưa liên hệ vào Brew

MCP cập nhật liên hệ nhưng không tạo liên hệ. Hãy tạo liên hệ từ backend bằng `POST /v1/contacts`: endpoint này tạo hoặc cập nhật (upsert) một liên hệ hay một lô tối đa 1.000 liên hệ và tự động tạo trường tùy chỉnh. Bạn cũng có thể tải tệp CSV lên trong ứng dụng. Khi backend gọi trình kích hoạt, Brew cũng tạo hoặc cập nhật liên hệ có trong payload. Chỉ nhập những người đã đồng ý nhận email từ bạn. [Thêm liên hệ](https://docs.brew.new/audience/add-contacts) giải thích những gì Brew cần.

### 3. Thay giá trị xem trước bằng thẻ cá nhân hóa

Một điểm dễ bỏ sót: email chào mừng đã nhập ghi đúng nguyên văn “Hi Ana”. Hãy thay từng giá trị mẫu bằng một thẻ cá nhân hóa như `{{ firstName | there }}`, trong đó phần chữ sau dấu gạch đứng là giá trị dự phòng. Hãy dùng trình chỉnh sửa của Brew, hoặc cho agent chạy `edit_email` và chỉ giới hạn ở những chỗ thay thế đó. Đây là thao tác chỉnh sửa bằng AI có tốn tín dụng, nên hãy kiểm tra bản xem trước. [Thẻ cá nhân hóa](https://docs.brew.new/create-emails/merge-tags) liệt kê cú pháp.

### 4. Tạo trình kích hoạt và tự động hóa

Agent tạo trình kích hoạt bằng `save_trigger`. Lược đồ payload phải có `email` của người nhận. Hãy gán cho trình kích hoạt một định nghĩa payload có kiểu dữ liệu (`infer_payload_contract` soạn nháp định nghĩa từ một payload mẫu, `set_trigger_contract` lưu lại), rồi lấy định nghĩa đó bằng `get_trigger_contract` ở định dạng `ts` hoặc `zod`, để backend của bạn và Brew thống nhất về cấu trúc payload.

Sau đó, `save_automation` dựng trình kích hoạt cùng một bước Gửi email với email, tên miền và tiêu đề. Tự động hóa ở trạng thái bản nháp cho đến khi bạn xuất bản. `dryRun` xác thực sơ đồ, `test_automation` gửi một bản thật đến địa chỉ của bạn, và `check_trigger` xác nhận đã có một tự động hóa được xuất bản đang lắng nghe trước khi môi trường thực tế kích hoạt bất cứ thứ gì.

### 5. Thay lệnh gọi trong mã

Đây là thay đổi duy nhất trong mã ứng dụng của bạn. Trước đây, với Resend:

```ts
await resend.emails.send({
  from: 'Acme <hello@acme.com>',
  to: user.email,
  subject: 'Welcome to Acme',
  react: <WelcomeEmail firstName={user.firstName} planName={plan.name} />,
})
```

Sau khi chuyển, với Brew:

```ts
const res = await fetch(
  `https://brew.new/api/v1/automations/triggers/${process.env.BREW_WELCOME_TRIGGER_ID}/fire`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BREW_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `welcome-${user.id}`,
    },
    body: JSON.stringify({
      payload: { email: user.email, firstName: user.firstName, planName: plan.name },
    }),
  }
)

if (!res.ok) {
  // 422 means the automation is still a draft; 400 means the payload failed the schema.
  throw new Error(`Brew trigger failed: ${res.status} ${await res.text()}`)
}
```

`fetch` vẫn resolve ngay cả khi yêu cầu bị từ chối, nên hãy kiểm tra `res.ok` và để trình chạy tác vụ thử lại hoặc gửi cảnh báo. Endpoint trả về `202` khi tiếp nhận sự kiện và `422` nếu không có tự động hóa đã xuất bản nào lắng nghe trình kích hoạt.

Hãy tạo `Idempotency-Key` từ một ID ổn định, chẳng hạn ID người dùng hoặc ID đơn hàng. Một khóa lặp lại sẽ trả về lượt chạy ban đầu thay vì gửi hai lần, còn dùng lại một khóa với nội dung yêu cầu khác sẽ nhận `409 IDEMPOTENCY_CONFLICT`. Brew lưu khóa của mỗi lượt kích hoạt lâu bằng biên nhận của trình kích hoạt, tức 90 ngày, chứ không phải 24 giờ như nhiều API vẫn dùng. Hãy coi khóa là vĩnh viễn, mỗi hành động một khóa. Khóa dựa trên dấu thời gian sẽ thay đổi sau mỗi lần thử lại, còn một khóa được dùng lại cho một hành động khác vài tuần sau có thể bị phát lại thay vì được gửi.

Mã chạy thực tế gọi endpoint này bằng khóa API, không qua MCP. Qua kết nối OAuth, MCP yêu cầu một người xác nhận các lượt gửi thực tế, lượt kích hoạt và việc xuất bản: hợp lý trong một đoạn chat, nhưng không phù hợp với một trình xử lý yêu cầu. [Hướng dẫn chọn MCP hay REST](/vi/blog/mcp-vs-rest-api-for-email-marketing-which-workflow-should-you-use) giải thích cách phân chia này.

### 6. Chuyển từng email một

1. Đặt lệnh gọi mới sau một cờ tính năng (feature flag), cạnh lệnh gọi cũ, để việc quay lại chỉ là một thay đổi cấu hình.
2. Bắt đầu với một email ít rủi ro, chẳng hạn một email nhắc trong chương trình email vòng đời, và theo dõi tình trạng gửi đến cùng lượt nhấp trong vài ngày.
3. Tiếp theo, chuyển các chuỗi email, khi trình kích hoạt của chúng đã chạy với đúng payload.
4. Chuyển email giao dịch sau cùng. Người dùng sẽ nhận ra trước tiên khi email đặt lại mật khẩu hay magic link bị hỏng.
5. Xóa đường gửi cũ sau một tuần không có sự cố, và cập nhật `EMAILS.md`.

[Hướng dẫn chuyển từ Loops](https://docs.brew.new/migrations/loops) cũng dùng cách làm tương tự.

### Những gì nên giữ lại trong mã

Email xác nhận và email magic link của Supabase vẫn tiếp tục được gửi từ Supabase. Tích hợp Supabase của Brew lắng nghe các sự kiện người dùng để khởi động tự động hóa, chứ không thay thế email của Supabase Auth. Một hóa đơn liệt kê chi tiết từng khoản, được dựng từ dữ liệu phức tạp ngay lúc gửi, cũng có thể đơn giản hơn nếu để trong mã, nên hãy kiểm tra trước xem thẻ cá nhân hóa của bạn cần những gì. Hãy giữ các bản đã nhập trên canvas để những email này vẫn luôn được nhìn thấy.

## Bước 3: Tối ưu hóa

**Kiểm tra toàn bộ.** Chạy `audit_email` cho cả một nhóm, sửa lỗi, rồi kiểm tra lại trước những lượt gửi lớn.

**Kiểm tra trên ứng dụng email thật.** `test_email_rendering` trả về ảnh chụp màn hình từ những ứng dụng email và thiết bị cụ thể, kể cả chế độ tối, với 10 tín dụng mỗi lượt chạy. `create_inbox_placement_test` gửi một bản thật tới các hộp thư thử nghiệm và báo cáo email đã rơi vào đâu, cũng tốn 10 tín dụng.

**Làm cho cả bộ email nhất quán.** Sửa những điểm không nhất quán trong trình chỉnh sửa, hoặc cho agent áp dụng một thay đổi, chẳng hạn chân email mới, cho cả nhóm bằng `edit_email`.

**Đọc kết quả.** `get_email_analytics` báo cáo lượt gửi và mức tương tác. `list_insights` trả về các phát hiện mà Brew đã nhận thấy trong tài khoản của bạn, kèm các chỉ số làm cơ sở cho từng phát hiện.

**Thử nghiệm phiên bản một cách có chủ đích.** Tạo các phiên bản trên canvas và điều hướng liên hệ bằng một bước Phân nhánh dựa trên hành vi, chẳng hạn họ đã nhấp hay chưa. Brew không tự chọn phiên bản thắng cuộc của phép chia ngẫu nhiên cho bạn, nên hãy tự so sánh các phiên bản.

**Giữ cho email mới luôn được nhìn thấy.** Thêm một dòng vào `AGENTS.md` hoặc tệp tương tự: email sản phẩm mới được tạo trong Brew, gửi đi qua trình kích hoạt, và được thêm vào `EMAILS.md` trong cùng pull request.

## Câu hỏi thường gặp

### Brew có nhập được mẫu React Email không?

Có. `import_email` nhận HTML đã kết xuất, MJML, một thư `.eml` đã lưu hoặc một module React Email. HTML đã kết xuất là đáng tin cậy nhất, vì module được đọc tĩnh: props nhận giá trị mặc định hoặc giá trị xem trước, còn component được nhập từ các tệp cục bộ khác sẽ bị bỏ qua.

### Brew có kết nối với tài khoản Resend của tôi không?

Không. Brew không có tích hợp, tính năng nhập hay xuất nào với Resend. Bạn nhập HTML do mã của mình kết xuất, và nếu chuyển nền tảng, mã đó sẽ gọi trình kích hoạt của Brew thay vì gọi Resend.

### Backend của tôi có nên gọi Brew qua MCP không?

Không. Hãy dùng MCP từ agent lập trình cho việc thiết lập, nhập email, kiểm tra và thử nghiệm. Mã chạy thực tế nên gọi REST API bằng khóa API, vì trên kết nối OAuth, MCP yêu cầu một người xác nhận các lượt gửi thực tế và lượt kích hoạt.

### Nhập email có làm thay đổi giao diện email không?

Việc nhập chuyển email thành một thiết kế Brew có thể chỉnh sửa và giữ HTML nguồn gốc để gửi đi. Công cụ loại bỏ script, iframe, biểu mẫu và trình xử lý sự kiện, vô hiệu hóa các pixel theo dõi có sẵn và lưu trữ lại hình ảnh công khai. Hãy rà soát từng email trên canvas, ở cả khung nhìn máy tính và di động, trước khi gửi từ Brew.
