Runbook — hoctapsuotdoi.com

Từ ebook tới hộp thư khách

Dựng lại toàn bộ hệ thống leadpage + tự động gửi ebook qua Lark Mail, viết cho người đã quên hết mọi thứ.

Ebook
Leadpage
Cloudflare Pages
Lark Base
Task Scheduler
(mỗi 2 phút)
Lark Mail
1. Tổng quan 2. Chuẩn bị trước khi bắt đầu 3. Các bước BẮT BUỘC làm thủ công 4. Nơi lưu thông tin nhạy cảm 5. Phần có thể tự động hoá 6. Cách chạy lại từ đầu trên máy hoàn toàn mới 7. Lỗi thường gặp đã xử lý Phụ lục — tham số của dự án mẫu "Học Nhớ Lâu" (đối chiếu nhanh) Phụ lục 2 — tham số dự án "Case Study Business" (2026-09-06, Base RIÊNG không dùng chung)

1. Tổng quan

Đây là một cái máy nhỏ chạy tự động, làm đúng 1 việc: ai vào trang web điền tên/số điện thoại/email để xin một cuốn ebook, thì trong vòng ~2 phút họ nhận được email có link tải ebook đó — không cần ai ngồi bấm gửi tay. Đồng thời, thông tin của người điền (tên, SĐT, email) được lưu lại vào một cái bảng dữ liệu (Lark Base) để sau này liên hệ/bán hàng tiếp.

Máy này gồm 3 mảnh ghép chạy nối tiếp nhau:

  1. Một trang web ("leadpage") — nơi khách điền form.
  2. Một cái bảng lưu dữ liệu (Lark Base) — nơi thông tin khách rơi vào.
  3. Một chương trình nhỏ chạy nền trên máy tính — cứ vài phút lại nhìn vào bảng dữ liệu, thấy ai chưa được gửi mail thì tự gửi.

2. Chuẩn bị trước khi bắt đầu

2.1 Tài khoản cần có sẵn (tạo trước, không cần làm ngay hôm nay)

Tài khoản Dùng để làm gì Ghi chú
Cloudflare (dash.cloudflare.com) Host trang web (miễn phí), quản lý DNS tên miền Đăng ký bằng email thật, cần xác thực email trước khi dùng được (xem mục 3)
Một tên miền (domain) đã mua sẵn Địa chỉ web (vd hoctapsuotdoi.com) Tên miền phải trỏ nameserver về Cloudflare trước (làm 1 lần lúc mua/chuyển domain, không nằm trong runbook này)
Lark / Feishu (workspace tổ chức + 1 tài khoản admin) Vừa là nơi lưu dữ liệu khách (Lark Base), vừa là hộp thư gửi mail (Lark Mail) Cần có sẵn 1 "app" đã đăng ký trong Lark Developer Console (open.larksuite.com) — xem mục 3
Một hộp Gmail (hoặc email bất kỳ) để test Dùng để tự gửi thử, kiểm tra email nhận được có đúng không Không bắt buộc nhưng nên có

"Trỏ nameserver về Cloudflare" nghĩa là gì, và có cần làm lại mỗi lần không? Tên miền khi mới mua luôn thuộc quyền quản lý DNS của nơi bán nó (Mắt Bão, GoDaddy, Namecheap...). Muốn Cloudflare host được trang web trên tên miền đó, phải "giao" quyền quản lý DNS cho Cloudflare — gọi là đổi nameserver. Chỉ cần làm đúng 1 lần cho mỗi tên miền, không phải mỗi lần làm leadpage mới: 1. Cloudflare Dashboard → Add a site → gõ tên miền → chọn gói Free. 2. Cloudflare tự quét DNS hiện có, hiện danh sách bản ghi → bấm Continue. 3. Cloudflare đưa ra 2 địa chỉ nameserver (dạng xxx.ns.cloudflare.com). 4. Đăng nhập trang quản lý tên miền ở nơi đã mua (Mắt Bão/GoDaddy/...) → tìm mục Nameserver/DNS Server → thay bằng 2 địa chỉ Cloudflare vừa đưa. 5. Đợi vài phút tới vài giờ, Cloudflare tự phát hiện và chuyển trạng thái sang Active.

Domain hoctapsuotdoi.com dùng trong runbook này đã làm xong bước này rồi — muốn làm thêm ebook/leadpage mới thì dùng lại chính domain này, chỉ cần thêm 1 tên miền con mới, không cần trỏ nameserver lại từ đầu. Chỉ áp dụng lại từ đầu khi dùng một domain hoàn toàn khác.

Muốn chuyển 1 domain ĐÃ CÓ SẴN trên Cloudflare sang một TÀI KHOẢN Cloudflare khác thì làm sao? (đã tự tay thử thành công ngày 2026-09-06, với hoctapsuotdoi.com từ tài khoản cũ sang tài khoản mới) 1. Ghi lại 2 nameserver hiện tại trước khi đổi gì cả (Cloudflare tài khoản cũ → domain → mục Nameservers) — để có đường lùi nếu cần trả lại như cũ. 2. Đăng nhập tài khoản Cloudflare mới → menu trái bấm Domains → góc trên phải bấm nút xanh Add domain. 3. Trong 3 lựa chọn hiện ra, bấm Connect a domain (KHÔNG bấm "Transfer a domain" — cái đó là chuyển quyền sở hữu đăng ký domain; cũng không bấm "Buy a domain" vì domain đã có sẵn rồi). 4. Gõ tên domain → Continue → chọn gói FreeContinue. 5. Cloudflare tự quét DNS hiện có, hiện danh sách bản ghi tìm thấy được. Đọc kỹ — công cụ quét không phải lúc nào cũng bắt được hết, đặc biệt các CNAME ít phổ biến. Cứ bấm Continue, bổ sung tay phần thiếu ở bước 10. 6. Cloudflare đưa ra 2 nameserver mới — ghi lại. 7. Đăng nhập trang quản lý domain ở nơi đã mua → tìm mục Nameserver/DNS Server → xoá 2 cái cũ, điền 2 cái mới ở bước 6 → Save. 8. Đợi vài phút, kiểm tra đã đổi thật chưa: nslookup -type=NS <ten-mien> 1.1.1.1 — thấy trả về đúng 2 nameserver mới ở bước 6 là thành công. 9. Vào lại tài khoản Cloudflare mới → ô Quick search góc trên trái (hoặc Ctrl K) → gõ tên domain → bấm vào kết quả để mở đúng trang quản lý domain đó (domain "Connect" kiểu này KHÔNG hiện trong mục "Domains" ở sidebar, phải tìm bằng Quick Search). Thấy dòng "Your domain is now protected by Cloudflare" là domain đã Active. 10. Bấm DNS Records ở khung bên phải, tự thêm tay lại mọi bản ghi bị thiếu ở bước 5 (thường là CNAME của các leadpage/Cloudflare Pages đang dùng domain con). 11. Kiểm tra lại: curl -sI https://<tên-miền-con>/ phải trả 200; nếu dùng Lark Mail, kiểm tra thêm nslookup -type=MX <ten-mien> 1.1.1.1 vẫn còn đủ 3 dòng mx1/mx2/mx3.larksuite.com.

Muốn đổi lại như cũ: lặp lại bước 7 nhưng điền 2 nameserver đã ghi ở bước 1 — không cần làm lại "Connect a domain" ở tài khoản cũ vì domain vẫn còn nguyên cấu hình ở đó.

2.2 Thông tin nội dung cần chuẩn bị (không phải tài khoản, mà là thông tin thật)

Trước khi viết trang, phải có sẵn — không được bịa, thiếu thì phải hỏi người sở hữu thương hiệu:

2.3 Phần mềm cần cài trên máy tính chạy hệ thống này

Máy này là máy chạy Claude Code (hoặc máy chạy chương trình gửi mail nền — xem mục 5.3), không phải máy của khách. Cần cài:

Phần mềm Kiểm tra đã cài chưa Cài ở đâu
Node.js (bản LTS mới nhất) mở PowerShell gõ node --version nodejs.org
Python 3 python --version python.org (nhớ tick "Add to PATH" lúc cài)
Thư viện Pillow cho Python (dùng để tự vẽ ảnh bìa ebook) python -m pip install pillow
lark-cli (công cụ dòng lệnh điều khiển Lark) lark-cli doctor, phải thấy "ok": true npm install -g @larksuite/cli
Google Chrome cài bản thường, dùng để tự chụp ảnh xem-thử trang web

wrangler (công cụ deploy web lên Cloudflare) không cần cài riêng — dùng qua npx mỗi lần cần, tự tải bản mới nhất.


3. Các bước BẮT BUỘC làm thủ công

Đây là những bước không thể và không nên viết thành code — lý do chung: mỗi bước đều dính tới bảo mật (chống robot giả làm người, hoặc cần một con người thật chịu trách nhiệm bấm nút xác nhận), hoặc là một quyết định nội dung/thương hiệu mà máy không tự biết đúng sai. Người làm lại hệ thống này (kể cả AI) phải tự tay đi qua từng bước, không được viết script giả lập thay thế. (Lý do riêng của từng bước không nhắc lại nữa — chỉ ghi khi nào có cách bỏ bớt bước tay đó.)

3.1 Tạo & xác thực tài khoản Cloudflare

  1. Vào dash.cloudflare.com, đăng ký tài khoản bằng email thật.
  2. Mở hộp thư, tìm email xác thực Cloudflare gửi, bấm link xác nhận trong đó.
  3. Kết quả cần lưu: không cần lưu gì đặc biệt — chỉ cần nhớ email + mật khẩu đăng nhập Cloudflare (lưu trong trình quản lý mật khẩu cá nhân, không lưu trong dự án).

Dấu hiệu chưa xong: tạo Pages project mà báo lỗi Your user email must been verified (mã lỗi 8000077) → quay lại bước 2, chưa bấm xác thực.

3.2 Đăng nhập Cloudflare cho công cụ deploy (wrangler login)

  1. Chạy lệnh: npx wrangler login
  2. Trình duyệt tự bật ra, hiện câu hỏi "Cho phép Wrangler truy cập tài khoản của bạn?"
  3. Bấm Allow.
  4. Kết quả cần lưu: không cần lưu tay — wrangler tự lưu phiên đăng nhập vào %APPDATA%\xdg.config\.wrangler\config\default.toml, dùng lại cho mọi lệnh sau. Phiên này có thể hết hạn sau thời gian dài không dùng — hết hạn thì chạy lại đúng lệnh ở bước 1.

3.3 Thêm bản ghi DNS cho tên miền con của trang web (CNAME)

  1. Vào dash.cloudflare.com, chọn đúng tên miền gốc trong danh sách.
  2. Menu bên trái, bấm DNS.
  3. Bấm nút Add record.
  4. Điền đúng 4 ô: - Type: CNAME - Name: chỉ phần tên miền con thôi, ví dụ hocnholaukhông gõ thêm .tenmien.com phía sau - Target: <tên-project-cloudflare-pages>.pages.dev - Proxy status: bấm cho nó chuyển sang đám mây màu cam (chữ "Proxied") — để màu xám thì không cấp được SSL
  5. Bấm Save.

Cách bỏ hẳn bước tay này cho các lần sau (tuỳ chọn): 1. Vào dash.cloudflare.com → góc trên phải → My ProfileAPI Tokens. 2. Bấm Create TokenCustom Token. 3. Cấp đúng quyền Zone → DNS → Edit, giới hạn chỉ 1 tên miền cụ thể. 4. Lưu chuỗi token hiện ra như một bí mật (xem mục 4) — Cloudflare chỉ hiện 1 lần duy nhất. 5. Có token này thì bước 3.3 chuyển được sang gọi API tự động (mục 5), không cần vào Dashboard nữa.

3.4 Đăng nhập Lark cho lark-cli (lark-cli auth login)

  1. Chạy lệnh: lark-cli auth login --domain mail --no-wait --json
  2. Lệnh trả về 1 verification_url — mở link đó (hoặc quét QR bằng lark-cli auth qrcode <url> trên điện thoại).
  3. Bấm xác nhận đăng nhập & đồng ý cấp quyền trên trang/app Lark.
  4. Quay lại terminal, chạy: lark-cli auth login --device-code <device_code-đã-nhận-ở-bước-1>
  5. Kết quả cần lưu: không cần lưu tay — lark-cli tự giữ phiên đăng nhập an toàn trên máy. Kiểm tra còn hiệu lực bằng lark-cli auth status.

Khi nào phải lặp lại: đổi máy tính, hoặc phiên đăng nhập bị thu hồi/hết hạn.

3.5 Lấy App Secret của app Lark đã có sẵn (Lark Developer Console)

  1. Vào open.larksuite.com, đăng nhập tài khoản quản trị app.
  2. Bấm Developer Console (góc trên phải).
  3. Dùng icon kính lúp 🔍 cạnh nút đó, gõ tên app cần tìm → bấm vào đúng app.
  4. Menu dọc bên trái, bấm Credentials & Basic Info.
  5. Mục App Secret bị che dấu *** — bấm icon con mắt 👁 bên cạnh để hiện chuỗi thật.
  6. Copy chuỗi đó (~32 ký tự).
  7. Kết quả cần lưu: dán App Secret trực tiếp vào bước nạp secret Cloudflare (mục 5) — không lưu vào bất kỳ file text/code nào trong dự án.

3.5b Tạo MỘT APP LARK MỚI từ đầu (khi chưa có app nào, hoặc muốn tách riêng app cho từng dự án)

Mục 3.5 ở trên giả sử app đã tồn tại sẵn. Mục này dành cho trường hợp chưa có app nào cả — đã làm thật ngày 2026-09-06, mất khá nhiều lượt hỏi qua lại vì giao diện Lark Developer dễ gây lạc.

  1. Vào open.larksuite.com, bấm Developer Console (góc trên phải).
  2. ⚠️ Điểm hay lạc: màn hình mặc định hiện ra là "Start from scratch" / "Create Custom App" — đây là màn tạo app mới, KHÔNG phải danh sách app đã có. Đang tìm app cũ thì đừng bấm gì ở đây — quay lại mục 3.5 (dùng kính lúp 🔍 cạnh nút Developer Console để tìm theo tên).
  3. Thật sự cần tạo mới thì bấm "Create Custom App".
  4. Điền App Name (gõ gì cũng được).
  5. Điền Description (gõ gì cũng được).
  6. Bấm Create.
  7. Lark tự chuyển vào trang quản lý app đó — menu trái hiện ra, bấm Credentials & Basic Info.
  8. Thấy ngay App ID (không bị che) — copy lại.
  9. Bấm icon con mắt 👁 cạnh App Secret để hiện, copy lại.

Kết quả cần lưu: App ID mới (cli_...) + App Secret mới — dùng thay cho App ID/Secret cũ trong functions/api/lead.js và mọi script gọi Lark API cho dự án này.

⚠️ App vừa tạo CHƯA có quyền đọc/ghi Bitable — làm ngay mục 3.5c dưới đây trước khi dùng, không thì gọi API ghi lead sẽ bị từ chối mã lỗi 99991672.

3.5c Cấp quyền Bitable cho app mới (bitable:app + base:record:create)

  1. Gọi thử 1 API bất kỳ tới app đó (ví dụ ghi 1 bản ghi test) — nếu thiếu quyền, Lark trả lỗi kèm sẵn 1 link tắt trong trường helps[0].url, dạng: https://open.larksuite.com/app/<APP_ID>/auth?q=bitable:app,base:record:create&op_from=openapi&token_type=tenant
  2. Mở link đó bằng trình duyệt.
  3. Trang hiện sẵn đúng các quyền còn thiếu — bấm Add (hoặc Enable) từng quyền một.
  4. Bấm Save / Create version / Publish để lưu (app nội bộ dùng riêng thì có hiệu lực ngay, không cần chờ duyệt).

Dấu hiệu app đang thiếu quyền: gọi API trả về {"code":99991672,"msg":"Access denied. One of the following scopes is required: [...]"} — mở lại đúng link trong helps[0].url của lỗi đó rồi lặp lại bước 2-4.

3.5d Copy một Base có sẵn làm bảng riêng cho dự án/thương hiệu mới

Khi nào cần: bảng "17.1 Lead Magnet" mẫu đang nằm chung 1 Base lớn với nhiều việc khác (nhân sự, kho, kế toán...), muốn có bản riêng sạch chỉ cho 1 thương hiệu/dự án.

  1. Mở Base mẫu.
  2. Bấm menu "..." ở góc trên Base.
  3. Chọn Copy (hoặc Duplicate base).
  4. Lark tạo ra 1 Base hoàn toàn mới, tên có thêm chữ "Sao chép".

⚠️ Base mới có app_token (base token) và toàn bộ table_id bên trong đổi khác 100% so với bản gốc — kể cả bảng "17.1 Lead Magnet" copy sang cũng có table_id mới tinh. Không được giả định giống bản gốc — phải làm lại từ đầu: 1. Lấy app_token mới = chuỗi ngay sau /base/ trong URL của Base mới. 2. Liệt kê lại table_id các bảng (mục 5.3). 3. Kiểm tra lại tên cột (có thể khác thứ tự). 4. Thử ghi 1 bản ghi test lại từ đầu (mục 5.3) — đừng bỏ qua bước này.

Kết quả cần lưu: app_token + table_id mới, ghi đè vào hằng số BASE_TOKEN_MAC_DINH / TABLE_ID_MAC_DINH trong functions/api/lead.js và mọi script nền đọc/ghi Base này.

3.6 Bật dịch vụ Lark Mail cho tổ chức (chỉ làm 1 lần cho cả tổ chức)

  1. Vào admin.larksuite.com, đăng nhập tài khoản admin.
  2. Gõ "Mail" vào ô tìm kiếm trên cùng (hoặc bấm Product Settings) → chọn mục Email/Mail.
  3. Bấm Configure Now.
  4. Nhập tên miền muốn dùng để gửi mail — dùng tên miền riêng, ví dụ hoctapsuotdoi.com. Không dùng kiểu ...@xxx.larksuite.com — địa chỉ đó lộ chữ "larksuite", kém chuyên nghiệp.
  5. Lark trả về 5 bản ghi DNS cần thêm (thường 2 TXT + 3 MX) — thêm đúng nguyên văn vào Cloudflare DNS của tên miền đó (mục 3.3), Name = @ cho cả 5 bản ghi.
  6. Đợi vài phút cho DNS lan truyền, bấm Verify Now trên trang Lark.
  7. Vào Quản lý tổ chức → Thành viên và Bộ phận.
  8. Chọn đúng tài khoản người dùng cần gửi mail.
  9. Cột Email doanh nghiệp đang ghi "Chưa thiết đặt" → bấm chữ "Thêm".
  10. Gõ phần tên trước dấu @ (local-part), ví dụ ebook.
  11. Bấm Lưu.

Kết quả cần lưu: địa chỉ mailbox vừa tạo (vd ebook@hoctapsuotdoi.com) — dùng lại ở bước gửi mail (mục 5). Bước này không cần lặp lại cho các leadpage sau, miễn dùng lại cùng domain.

Kiểm tra đã xong chưa: chạy lệnh lark-cli mail user_mailboxes profile --as user --params '{"user_mailbox_id":"me"}' → phải trả về đúng địa chỉ mail, không phải chuỗi rỗng.

3.7 Duyệt nội dung/giao diện trang trước khi đưa lên mạng

  1. Mở ảnh chụp thử trang (preview.png).
  2. Đọc lại toàn bộ copy (câu chữ) trên trang.
  3. Nói 1 câu "duyệt" hoặc "sửa lại chỗ X" — chỉ khi có câu "duyệt" mới được deploy lên tên miền thật.

4. Nơi lưu thông tin nhạy cảm

Hệ thống này hiện KHÔNG dùng file .env (chưa gom lại thành 1 chỗ) — các giá trị đang nằm rải rác trực tiếp trong code, ngoại trừ đúng 1 giá trị được cách ly làm bí mật thật sự. Bảng dưới liệt kê từng giá trị và nơi nó thực sự đang nằm, để biết chỗ nào cần thận trọng khi chia sẻ file:

Tên biến (khái niệm) Là gì Nơi đang lưu thật Mức nhạy cảm
LARK_APP_SECRET Bí mật app Lark Cloudflare Pages Secret (wrangler pages secret put) — không nằm trong bất kỳ file nào, không ai đọc lại được sau khi nạp 🔴 Rất nhạy cảm — lộ ra là mất quyền kiểm soát app Lark
LARK_APP_ID Định danh app Lark (không bí mật) Hằng số trong functions/api/lead.jsscripts/send-ebook-emails.py 🟢 Công khai được
LARK_BASE_TOKEN Định danh Lark Base (bảng dữ liệu) Hằng số trong 2 file trên 🟡 Nên giữ kín (kèm App Secret mới khai thác được)
LARK_TABLE_ID Định danh bảng con trong Base Hằng số trong 2 file trên 🟡 Tương tự
CLOUDFLARE_ACCOUNT_ID Định danh tài khoản Cloudflare Ghi tạm trong lịch sử lệnh/ghi chú dự án 🟢 Không tự làm gì được nếu thiếu quyền đăng nhập
CLOUDFLARE_API_TOKEN (nếu làm mục 3.3 tuỳ chọn) Chìa khoá sửa DNS phạm vi hẹp Chưa tạo — nếu tạo thì nên lưu ở Windows Credential Manager hoặc 1 file .env không commit vào git 🔴 Nhạy cảm trong phạm vi đã giới hạn
Mailbox gửi mail (ebook@hoctapsuotdoi.com) Địa chỉ gửi Hằng số trong scripts/send-ebook-emails.py, và cấu hình thật nằm trong tài khoản Lark (mục 3.6) 🟢 Công khai được (khách nhìn thấy)

Khuyến nghị cải thiện (chưa làm): gom hết các hằng số trên vào 1 file .env thật (không đưa vào git), rồi cho 2 file code đọc từ đó — hiện tại vì chỉ có 1 giá trị thật sự bí mật (LARK_APP_SECRET, đã cách ly đúng cách) nên rủi ro thấp, nhưng gom lại vẫn gọn hơn khi cần đổi.


5. Phần có thể tự động hoá

Từ đây trở xuống là phần có sẵn credentials từ mục 3 rồi thì chạy được hết bằng lệnh, không cần thao tác chuột nữa (trừ khi làm thêm mục 3.3 tuỳ chọn thì DNS cũng tự động luôn).

5.1 Dựng nội dung + ảnh bìa + trang web tĩnh

# 2. Nới đáy ảnh (chừa chỗ cho nút bấm đè lên) + nén sang WebP cho nhẹ powershell -File "\scripts\render-bia-hero.ps1" ` -Nguon ".\bia\anh-bia-goc.png" -Ra ".\bia\bia-hero.webp"

# 3. Build trang từ file cấu hình python "\scripts\build_ebook_leadpage.py" ` --config ".\config-ebook.json" --out ".\site"

# 4. Chụp ảnh xem thử bằng Chrome (để duyệt ở bước 3.7) & "C:\Program Files\Google\Chrome\Application\chrome.exe" --headless=new --disable-gpu --hide-scrollbars --window-size=460,2300 --virtual-time-budget=6000 --screenshot=".\preview.png" "file:///<đường dẫn tuyệt đối>/site/index.html" ```

5.2 Đưa trang lên mạng (Cloudflare Pages) + gắn tên miền

npx wrangler pages deploy site --cwd "" ` --project-name --branch main --commit-dirty=true # Phải thấy dòng "✨ Compiled Worker successfully"

# Gắn tên miền con (wrangler không có lệnh này, gọi thẳng API): $tok = [regex]::Match([IO.File]::ReadAllText("$env:APPDATA\xdg.config.wrangler\config\default.toml"), 'oauth_token\s*=\s*"([^"]+)"').Groups[1].Value Invoke-RestMethod -Method Post -Uri "https://api.cloudflare.com/client/v4/accounts//pages/projects//domains" -Headers @{ Authorization = "Bearer $tok" } -ContentType "application/json" -Body '{"name":""}' ``` → sau lệnh cuối vẫn cần bước tay 3.3 (thêm CNAME) trừ khi đã có Cloudflare API Token riêng.

Dùng khi có sẵn 1 link dài/xấu (link sách lật Heyzine, link Google Drive, link YouTube...) và muốn thay bằng 1 tên miền con dễ nhớ, mang tên thương hiệu, dễ dán vào email/Zalo/Facebook. Ví dụ đã làm thật: rút gọn link https://heyzine.com/flip-book/4e300476a9.html thành https://casestudybusiness.hoctapsuotdoi.com/.

Bước 1 — Tạo đúng 1 thư mục + 1 file, không cần gì khác:

Tạo thư mục mới, ví dụ C:\Chip brain\production\redirect-<ten-ngan>\site\, rồi tạo file index.html bên trong với đúng nội dung này (chỉ sửa dòng có chữ ĐỔI:):

<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=ĐỔI: DÁN LINK ĐÍCH DÀI VÀO ĐÂY">
<link rel="canonical" href="ĐỔI: DÁN LINK ĐÍCH DÀI VÀO ĐÂY">
<title>ĐỔI: tên trang</title>
</head>
<body>
  <p>Đang chuyển tới trang đích...
     Nếu không tự chuyển, <a href="ĐỔI: DÁN LINK ĐÍCH DÀI VÀO ĐÂY">bấm vào đây</a>.</p>
</body>
</html>

Bước 2 — Mở PowerShell, chạy đúng 2 lệnh này (thay <ten-du-an> bằng 1 cái tên ngắn, chỉ chữ thường/số/gạch ngang, ví dụ redirect-casestudy-business):

cd "C:\Chip brain\production\redirect-<ten-ngan>"

npx wrangler pages project create <ten-du-an> --production-branch main

npx wrangler pages deploy site --project-name <ten-du-an> --branch main --commit-dirty=true

Chạy xong lệnh thứ 2, màn hình in ra 1 dòng dạng ✨ Deployment complete! ... https://xxxx.<ten-du-an>.pages.dev — vậy là trang đã sống, nhưng còn ở địa chỉ xấu .pages.dev, cần làm tiếp bước 3-4 để có tên miền đẹp.

Bước 3 — Gắn tên miền con đẹp (chạy lệnh, không cần vào Cloudflare Dashboard ở bước này):

$tok = [regex]::Match([IO.File]::ReadAllText("$env:APPDATA\xdg.config\.wrangler\config\default.toml"), `
  'oauth_token\s*=\s*"([^"]+)"').Groups[1].Value
Invoke-RestMethod -Method Post `
  -Uri "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/pages/projects/<ten-du-an>/domains" `
  -Headers @{ Authorization = "Bearer $tok" } -ContentType "application/json" `
  -Body '{"name":"<ten-mien-con-day-du-vd-casestudybusiness.hoctapsuotdoi.com>"}'

(<ACCOUNT_ID> lấy trong Phụ lục ở cuối file này.) Lệnh này chỉ báo cho Cloudflare biết sắp có tên miền đó — CHƯA làm tên miền chạy được, phải làm tiếp bước 4.

Bước 4 — Bước tay DUY NHẤT: thêm 1 bản ghi DNS trên Cloudflare Dashboard

  1. Mở trình duyệt, vào dash.cloudflare.com, đăng nhập.
  2. Bấm chọn đúng tên miền gốc (ví dụ hoctapsuotdoi.com) trong danh sách.
  3. Menu bên trái, bấm DNS → bấm nút Add record (thường ở góc phải).
  4. Điền đúng 4 ô: - Type: chọn CNAME - Name: chỉ gõ phần tên miền con thôi (ví dụ casestudybusiness) — KHÔNG gõ thêm .hoctapsuotdoi.com phía sau, gõ dư là bị nối đuôi 2 lần thành sai. - Target: gõ đúng <ten-du-an>.pages.dev (tên dự án đặt ở Bước 2, thêm .pages.dev phía sau) - Proxy status: bấm vào để nó chuyển thành đám mây màu CAM (chữ "Proxied") — nếu đang màu xám (chữ "DNS only") thì bấm 1 lần để đổi sang cam.
  5. Bấm nút Save.

Bước 5 — Kiểm tra đã chạy chưa: chờ khoảng 30-60 giây, rồi mở đúng link tên miền con vừa gắn (ví dụ https://casestudybusiness.hoctapsuotdoi.com) bằng trình duyệt — phải tự động nhảy sang đúng trang đích dài. Nếu báo lỗi/không nhảy, chờ thêm 1-2 phút rồi thử lại (DNS cần chút thời gian lan truyền), hoặc kiểm tra lại đúng 4 ô ở Bước 4 có gõ sai chỗ nào không.

5.3 Nối form vào Lark Base (lưu lead)

# Xem tên các cột thật trong bảng đó lark-cli api GET /open-apis/bitable/v1/apps//tables//fields --as bot

# Thử ghi 1 bản ghi test TRƯỚC khi viết mã — chạy được thì xoá đi rồi mới yên tâm lark-cli base +record-upsert --as bot --base-token --table-id --json '{"Họ và Tên":"TEST"}' ``` Sau khi test ghi thành công: sửafunctions/api/lead.js(điền đúngLARK_APP_ID,LARK_BASE_TOKEN,LARK_TABLE_ID`, tên cột), rồi nạp secret và deploy lại: ```powershell # Dán App Secret (lấy từ bước 3.5) qua PowerShell, KHÔNG in ra lại: $secret = "" $secret | npx wrangler pages secret put LARK_APP_SECRET --project-name $secret = $null

npx wrangler pages deploy site --cwd "" ` --project-name --branch main --commit-dirty=true ```

5.4 Chương trình chạy nền: tự gửi email qua Lark Mail


6. Cách chạy lại từ đầu trên máy hoàn toàn mới

Giả sử đã có sẵn: tài khoản Cloudflare (đã xác thực), tên miền đã trỏ Cloudflare, app Lark đã đăng ký (có App ID, App Secret, đã bật đủ quyền bitable + mail:user_mailbox.message:send), và 1 Lark Base đã có bảng lưu lead. Nếu chưa có bất kỳ thứ nào ở trên, phải làm xong toàn bộ mục 3 trước.

  1. Cài phần mềm nền (mục 2.3): Node.js, Python + Pillow, lark-cli, Chrome.
  2. Đăng nhập lark-cli trên máy mới: lark-cli auth login --domain mail --no-wait --json rồi hoàn tất theo hướng dẫn (mục 3.4). Nếu chỉ cần đọc/ghi Base (chưa cần gửi mail ngay) thì --domain base là đủ, thêm --domain mail khi cần gửi mail.
  3. Đăng nhập wrangler trên máy mới: npx wrangler login (mục 3.2).
  4. Nếu domain gửi mail chưa từng bật Lark Mail trước đó: làm trọn mục 3.6 (bật dịch vụ, thêm DNS, gán mailbox). Nếu domain này đã bật rồi (dùng lại cho ebook mới cùng thương hiệu) thì bỏ qua bước này hoàn toàn, mailbox vẫn còn nguyên.
  5. Đọc ebook thật, hỏi đủ thông tin thương hiệu (mục 2.2), viết config-ebook.json.
  6. Chạy trọn mục 5.1 (vẽ bìa → build trang → chụp ảnh xem thử).
  7. Đưa preview.png cho người có quyền duyệt — có gật đầu mới sang bước 8 (mục 3.7).
  8. Chạy mục 5.2 (deploy Cloudflare Pages + gọi API gắn tên miền con).
  9. Thêm bản ghi CNAME bằng tay trên Cloudflare Dashboard (mục 3.3) — trừ khi đã có sẵn API Token DNS riêng, thì tự động luôn trong bước 8.
  10. Lấy app_token thật + tên cột của Base, thử ghi test, sửa functions/api/lead.js, lấy App Secret (mục 3.5) dán vào, nạp secret, deploy lại (mục 5.3).
  11. Bắn 1 lead thật qua form, mở Base nhìn tận mắt xác nhận có ghi vào — nếu sai thì sửa lại functions/api/lead.js rồi deploy lại, đừng bỏ qua bước nhìn tận mắt này.
  12. Tạo cột checkbox đánh dấu đã gửi, viết scripts/send-ebook-emails.py (mục 5.4), đăng ký Task Scheduler chạy mỗi 2 phút.
  13. Bắn 1 lead test khác, đợi ≤2 phút, mở hộp thư thật kiểm tra: tiêu đề đúng, tên không vỡ dấu, nút bấm mở đúng ebook.
  14. Xoá sạch mọi bản ghi test khỏi Lark Base trước khi chạy quảng cáo thật.

7. Lỗi thường gặp đã xử lý

# Lỗi gặp phải Nguyên nhân Cách đã sửa
1 Gọi lark-cli api GET /open-apis/... qua Git Bash trả về lỗi 404 vô lý Git Bash (MSYS) tự "dịch" chuỗi bắt đầu bằng / thành đường dẫn ổ đĩa Windows, biến /open-apis/... thành /open-apis/C:/Program Files/Git/open-apis/... Thêm biến môi trường: MSYS_NO_PATHCONV=1 lark-cli api GET /open-apis/... (chỉ cần trong Git Bash, PowerShell không dính lỗi này)
2 Tạo Cloudflare Pages project báo lỗi Your user email must been verified (mã 8000077) Tài khoản Cloudflare mới chưa xác thực email Vào hộp thư bấm link xác thực Cloudflare gửi, chạy lại lệnh
3 Gắn tên miền xong, gọi API lấy DNS records trả về 403 Authentication error Token của wrangler login chỉ có quyền zone:read, không có quyền sửa DNS Chấp nhận thêm CNAME bằng tay trên Dashboard, hoặc tạo riêng 1 API Token có quyền Zone:DNS:Edit
4 Python subprocess.run(["lark-cli"], ...) báo FileNotFoundError: [WinError 2] Trên Windows, subprocess không tự tìm phần mở rộng file (.cmd/.exe) như khi gõ trong terminal Trỏ thẳng đường dẫn đầy đủ có phần mở rộng, ví dụ lark-cli.cmd (bước tạm)
5 Tên khách có dấu tiếng Việt bị vỡ chữ (Trần Hà LinhTr?n H<ký tự lỗi> Linh) ngay trong email đã gửi thật, dù dữ liệu trong Lark Base vẫn đúng lark-cli.cmd là file .cmd, bắt buộc chạy qua cmd.exe — và cmd.exe áp lại bảng mã ANSI của hệ điều hành lên MỌI tham số dòng lệnh trước khi chuyển cho chương trình thật, làm hỏng ký tự Unicode không nằm trong bảng mã đó. Đây là bẫy nặng nhất, khó phát hiện nhất: chương trình chạy xong không báo lỗi gì cả, số liệu đọc ra vẫn đúng — chỉ có DỮ LIỆU TRUYỀN VÀO LÀM THAM SỐ (tên khách trong --subject, tên cột tiếng Việt trong --field-id) mới bị hỏng Không gọi lark-cli.cmd nữa — gọi thẳng node.exe kèm file JS thật của lark-cli: node.exe "...\npm\node_modules\@larksuite\cli\scripts\run.js" <tham số>. Cách này để Windows chuyển tham số Unicode thẳng cho Node.js, không qua lớp cmd.exe trung gian
6 Ảnh bìa tự vẽ bằng Pillow: chữ có dấu tiếng Việt hiện ra ô vuông trống (H□c Nh□ Lâu) Font georgia.ttf/georgiab.ttf không có sẵn glyph (hình dạng ký tự) cho các chữ có dấu tổ hợp của tiếng Việt (, ...) Đổi sang arialbd.ttf — font Arial trên Windows có đủ bộ chữ tiếng Việt. Kiểm tra nhanh 1 font có đủ dấu không bằng thư viện fontTools: ord('ọ') in TTFont(đường_dẫn_font).getBestCmap()
7 Windows Task Scheduler cứ 2 phút lại bật 1 cửa sổ đen (console) nhấp nháy Task gọi python.exe — bản Python có cửa sổ console đi kèm Đổi action của task sang pythonw.exe (nằm cùng thư mục python.exe, không có console). Với tiến trình con gọi từ trong script (lark-cli), thêm creationflags=subprocess.CREATE_NO_WINDOW để nó cũng không tự bật cửa sổ riêng
8 New-ScheduledTaskSettingsSet -AllowStartIfOnBattery báo lỗi "tham số không tồn tại" Gõ nhầm tên tham số, thiếu chữ "ies" ở cuối Tên đúng là -AllowStartIfOnBatteries (và -DontStopIfGoingOnBatteries)
9 Register-ScheduledTask báo lỗi "task XML sai định dạng" khi đặt lặp lại "vĩnh viễn" bằng [TimeSpan]::MaxValue Giá trị MaxValue tạo ra chuỗi thời lượng quá lớn, không hợp lệ với định dạng XML của Task Scheduler Dùng một khoảng thời gian rất dài nhưng hợp lệ, ví dụ (New-TimeSpan -Days 3650) (~10 năm) thay vì vô hạn thật
10 Gọi lark-cli mail user_mailboxes profile --as bot báo lỗi "chỉ hỗ trợ user" Lark Mail là hộp thư gắn với con người thật, API gửi/đọc mail cố tình không cho "app/bot" tự đứng tên gửi thay, chỉ cho phép sau khi một người dùng thật đăng nhập cấp quyền Đăng nhập lại bằng --as user (mục 3.4), không dùng --as bot cho mọi thao tác liên quan tới mail
11 Kiểm tra mailbox trả về accessible_mailboxes: [], mailbox address not found dù đã đăng nhập đủ quyền Workspace Lark chưa từng bật dịch vụ Mail — quyền (scope) có sẵn không có nghĩa dịch vụ đã tồn tại Phải vào Lark Admin Console bật dịch vụ Mail cho cả tổ chức trước (mục 3.6), quyền chỉ là điều kiện cần, không phải điều kiện đủ
12 Gõ trực tiếp tên tiếng Việt vào lệnh curl -d '{"full_name":"Trần..."}' trong Git Bash để test → dữ liệu lưu vào Base cũng bị vỡ chữ Ngay bước gõ lệnh qua Git Bash trên Windows, chuỗi tiếng Việt bị mã hoá sai trước khi tới curl Ghi nội dung cần gửi ra 1 file .json bằng công cụ hỗ trợ UTF-8 (không gõ tay trong terminal), rồi gọi curl --data-binary @file.json
13 Publish trang HTML (bản hướng dẫn) báo lỗi content has U+FFFD ... invalid UTF-8 Trong file có 1 ký tự lỗi thật (dùng làm ví dụ minh hoạ cho lỗi #5, gõ nguyên văn ký tự hỏng vào tài liệu) Thay ký tự lỗi bằng mã HTML tương đương &#xFFFD; thay vì gõ ký tự thật
14 Ghi lead vào app Lark mới tạo báo code: 99991672 "Access denied. One of the following scopes is required: [bitable:app, base:record:create]" App mới tạo mặc định chưa có quyền đọc/ghi Bitable nào cả — quyền phải xin thủ công, không tự có sẵn dù dùng --as bot đúng cách Lỗi trả kèm sẵn link tắt trong error.helps[0].url dạng .../app/<APP_ID>/auth?q=<quyền-thiếu> — mở link đó, bấm Add/Enable đúng quyền được liệt kê sẵn, bấm Save/Publish. Xem mục 3.5c
15 Đăng nhập Lark Developer Console, bấm "Developer Console" chỉ thấy 2 khung "Start from scratch" / "Start with agent-ready defaults" mời tạo app mới, không thấy app cũ đâu Đây là màn hình mặc định để tạo app mới, không phải danh sách app đã có — dễ tưởng nhầm là "chưa có app nào" hoặc "lạc trang" Dùng icon kính lúp 🔍 cạnh nút "Developer Console" để tìm app theo tên, hoặc kiểm tra lại đã đăng nhập đúng tài khoản/tổ chức Lark chưa. Xem mục 3.5b
16 Copy một Base sang bản mới ("... Sao chép"), dùng lại app_token/table_id cũ để ghi thì lỗi/ghi nhầm chỗ Base copy sinh ra app_token và toàn bộ table_id mới hoàn toàn, không giữ lại token nào từ bản gốc Lấy lại app_token từ URL Base mới, liệt kê lại bảng + cột, thử ghi test lại từ đầu — không được giả định giống bản gốc. Xem mục 3.5d
17 Tiêu đề trên runbook.html (bản web) hiện dấu tiếng Việt bị tách rời, lệch vị trí (kiểu "băt́ đâù" thay vì "bắt đầu") CSS khai font-family:'Source Serif 4',Georgia,serif cho tiêu đề nhưng không có <link> tải font "Source Serif 4" → trình duyệt tự rớt xuống Georgia — đúng font đã biết thiếu glyph dấu tổ hợp tiếng Việt (xem lỗi #6, lần này xảy ra ở CSS trình duyệt chứ không phải Pillow) Đổi font tiêu đề sang cùng họ với phần thân bài ('IBM Plex Sans',-apple-system,'Segoe UI',sans-serif) — bỏ hẳn Georgia khỏi danh sách font dự phòng cho tiếng Việt

Phụ lục — tham số của dự án mẫu "Học Nhớ Lâu" (đối chiếu nhanh)

Tên Giá trị
Thư mục dự án C:\Chip brain\production\leadpage-hoc-nho-lau\
Trang live https://hocnholau.hoctapsuotdoi.com/
Cloudflare Pages project leadpage-hoc-nho-lau
Cloudflare Account ID 60de3aaef715b29963bf41f78202e2f1
Lark App ID cli_aa1d3dcf18781e18
Lark Base app_token WnpLbtT3Aan18esDVeXjFhvUpee
Bảng (table_id) tbllJvXvMUhvvPC6 ("17.1 Lead Magnet")
Link mẫu bảng 17.1 https://kjp96ge2h38x.jp.larksuite.com/base/WnpLbtT3Aan18esDVeXjFhvUpee?table=tbllJvXvMUhvvPC6&view=vewD0CUDke — đây là bảng dùng chung cho MỌI leadpage (lọc theo cột SourceID), không tạo bảng mới cho mỗi ebook. Ai cần dựng một hệ thống lead-capture mới (dự án khác) thì copy bảng từ link mẫu này làm điểm khởi đầu, thay vì dựng lại từ đầu.
Cột đánh dấu đã gửi Email Ebook Đã Gửi (checkbox)
Mailbox gửi mail ebook@hoctapsuotdoi.com
Task Scheduler HocNhoLau-GuiEbookQuaLarkMail — mỗi 2 phút
Script gửi email scripts\send-ebook-emails.py
Log gửi email logs\send-ebook.log
Skill gốc dùng để dựng GCD-mkt-leadpage-ebook-tron-goi

Phụ lục 2 — tham số dự án "Case Study Business" (2026-09-06, Base RIÊNG không dùng chung)

Khác với dự án "Học Nhớ Lâu" ở trên (dùng chung 1 Base cho mọi leadpage), dự án này copy hẳn 1 Base riêng (mục 3.5d) + tạo 1 app Lark mới riêng (mục 3.5b-c) — mỗi thứ đều có token khác 100% so với bản gốc, KHÔNG được lẫn lộn với bảng tham số ở trên.

Tên Giá trị
Thư mục dự án (ebook) C:\Chip brain\output\2026-09-06-ebook-netflix-blockbuster\
Thư mục dự án (leadpage) C:\Chip brain\production\leadpage-casestudy-business\
Ebook PDF casestudy-netflix-blockbuster.pdf (case study Netflix vs Blockbuster, 4 trang)
Sách lật Heyzine https://heyzine.com/flip-book/4e300476a9.html
Trang chuyển hướng ebook (dễ nhớ, thay vì dán link Heyzine dài) https://casestudybusiness.hoctapsuotdoi.com/ — chỉ là 1 trang HTML redirect, deploy qua Cloudflare Pages project casestudy-business
Trang leadpage live https://casestudy.hoctapsuotdoi.com/
Cloudflare Pages project (leadpage) leadpage-casestudy-business
Cloudflare Account ID 60de3aaef715b29963bf41f78202e2f1 (dùng chung tài khoản Cloudflare, khác Pages project)
Lark App ID (RIÊNG, mới tạo) cli_aa17c70acda1de14
Lark Base app_token (Base COPY riêng) VeVvbTwO2aJcXmsi5NdjFRydp8c
Bảng (table_id trong Base copy) tbl7zAcb8zbvPq1t ("17.1 Lead Magnet", bản copy)
Link Base copy https://ijprnxesfdd8.jp.larksuite.com/base/VeVvbTwO2aJcXmsi5NdjFRydp8c
Quyền đã cấp cho app mới bitable:app, base:record:create (xem mục 3.5c)
Ai cần dựng thương hiệu/dự án mới tương tự Lặp lại đúng mục 3.5b (tạo app) → 3.5c (cấp quyền) → 3.5d (copy Base) thay vì dùng chung Base "Học Nhớ Lâu"