Bài 1 series LLM (commit 840aa22) là post đầu tiên trên blog dùng diagram style mới. 4 inline SVG nhúng thẳng trong markdown, palette coral/amber/teal kiểu Anthropic, theme-adaptive qua CSS vars. Trước đó cả blog chỉ có một style diagram duy nhất, terminal mono green dùng cho series Kibana và K8s. Bài này document quá trình từ “cần một option khác” sang “wire-up xong, deploy production, không hồi quy theme cũ”.
Không phải tutorial step-by-step. Đây là engineering log: decision matrix, 4 cái pitfall đã đụng, pre-deploy checklist sau cùng.
Tại sao cần Style 2
Style 1 (terminal mono, accent green #16a34a) hợp với mọi diagram trong series Kibana và K8s: lifecycle ILM, ingest pipeline, RBAC matrix. Mấy thứ đó có vibe yaml/readme, terminal mono kéo độ dày visual xuống vừa phải.
Sang series LLM thì câu hỏi khác hẳn. Cách visualize attention scoring (token “nó” với 7 token trước, score 0.05 cho “Con”, 0.70 cho “thảm”) nhìn như nào với palette mono green duy nhất? Mọi <rect> đều cùng màu, mọi <text> cùng fill, distinction giữa “nguồn”, “trung gian”, “đích” phải dựa vào dashed border. Tôi thử phác một bản, không pass. Concept-heavy diagram cần semantic color coding (input/process/output, hot/cold), không phải austerity.
Câu hỏi specific hơn: vẽ pipeline tokenize → embed → transformer → project → sample → loop mà mỗi giai đoạn có role khác nhau (data movement, computation, sampling), 5 box cùng màu là cách dở. Style 1 không cho thêm ramp mà không gãy convention của series cũ.
Decision matrix
Bốn lựa chọn evaluated trước khi chốt:
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| Mermaid | Markdown native, ai cũng quen | Visual không match terminal aesthetic kể cả theming. Đã reject 2026-04-15 | Đã loại |
| Pre-rendered PNG | Author bằng Figma, control 100% | Không adapt dark mode, không zoom, file size, không edit nhanh | Loại |
| Extend Style 1 với extra ramps | Reuse infra, không thêm convention | Style 1 dùng 4 var (--svg-fg/muted/border/accent), không có pattern cho semantic ramps. Bolt-on sẽ gãy mental model | Loại |
| Hand-roll inline SVG với var system riêng | Theme-adaptive, semantic ramps, control 100%, fit vào Astro markdown sẵn có | Phải register vars, viết convention, maintain 2 system | Chọn |
Lý do chốt hand-roll: Astro markdown render inline SVG natively (không cần plugin), CSS var trong :root + .dark đã có sẵn cho Style 1, mở rộng cùng pattern thì không thêm dependency. Cost chính là viết convention và register vars, làm một lần dùng dài.
Anthropic palette: vì sao coral + amber + teal
Public material của Anthropic (anthropic.com/research, docs) đi theo hướng pastel soft, rounded rectangles, generous whitespace, subtle borders. Tone warm minimal. Cụ thể trio màu thường thấy: coral/sage/beige, đôi khi lavender.
Tôi cân nhắc 3 phương án palette:
- Coral + amber + teal (chọn). Warm minimal, đủ contrast 3 categories, semantic-friendly: coral = sample/output (warm), amber = transform/compute (intensive), teal = data movement (cool, neutral).
- Purple + teal + gray. Lạnh hơn, gần Stripe/Linear hơn là Anthropic. Reject.
- Coral + purple + green. Vấn đề: green đụng accent của Style 1 (
#16a34alight,#4ade80dark). Cùng trang nếu có cả 2 style, reader sẽ nhầm semantic của green. Reject.
Coral + amber + teal vẫn để chỗ cho 6 ramps khác (gray, purple, pink, blue, green, red) khi diagram khác cần. 9 ramps total, dùng max 2-3 ramps per diagram. Constraint cứng trong skill ~/.claude/skills/nf-diagram-authoring/.
Implementation
Hai phần. Phần một: register CSS vars trong global stylesheet. Phần hai: convention cho mỗi SVG file.
Vars trong global.css
Commit cb3bb82 (2026-05-17), thêm vào src/styles/global.css cả block :root (light) và .dark. Light block (lines 88-99):
:root {
/* SVG diagram tokens (style 2 - guide-based) */
--t: #171717;
--s: #737373;
--b: #fafafa;
--c-gray-bg: #f3f4f6; --c-gray-st: #9ca3af;
--c-teal-bg: #ccfbf1; --c-teal-st: #14b8a6;
--c-purple-bg: #ede9fe; --c-purple-st: #8b5cf6;
--c-coral-bg: #ffedd5; --c-coral-st: #fb923c;
--c-pink-bg: #fce7f3; --c-pink-st: #ec4899;
--c-blue-bg: #dbeafe; --c-blue-st: #3b82f6;
--c-green-bg: #d1fae5; --c-green-st: #10b981;
--c-amber-bg: #fef3c7; --c-amber-st: #f59e0b;
--c-red-bg: #fee2e2; --c-red-st: #dc2626;
}
Dark block (lines 158-169) đảo: --b: #0a0a0a, --t: #e5e5e5, mỗi ramp -bg đẩy về tone deep (#431407 cho coral dark vs #ffedd5 cho coral light), -st đẩy về tone sáng hơn so với light. Logic chung: bg phải đủ tối để text fill var(--t) đọc được, st phải đủ sáng để stroke nổi trên bg.
3 base vars: --t (text primary), --s (text secondary, muted), --b (canvas bg, ít khi dùng trực tiếp vì SVG thường để fill="none" cho canvas).
Inline SVG pattern
Mỗi SVG dùng class guide-svg ở <svg> root. Bên trong có <style> block riêng để map class-to-fill. Pattern lặp đi lặp lại:
<svg class="guide-svg" xmlns="..." viewBox="0 0 680 220" ...>
<defs>
<marker id="arr-p" viewBox="0 0 10 10" refX="8" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" .../>
</marker>
</defs>
<style>
.guide-svg { font-family: 'JetBrains Mono', 'SF Mono', Menlo, monospace; }
.guide-svg .th { font-size: 13px; font-weight: 500; fill: var(--t); }
.guide-svg .t { font-size: 13px; fill: var(--t); }
.guide-svg .ts { font-size: 11px; fill: var(--s); }
.guide-svg .arr { stroke: var(--t); stroke-width: 1.25; fill: none; }
.guide-svg .c-coral rect { fill: var(--c-coral-bg); stroke: var(--c-coral-st); }
.guide-svg .c-amber rect { fill: var(--c-amber-bg); stroke: var(--c-amber-st); }
.guide-svg .c-teal rect { fill: var(--c-teal-bg); stroke: var(--c-teal-st); }
</style>
<text class="th" x="40" y="22">Pipeline inference LLM</text>
<g class="c-teal"><rect x="58" y="70" width="100" height="56" rx="10"/></g>
<text class="th" x="108" y="94" text-anchor="middle">tokenize</text>
...
</svg>
Astro markdown render thẳng. Không cần plugin (@astrojs/mdx đã enable nhưng SVG block không phụ thuộc MDX, post .md thuần cũng pass). Build pipeline coi nó như HTML fragment.
4 SVG cụ thể của bài 1 LLM (pipeline overview, attention scoring, projection+softmax với bar chart, autoregressive loop) đều theo pattern này, file llm-hoat-dong-the-nao-mental-model-cho-dev.md trên main.
4 pitfall
1. Class-to-fill mapping KHÔNG nằm trong global stylesheet
Bài học lớn nhất. CSS vars (--c-coral-bg, --c-coral-st…) global trong global.css. Nhưng cái rule .guide-svg .c-coral rect { fill: var(--c-coral-bg); stroke: var(--c-coral-st); } thì KHÔNG global. Phải inline trong <style> block của mỗi SVG.
Decision này có lý do: nếu cho global, mọi page (kể cả page không có SVG) phải load 27 rule (9 ramps × 3 shape rect/circle/ellipse). Page weight tăng cho 0 benefit. Inline thì chỉ post nào có SVG mới ship rules đó.
Trade-off: copy/paste 5-10 dòng <style> cho mỗi SVG. Hơi noisy, accept được. Khi tôi viết SVG đầu tiên cho bài 1, lỗi đầu tiên là quên cái block này. Box <g class="c-coral"> không paint. Tưởng vars chưa register, kiểm tra DevTools mới thấy thiếu rule. Reference template: .local/demo-svg/index.html lines 181-202.
2. Inline SVG <style> là GLOBAL scope, không scoped
Cái này nhiều dev không biết. Inline <style> trong SVG không scoped vào SVG container. Nó cascade ra toàn page. Nếu tôi viết .th { font-size: 13px } thì mọi element class th trên page đều bị apply, kể cả markdown table headers nếu vô tình cùng class.
Workaround: prefix mọi selector bằng .guide-svg. Tức .guide-svg .th, .guide-svg .c-coral rect, không bao giờ chỉ .th. Class guide-svg đặt trên <svg> root, chỉ match descendants bên trong. Nếu 2 SVG cùng class guide-svg cùng page, rule lặp 2 lần, browser dedupe, OK.
3. Theme-specific hex fallback trap (lesson 2026-04-21)
Cũ nhưng phải nhắc lại vì pattern lặp. Khi viết fill: var(--t, #171717), fallback #171717 chỉ fire khi --t missing. Nhìn ổn light mode (cùng #171717 của light --t). Nhưng nếu var bị unregister sau migration, dark mode rơi về #171717 text trên #0a0a0a bg = gần như invisible. Light mode vẫn pass visual check, bug ship silent.
Bug đã xảy ra 2026-04-21 với bài tieng-viet-ton-x2-token-data-noi-khac: vars Style 1 lúc đó chỉ define inline trong SVG <style>, memory (sai) ghi là global. Dark mode tất cả phase heading + attr value gần như mất hình.
Rule: trong SVG <style>, dùng fill: var(--t) không fallback, hoặc fill: var(--t, #888) (mid-gray neutral). KHÔNG bao giờ #171717, #1c1917, #0a0a0a, #fafaf9, #e7e5e4 làm fallback. Grep check trước deploy.
4. Em-dash trong SVG <text> content lọt qua linter prose
Đây là cái mới gặp khi nhúng 4 SVG vào bài 1. Tôi accidentally để 1 em-dash trong subtitle SVG (đại loại prompt → tokens → vectors). Project rule .claude/rules/no-ai-dash-tells.md ban em-dash trong prose. Mọi pipeline pre-deploy của tôi grep em-dash trong markdown prose, không grep trong SVG <text> content.
May là grep pattern grep -nE "—|[^<!]--[^>]| - " chạy trên cả file .md, nên SVG embed cũng bị catch (vì SVG nằm trong file .md). Fix bằng cách đổi em-dash sang . hoặc ,. Nhưng nếu SVG đặt trong file .svg riêng (như Style 1) thì grep prose sẽ miss. Pattern này sneaky vì SVG text feel như “asset”, reader não bypass linter.
Pre-deploy checklist
3 bước, phải pass trước mỗi commit có SVG mới hoặc edit global.css:
- Vars registered trong cả
:rootvà.dark. Grep--svg-fgcho Style 1 hoặc--c-coral-bgcho Style 2. Phải match trong CẢ hai block. Một block thôi đủ break dark mode. - Không có theme-specific hex fallback bên trong SVG
<style>. Grep pattern#171717\|#1c1917\|#0a0a0a\|#fafaf9\|#e7e5e4trong file SVG hoặc file.mdchứa SVG. Match nào cũng sai. - Visual check trên CẢ light và dark mode trên production sau khi deploy. Click
[light]/[dark]toggle, scroll qua từng diagram, text node nào cũng phải đọc được. KHÔNG tin preview light-only. Bug 2026-04-21 ship được vì skip step này.
Thêm bước 4 sau khi gặp pitfall em-dash: grep —|[^<!]--[^>]| - trên file .md chứa SVG, không chỉ markdown prose.
Pattern reusable
Setup này portable. Mọi blog Astro có theme light/dark qua CSS vars đều copy được. Concrete: register 21 vars (3 base + 9 ramps × 2 properties) vào :root và .dark, copy convention <style> block, dùng class prefix .guide-svg. Astro markdown render inline SVG không cần plugin.
Nhưng vẫn có một câu hỏi tôi chưa trả lời được: khi nào dùng Style 1, khi nào Style 2? Convention hiện tại trong memory là “terminal feel, detail-dense, yaml vibe → Style 1; multi-category process flow, semantic color coding → Style 2”. Trong thực tế hơi mờ. Bài Kibana ingest pipeline nếu viết lại hôm nay, tôi có thể chọn Style 2 vì có 3-4 stage rõ ràng. Bài LLM autoregressive loop có thể dùng Style 1 vì chỉ linear arrow. Default tôi đặt là Style 1 (gần aesthetic gốc của blog hơn), Style 2 cho concept-heavy.
Blog của bạn dùng diagram style nào? Hand-rolled SVG, Mermaid, hay pre-rendered PNG? Có gặp dark mode bug nào tương tự không?