Sửa Lỗi Internal Links Checker CI Fail — Pattern vs File
🔧 Sự cố CI: 14 broken links và cách fix — phân tích root cause, implement solution mới, và bài học về validation script.
Hôm nay, một series bài về Google Preferred Sources của blog đột ngột nhận thông báo sửa lỗi internal links checker CI fail: 14 broken internal links trải rộng trên 8 file. Nhưng lạ là, khi mở những file đó, content toàn bộ đã đúng, slug cũng hợp lệ. Vậy tại sao link lại "broken"?
Đây là câu chuyện về một script validation sai cách, việc debug để tìm root cause, và cách rewrite lại script để nó thực sự hữu ích.
Vấn đề: Sửa Lỗi Internal Links Checker Khi CI Báo FAIL
Đầu tiên, cùng nhìn vào error output từ GitHub Actions CI:
FAIL: 14 bad href(s) in 8 file(s)
public/posting/debug-zola-series-navigation-links/index.html
- /posting/zola-series-nav-setup/
- /posting/seo-series-articles/
- /posting/zola-build-debug-templates/
public/posting/google-preferred-sources-1-tu-thuat-toi-giao-dien-tim-kiem/index.html
- /posting/google-preferred-sources-2-dieu-kien-va-giac-nay-hoat-dong/
[...8 more files...]
Điều đáng nghi là: tất cả 6 bài Google Preferred Sources đã tạo xong (google-preferred-sources-1.md tới -6.md đều trong repo). HTML được sinh đúng, slug đúng. Tại sao lại báo missing?
Phân tích ban đầu
Một số giả thuyết:
- File HTML không được sinh đúng? (Không — CI log hiển thị build success)
- Slug sai? (Không — grep xác nhận slug khớp)
- Link format sai? (Có thể — cần kiểm tra URL format)
Vấn đề nằm ở chính script kiểm tra, không phải content.
Root Cause #1: Link tới bài viết không tồn tại
Ba file được tham chiếu trong debug-zola-series-navigation-links.md không bao giờ tồn tại:
- [Cách setup Zola series navigation](/posting/zola-series-nav-setup/) — thiếu
- [SEO best practices cho series](/posting/seo-series-articles/) — thiếu
- [Debugging Zola build issues](/posting/zola-build-debug-templates/) — thiếu
Đó là những bài viết mà tác giả lên kế hoạch viết nhưng chưa kịp. Để sửa, mình thay thế chúng bằng các bài Zola thực tế tồn tại:
- [Tạo blog với Zola từ A–Z](/posting/tao-blog-voi-zola/)
- [Tự động deploy Zola với GitHub Actions](/posting/tu-dong-deploy-zola-github-actions/)
- [Zola vs Hugo: so sánh chi tiết](/posting/zola-vs-hugo/)
✅ Fix #1 complete: 3 broken links đã được replace bằng bài viết thực tế.
Root Cause 2: Script kiểm tra dùng pattern matching sai cách
Đây là phần thực sự quan trọng. Script check_internal_links.py ban đầu có logic như thế này:
# Cũ — kiểm tra /zola/ prefix
_BAD_HREF_RE = re.compile(
r"""href=["'](/(?!zola/)[^"'#?]+)["']""",
re.IGNORECASE,
)
def _is_bad_href(href: str) -> bool:
href = href.strip()
if href.startswith(SITE_PREFIX + "/"): # /zola/...
return False
if href.startswith("/"):
return True # ❌ BẤT CỨ link "/" không có /zola/ đều "bad"
return False
Vấn đề: Script này yêu cầu mọi internal link phải có /zola/ prefix. Nhưng:
- Production site (seomoney.org) không dùng
/zola/— URL là/posting/... - GitHub Pages build site ở
/zola/nhưng lại dùng custom domain - Script kiểm tra pattern, không kiểm tra file existence
Nói cách khác, script nói: "Link này không có /zola/ nên nó bad" — nhưng không bao giờ kiểm tra liệu file đó có tồn tại trong public/ không.
Vấn đề với pattern matching
Pattern /zola/ là hardcoded. Nếu:
- Site deploy ở
/blog/thay vì/zola/→ script fail - Site dùng custom domain không cần prefix → script fail
- URL structure thay đổi → phải rewrite script
Giải pháp tốt hơn: Kiểm tra xem link có trỏ tới file tồn tại hay không.
Fix #2: Rewrite script để validate file existence
Mình rewrite script từ:
# ❌ CŨ: Kiểm tra prefix pattern
href="/posting/..." → BAD (không có /zola/)
href="/posting/..." → GOOD
Sang:
# ✅ MỚI: Kiểm tra file existence
href="/posting/..." → Kiểm tra public/posting/.../ hoặc public/posting/.../index.html tồn tại?
Có → GOOD
Không → BAD
Code mới:
def _resolve_path(href: str) -> Path | None:
"""Resolve href to a file in public/."""
if not href.startswith("/"):
return None
relative = href.lstrip("/")
# Thử cả hai: file trực tiếp hoặc index.html trong folder
candidates = [
PUBLIC / relative,
PUBLIC / relative / "index.html",
]
for candidate in candidates:
if candidate.exists():
return candidate
return None
def _is_bad_href(href: str) -> bool:
"""Check if href is an internal link that doesn't exist."""
href = href.strip()
if not href or any(href.startswith(p) for p in SKIP_PREFIXES):
return False
if href.startswith(BASE_URL):
return False
if not href.startswith("/"):
return False
# ✅ Kiểm tra file existence thay vì pattern
return _resolve_path(href) is None
Lợi ích:
- URL agnostic — không phụ thuộc
/zola/,/blog/, hay bất kỳ prefix nào - Thực tế — kiểm tra file thực sự tồn tại, không phải giả định pattern
- Dễ maintain — nếu URL structure thay đổi, script vẫn hoạt động
Kết quả
Sau 2 fix:
- ✅ 3 broken reference links → replace bằng bài viết tồn tại
- ✅ Script rewrite để validate file existence thay vì
/zola/prefix
CI giờ:
- Không còn yêu cầu
/zola/prefix - Thực tế kiểm tra link có trỏ tới file tồn tại không
- Phù hợp với production site structure (seomoney.org không dùng
/zola/)
Bài học rút ra
1. Pattern matching không phải validation
# ❌ Pattern matching — brittle
if not href.startswith("/"):
return True # Bad!
# ✅ File existence — robust
if not file_exists(href):
return True # Bad!
Pattern là hardcoded assumption. File existence là ground truth.
2. Test assumptions trước implement
Script này tồn tại vì ai đó giả định: "Internal links phải có /zola/". Nhưng họ không kiểm tra:
- Production URL structure là gì?
- GitHub Pages routing hoạt động như thế nào?
- Custom domain CNAME file có ảnh hưởng không?
→ Ngay từ đầu, nên ask: "Cái gì là broken link? File không tồn tại? URL sai format? Routing fail?"
3. Script validation nên kiểm tra "là gì", không phải "không phải gì"
# ❌ Phủ định logic
if not thing.has_property:
return bad
# ✅ Khẳng định logic
if not thing_exists:
return bad
Khẳng định dễ verify hơn phủ định.
4. Local test trước CI
zola build
python3 scripts/check_internal_links.py
Nếu local pass, remote cũng pass. Tiết kiệm CI time + feedback loop nhanh hơn.
Áp dụng vào dự án của bạn
Nếu bạn cũng maintain Zola site hoặc static site generator nào khác, dưới đây là checklist validation script:
Checklist khi viết validation script
- Kiểm tra existence, không phải pattern — "file tồn tại?" thay vì "string match?"
-
URL agnostic — không hardcode
/zola/,/blog/, hay bất kỳ prefix nào - Test trên local trước — chạy script local, verify nó pass, rồi mới push
-
Error message rõ ràng — nên báo "link
/posting/foo/không trỏ tới file tồn tại" chứ không phải "bad href" - Cách quanh vòng edge cases — trailing slash, index.html, query string…
Code template
def validate_link(href: str) -> bool:
"""Return True if link is valid (exists), False if broken."""
if not href.startswith("/"):
return True # External link, không check
# Kiểm tra file tồn tại
target = PUBLIC / href.lstrip("/")
if target.exists():
return True
# Thử index.html
if (target / "index.html").exists():
return True
return False # ❌ BrokenKết luận
Lỗi CI này dạy rằng:
- Pattern matching là anti-pattern cho validation — kiểm tra existence thay vì format
- URL agnostic scripts bền vững hơn — không phụ thuộc infrastructure detail
- Ground truth là cái tồn tại, không phải cái ta giả định
Sau khi rewrite, script không còn yêu cầu /zola/ — nó thực tế kiểm tra link trỏ tới file nào tồn tại. Điều này phù hợp hơn với reality của site deployment.
Bài viết liên quan
- Debug Lỗi Zola Series Navigation: Từ 11 Broken Links Đến Fix — phân tích Zola-specific issue
- Tạo blog với Zola từ A–Z — setup Zola site đúng cách
- Tự động deploy Zola với GitHub Actions — CI/CD pipeline
Kỳ vọng từ bài này: khi gặp lỗi validation script tương tự, bạn sẽ nghĩ: "Pattern hay existence?" — và chọn existence làm ground truth.
Tham khảo & Nguồn dữ liệu
1. Liên kết bên ngoài được sử dụng trong bài viết
2. Liên kết nội bộ liên quan
3. Bản quyền & Ghi nguồn
Một phần dữ liệu trong bài viết được tham khảo từ GitHub Actions CI. Mọi thương hiệu, tên sản phẩm và tài liệu gốc thuộc quyền sở hữu của chủ sở hữu tương ứng. Bài viết chỉ trích dẫn, tổng hợp và phân tích — không nhằm thay thế tài liệu chính thức.
Bình luận
Đang tải bình luận…
Chưa có bình luận nào. Hãy là người đầu tiên chia sẻ ý kiến.
Đăng nhập để tham gia thảo luận.
Đăng nhập bằng Google để bình luậnChỉ dùng để bình luận. Không truy cập trình soạn thảo/CMS.
Không kết nối được máy chủ. Vui lòng thử lại.