Ảnh cover của khoá học
Một trăm chín mươi khoá thì không ai chụp đủ ảnh, và một thẻ không ảnh nằm cạnh chín thẻ có ảnh trông như khoá bị bỏ dở. Ảnh cover ở đây được vẽ bằng thuật toán: cùng một đầu vào luôn ra cùng một tệp, sửa phong cách một chỗ thì cả cộng đồng đổi theo, và mỗi ảnh chỉ vài kilobyte nằm thẳng trong D1.
Hai lớp, cố ý tách rời
| Lớp | Ai quyết | Ở đâu |
|---|---|---|
| Phong cách cộng đồng, bảng màu, hình nền, khung | Cố định theo cộng đồng | STYLES + LOOK trong scripts/course_cover_svg.py |
| Motif của khoá, hình ở giữa | Người soạn chọn theo nội dung | content/courses/covers.json |
Phong cách không lấy ngẫu nhiên theo slug. Ngẫu nhiên thì mỗi ảnh một kiểu và mất luôn cái chung, mười khoá của một cộng đồng phải nhìn là biết cùng một nhà.
Phần ngẫu nhiên duy nhất là các nhiễu nhỏ (độ nghiêng, khoảng cách, sắc độ), gieo từ slug bằng SHA-256 nên cố định vĩnh viễn với một khoá.
| Cộng đồng | Nền | Khung |
|---|---|---|
food | Kem như giấy gói, lưới gạch men bếp | Đĩa trắng |
drink | Xanh đá lạnh, sọc dọc mặt quầy | Ly cao nhìn thẳng |
edu | Giấy kẻ ngang như vở học trò | Bảng bo góc |
Sáu motif dùng chung cho mọi cộng đồng: bars, gauge, flow, grid, stack, split.
Luật: không chữ trong ảnh
Chữ trong ảnh không dịch được sang tiếng Anh, trình đọc màn hình không đọc được, và nó vỡ khi thẻ co lại trên điện thoại. Bộ biên dịch chặn cứng: thấy <text hoặc font- là dừng.
Tên khoá đã nằm ngay dưới ảnh, bằng chữ thật, dịch được và chọn được.
cover_prompt để làm gì
Câu mô tả ảnh bằng tiếng Việt, do người soạn viết, không hiển thị ở đâu cả. Nó tồn tại vì SVG là một đống toạ độ không ai đọc được, ba tháng sau, câu đó là thứ duy nhất cho biết ảnh này lẽ ra vẽ cái gì. Nó cũng là thứ đưa cho người vẽ nếu sau này thay ảnh thuật toán bằng ảnh thật.
Ba tầng ảnh trên thẻ khoá
image_url (ảnh riêng, người soạn tải lên qua admin, cất ở R2)
↓ không có
cover_svg (ảnh vẽ, cất trong D1)
↓ không có
khung màu token + chữ đầu của tênKhông tầng nào bắt buộc. image_url luôn thắng, ảnh vẽ chỉ là đường lùi.
SVG đổi thành data URI ở SERVER, không ở frontend
API đổi cover_svg thành data:image/svg+xml,... rồi trả trong image_url, và xoá cột thô khỏi payload, đúng cách bìa sách đã làm (svgDataUri trong domains/communities/index.ts). Frontend không bao giờ cầm một chuỗi markup.
Vì sao đổi ở server chứ không ở client: trả chuỗi SVG thô ra ngoài thì sớm muộn có người nhúng nó bằng dangerouslySetInnerHTML, và khi đó mọi thẻ script trong chuỗi ấy chạy với quyền của trang. Chặn ở chỗ dữ liệu rời khỏi API là chặn một lần cho mọi frontend.
Dấu # cắt đứt data URI
Mã màu trong SVG là #C4542B. Không encodeURIComponent thì trình duyệt đọc # là mốc neo và cắt data URI ngay tại đó, ảnh hiện ra mất sạch màu, và không có lỗi nào trong console. svgDataUri đã lo phần này; đừng tự viết lại chỗ khác.
Đường ống
# kiểm: render hết, đếm, và chặn ảnh có chữ
python3 scripts/course_covers_to_sql.py --check
# xem bằng mắt cả một cộng đồng: làm trước khi biên dịch
python3 scripts/course_covers_to_sql.py --preview food /tmp/food.html
# xem đúng một ảnh
python3 scripts/course_cover_svg.py food food-menu-engineering bars > /tmp/x.svg
# biên dịch ra migration
python3 scripts/course_covers_to_sql.py > migrations/<ngày>_covers_<nhóm>.sqlKhớp theo legacy_courses.id chứ không theo slug: legacy_courses.slug là slug tiếng Việt (thiet-ke-thuc-don) còn khoá trong pack mang slug tiếng Anh (food-menu-engineering), và hai thứ đó đã có lúc lệch nhau. course_id lấy từ manifest của chính khoá.
Mười tám khung, mỗi cộng đồng một vật của nghề
| Cộng đồng | Khung | Nền | Cộng đồng | Khung | Nền |
|---|---|---|---|---|---|
| Food | đĩa | lưới | Marketing | thẻ có sọc | lưới |
| Drink | ly cao | sọc dọc | Operations | băng chuyền | kẻ ngang |
| Education | bảng | kẻ ngang | People & HR | kẹp hồ sơ | sọc dọc |
| Beauty | gương đứng | sọc dọc | Product | lục giác | lưới |
| Commerce | tem hàng | lưới | Sales | phễu | sọc dọc |
| Health | vòng chạy | kẻ ngang | Finance | trang sổ | kẻ ngang |
| Home | mái nhà | lưới | Founder | hạt nảy mầm | sọc dọc |
| Hospitality | cửa vòm | sọc dọc | CEO | huy hiệu | kẻ ngang |
| AI & Technology | màn hình | lưới | Customer Success | bong bóng thoại | sọc dọc |
Đủ 190 khoá. Migration tách một tệp một cộng đồng (20260914c01…c18, mỗi tệp 22–48 KB): gộp cả 190 ảnh vào một tệp là khoảng 600 KB, vượt mốc 425 KB đã chạy được, và migration quá cỡ từng làm D1 bỏ dở giữa chừng.
Hệ số thu nhỏ motif: bắt buộc, không phải tuỳ chọn
LOOK có ba phần: khung, kiểu nền, và hệ số thu nhỏ motif. Motif được vẽ theo bán kính của cái đĩa, khung rộng nhất trong bộ. Khung hẹp hơn mà vẫn vẽ ở bán kính đó thì hình chồm ra ngoài viền và ảnh trông như lỗi dựng.
Đo theo chỗ hẹp nhất của khung, không theo chỗ rộng nhất. Gương và hạt nảy mầm xuống tới 0,58–0,62; tem hàng và màn hình giữ được 0,78–0,80.
Một motif viết số cứng thì hệ số không chạm tới được
m_split từng viết cứng cx - 230, width 215, height 300. Nó vẽ đúng một cỡ bất kể khung to nhỏ, nên hệ số thu nhỏ không có tác dụng gì với nó, và trong khung hẹp thì hai khối chồm hẳn ra ngoài. Lỗi này đã âm thầm tồn tại ở Drink và Education từ đợt đầu.
Mọi số đo trong một motif phải tính TỪ r. Có một phép kiểm hình học so hộp bao của motif với hộp bao của khung; nó bắt được đúng lỗi này.