Bài viết ghi lại quá trình tạo trang thaimeo.info — một homepage cá nhân tối giản — từ ý tưởng đến deploy. Không dùng framework, không build tool, chỉ HTML + CSS + một chút JS vanilla.
Yêu cầu ban đầu
Mục tiêu đơn giản: một trang cá nhân gọn, giống phong cách petehouston.com — tên to, tagline ngắn, vài nút liên kết. Không cần blog, không cần portfolio dài dòng.
Thư mục làm việc: /Users/phihongthai/Documents/claude/thaimeo-homepage
Vòng 1 — Bản đầu tiên (quá nhiều nội dung)
Claude tạo ra một trang có header sticky, danh sách bài viết, lưới dự án, section liên hệ, footer… Nhìn như một landing page hoàn chỉnh nhưng không phải thứ mình muốn.
Phản hồi: “hơi nhiều nội dung, đơn giản như ảnh tôi gửi thôi”
Sau khi xem ảnh tham khảo (petehouston.com), Claude hiểu ra: chỉ cần badge role, tên thật to, tagline, và 3-4 nút.
Vòng 2 — Reset về minimal
index.html cuối cùng chỉ có:
<header>
<div class="badge">🖥️ Fulltime father, part-time clickOps</div>
</header>
<main>
<h1>Phí Hồng Thái</h1>
<p class="tagline">Thích xây dựng thứ gì đó hữu ích và học công nghệ mới.</p>
<nav>
<!-- GitHub, Blog, Mountain Blog, Email -->
</nav>
</main>
CSS cũng đơn giản tương ứng: body căn giữa màn hình bằng flexbox,
background #f0f0f0, font Inter weight 300 cho chữ to.
Vòng 3 — Deploy lên Cloudflare (có vài vấn đề)
Vấn đề 1: Workers vs Pages
Mình chọn nhầm flow Cloudflare Workers thay vì Pages. Kết quả deploy ra
thaimeo-homepage.hongthaiphi.workers.dev nhưng truy cập chỉ thấy lỗi trắng trang.
Nguyên nhân: Workers không tự biết phải serve file nào.
Fix: Thêm wrangler.toml khai báo static assets:
name = "thaimeo-homepage"
compatibility_date = "2025-01-01"
[assets]
directory = "."
Vấn đề 2: Deploy command trống
Trong Build configuration, ô Deploy command để trống nên push GitHub không trigger gì cả.
Cần điền npx wrangler deploy để Cloudflare biết cách chạy.
Vấn đề 3: Thiếu API Token
Khi chạy npx wrangler deploy từ terminal thì báo lỗi:
In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN
Fix bằng cách tạo token tại Cloudflare Dashboard → Profile → API Tokens → Create Token (template Edit Cloudflare Workers), rồi:
CLOUDFLARE_API_TOKEN=<token> npx wrangler deploy
Vòng 4 — Chỉnh nội dung & thêm tính năng
Typo trong badge
Fulltim father, partime clickOps ❌
Fulltime father, part-time clickOps ✅
Badge lên đỉnh trang rồi lại xuống
Ban đầu tách badge ra <header> với position: fixed; top: 24px — nhìn ổn trên desktop nhưng trên mobile thì badge chiếm gần hết chiều ngang, đè lên nút toggle.
Sau đó đổi lại: đưa badge vào <main> ngay dưới <h1>, xoá hẳn <header>. Layout cuối cùng:
<main>
<h1>PHI HONG THAI</h1>
<div class="badge">...</div> <!-- ngay dưới tên -->
<p class="tagline">...</p>
<nav>...</nav>
</main>
Kết quả gọn hơn, tự nhiên hơn — tên → vai trò → mô tả → hành động, đọc từ trên xuống rất logic.
Đổi icon badge sang hình cha con
Icon mặc định là màn hình máy tính. Thay bằng SVG vẽ tay hai figure — một cao (bố), một thấp hơn (con):
<!-- dad -->
<circle cx="8" cy="4" r="2"/>
<path d="M5 21v-5H3.5l1.8-5.5..."/>
<!-- kid -->
<circle cx="17" cy="6" r="1.5"/>
<path d="M14.8 21v-4H14l1.3-3.8..."/>
Tên uppercase + wave animation
Dùng JS để split từng ký tự của tên thành <span>, mỗi span có animation-delay stagger:
h1.innerHTML = [...h1.textContent].map((c, i) =>
c === ' '
? '<span class="space"> </span>'
: `<span class="letter" style="animation-delay:${i * 55}ms">${c}</span>`
).join('');
CSS animation:
- Lúc load: chữ bay lên từ dưới với spring (
cubic-bezier(0.34, 1.56, 0.64, 1)) - Hover: wave lần lượt từng ký tự
@keyframes wave {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-14px); }
60% { transform: translateY(-6px); }
}
Dark mode tự động theo giờ
Dùng CSS custom properties để quản lý màu sắc:
:root {
--bg: #f0f0f0;
--text: #111;
--surface: #fff;
--border: #ddd;
}
[data-theme="dark"] {
--bg: #0f0f0f;
--text: #f0f0f0;
--surface: #1a1a1a;
--border: #2e2e2e;
}
Logic JS:
- 6:00–18:00 → light mode
- 18:00–6:00 → dark mode
- Nếu người dùng toggle thủ công → lưu vào
localStorage, ưu tiên hơn auto
const hour = new Date().getHours();
const autoTheme = (hour >= 6 && hour < 18) ? 'light' : 'dark';
html.dataset.theme = localStorage.getItem('theme') ?? autoTheme;
toggle.addEventListener('click', () => {
const next = html.dataset.theme === 'dark' ? 'light' : 'dark';
html.dataset.theme = next;
localStorage.setItem('theme', next);
});
Button toggle: icon mặt trăng (light mode) / mặt trời (dark mode), fixed góc trên phải.
Vòng 5 — Fix mobile
Xem trên iPhone thì tên bị vỡ dòng xấu: “PHI HONG T” / “HAI” — do clamp(3.5rem, 12vw, 7rem) lấy giá trị min 3.5rem = 56px thay vì scale theo viewport.
Vấn đề: khi 12vw < 3.5rem (màn hình nhỏ hơn ~467px), clamp chọn min → font quá to.
Fix: hạ min xuống và thêm white-space: nowrap:
h1 {
font-size: clamp(1.5rem, 9vw, 7rem); /* 9vw ≈ 35px trên iPhone 390px */
white-space: nowrap;
}
Ở 390px: 9vw = 35px nằm trong khoảng [1.5rem, 7rem] → dùng 35px → tên vừa một dòng.
Thêm media query cho mobile:
@media (max-width: 480px) {
.badge { font-size: 0.75rem; padding: 5px 12px; }
.theme-toggle { top: 16px; right: 16px; width: 34px; height: 34px; }
.tagline { font-size: 1rem; }
}
Kết quả
- Repo: github.com/hongthaiphi/thaimeo-homepage
- Live: thaimeo-homepage.hongthaiphi.workers.dev
- Tổng file: 3 file (
index.html,style.css,wrangler.toml) — không có build step, không có dependency
Bài học
- Cloudflare Workers ≠ Pages — Workers cần
wrangler.tomlkhai báo[assets]mới serve được static file. Pages thì tự hiểu hơn. - Deploy command không tự điền — phải set
npx wrangler deploytrong Build configuration mới có CI/CD từ GitHub. - API Token bắt buộc cho môi trường non-interactive (CI, terminal không đăng nhập).
- CSS variables là cách sạch nhất để làm dark mode — không cần JS can thiệp vào từng element, chỉ cần đổi
data-themetrên<html>. clamp()có min floor — nếu min quá lớn, nó override giá trị responsive. Luôn test clamp trên mobile thực tế.- Layout đơn giản thì đừng cố fixed — badge fixed trên đỉnh trông hay trên desktop nhưng lại gây đụng độ với toggle button trên mobile. Inline trong flow tự nhiên hơn.