Rà soát www.conan.school: 09.09.2026
Rà 59 đường (29 trang tĩnh + 30 trang chi tiết lấy slug thật từ chính các trang danh sách) trên hai trục: nội dung/SEO và giao diện mobile 390px. Hai script chạy lại được:
node scripts/audit-www-pages.mjs # trang lỗi, trang rỗng, thẻ tự mô tả
node scripts/audit-www-mobile.mjs # tràn ngang, chữ nhỏ, vùng bấm nhỏ, ảnh hỏngCả hai cần một server đang chạy (www-dev) và không nằm trong CI: chúng đo dữ liệu thật từ API production, nên một lượt chạy đỏ có thể vì dữ liệu chứ không vì mã.
Điều tốt, để biết chỗ nào KHÔNG cần đụng vào
- Không trang nào tràn ngang trên khung 390px. Đây là lỗi mobile phổ biến nhất và www không có.
- Mọi trang trả 200 đều có
<title>,meta description,canonical,<h1>. Hai đường duy nhất thiếu là/activitiesvà/album, chúng trả 301 (đúng), và đã cố ý vắng mặt trong sitemap. - Bản
/enmở được ở mọi đường đã kiểm, không đường nào 404.
P1: hỏng thật, và hỏng theo kiểu hệ vẫn trông như đang chạy
1. /challenges/:slug trả 200 với một trang trống
Loader gọi /api/learning/challenges/:slug thiếu header x-school-id. API trả 400 Missing x-school-id header, catch nuốt lỗi và trả { challenge: null }, nên trang trả 200 với 219 ký tự và không có nội dung nào. Đúng khuôn "catch biến lỗi thành chưa có gì" mà luật SEO đã cấm, và ở đây nó còn dạy máy tìm kiếm rằng URL này tồn tại và rỗng.
Hai chỗ phải sửa: gửi đúng header, và 404 khi không có dữ liệu thay vì 200.
2. Ba họ ảnh hỏng trên production
| Nơi | Đường ảnh | Tình trạng |
|---|---|---|
/books | /images/book-covers/*.svg (12 quyển) | 404 |
/news | /news/thumbnails/*.png | 404 |
/playbooks | https://ww3.conan.school/api/og/playbooks?… | host không phân giải |
ww3.conan.school đáng chú ý nhất: đó không phải một ảnh hỏng, đó là một tên miền không tồn tại được viết cứng trong ba chỗ của mã. Hai họ còn lại là cover_url/thumbnail_url trong D1 trỏ tới file không có trong public/, dữ liệu sai, không phải mã sai.
3. /jtbd trả 502
Thuộc phần Jobs-to-Be-Done đang làm dở ở một nhánh khác (route đã khai ở www, API chưa deploy). Ghi lại để không ai đi tìm nguyên nhân ở chỗ khác.
P2: không hỏng, nhưng trang rỗng hoặc lệch chuẩn
4. 144/181 Deep Book không có bìa
legacy_ebooks: 181 quyển đang bật, 144 không có cover_url, 12 trỏ file 404, chỉ 25 quyển có bìa thật. Kệ sách vì thế là một lưới ô xám ghi "No Cover". Đây là việc của skill course-covers, không phải của giao diện.
5. Bảy route tự render <main> bên trong <main> của khung trang
books.$slug.chapters, books.$slug.sections, jtbd* (3), membership, photos. Hai <main> lồng nhau là lỗi a11y (trình đọc màn hình mất mốc điều hướng chính), và nó cũng làm chính script rà này đo /photos được 66 ký tự trong khi trang đầy chữ.
6. Vùng bấm nhỏ hơn ngưỡng trên mobile
| Trang | Số chỗ | Nặng nhất |
|---|---|---|
| Mọi trang | 5 | liên kết chân trang cao 20px |
/books | 186 | nút VI / EN / BOTH cao 26px, mỗi quyển ba nút |
/reviews | 21 | "Xem trên Facebook" cao 21px |
/announcements | 14 | bộ lọc cao 34px |
WCAG 2.2 AA đặt sàn ở 24px; repo này vốn đã dùng min-h-11 (44px) ở navbar, nên chân trang 20px là lệch với chính chuẩn của mình.
7. /workshops/:slug gần như không có nội dung trong HTML server trả về
390 ký tự, trong khi trang hiển thị đầy chữ. Phần mô tả đi qua TipTapRenderer và chỉ dựng sau khi hydrate, người đọc thấy, máy tìm kiếm không thấy.
8. Vệt lệch design system
photos.tsxdùngborder-black/10thay vìborder-border(14 chỗ)./membershipcó hai khối trang tríblur-[100px]vượt mép phải; không gây cuộn ngang vì chaoverflow-hidden, nhưng nó là giá trị tuỳ ý nằm ngoài token.- Trước đợt này,
/workshops/:slugcó bốn tông xanh khác nhau (vendor-zoomdùng cả cho nút Zalo) vì ba thẻ tài nguyên là ba khối IIFE tự chọn màu riêng, đã gộp thành một component.
Đã sửa (đợt 1, 09.09.2026)
| # | Việc | Kết quả đo lại |
|---|---|---|
| 1 | /challenges/:slug gửi x-school-id, không có dữ liệu thì 404 chứ không 200 | 219 → 1.664 ký tự |
| 2 | Bỏ hẳn ww3.conan.school khỏi ba chỗ, rơi về ảnh mặc định của site | không còn ảnh vỡ ở /playbooks |
| 3 | Vùng bấm về min-h-11: chân trang, /reviews, /announcements, /books, trang chủ | 29/29 trang có lỗi → 1/29 |
| 4 | Gỡ <main> lồng ở bốn route (books chương/mục, membership, photos) | còn jtbd* ở nhánh khác |
| 5 | photos.tsx: border-black/10 → border-border (6 chỗ) | |
| 6 | Chép 12 file bìa sách sang www/public/images/book-covers/ | /books hết ảnh vỡ |
Nguyên nhân của số 6 đáng ghi lại: cover_url trong D1 là đường gốc tương đối (/images/book-covers/…), và file chỉ có trong com.conan.school/public. Cùng một giá trị dữ liệu chạy đúng ở tokyo và 404 ở www, không site nào sai, cái sai là một đường tương đối dùng chung giữa hai site có gốc khác nhau.
Đã sửa (đợt 2, 09.09.2026)
| # | Việc | Kết quả đo lại |
|---|---|---|
| 7 | TipTapRenderer dựng được ở server | /workshops/:slug 390 → 4.216 ký tự |
| 8 | Một bề ngang cho cả site: max-w-7xl (thay 6xl/5xl), trang đọc dài max-w-3xl | 37 file |
| 9 | Song ngữ /courses, /books, /membership |
Số 7 đáng ghi lại: bản cũ dùng useEditor + EditorContent với immediatelyRender: false
- ở server nó trả về
null. Người đọc vẫn thấy nội dung sau khi hydrate nên không ai nhận ra, còn máy tìm kiếm thấy một trang rỗng.generateHTMLcủa@tiptap/corekhông thay thế được: nó cầnwindow. Cách đi được là một bộ dựng JSON → React riêng, và nó còn là một whitelist, khối lạ bị bỏ qua thay vì chèn thẳng vào trang.
Số 8, luật bề ngang, để lần sau không lệch tiếp:
| Loại trang | Bề ngang |
|---|---|
| Danh sách, tổng quan, trang có lưới | max-w-7xl → max-w-[1600px] từ 09.09.2026 |
| Trang đọc dài (tin, thông báo, playbook, sự kiện, điều khoản) | max-w-3xl |
| Hero trang chủ | tràn viền, không giới hạn |
Trước đợt này có tám bề ngang khác nhau trong app/routes (md, xl, 2xl, 3xl, 4xl, 5xl, 6xl, 7xl), nên trang này hẹp một cách khó hiểu còn trang kia rộng hơn hẳn, mà không có quy tắc nào để tra. Navbar và chân trang cũng đưa về max-w-7xl, nếu không logo lệch khỏi cột nội dung.
Đã sửa (đợt 3, 09.09.2026)
| # | Việc | Kết quả đo lại |
|---|---|---|
| 10 | ImageWithFallback: ảnh CÓ đường dẫn nhưng tải không được thì đổi sang ô dự phòng | /members, /news hết ô vỡ |
| 11 | /photos: thẻ album dựng thêm mô tả và số ảnh (API đã trả sẵn, chỉ chưa dùng) | 236 ký tự → có nội dung thật |
| 12 | Song ngữ /communities, /workshops, /news, /reviews, /learner-stories, /photos | |
| 13 | Hai báo động giả của chính script rà (xem dưới) | 29 trang: 6 → 2 cờ |
Số 10, vì sao phép kiểm cũ không bắt được: mọi chỗ đều viết src ? <img/> : <fallback/>, tức chỉ bắt trường hợp thiếu đường dẫn. /members dùng ảnh đại diện Google (lh3.googleusercontent.com), thứ hết hạn và bị chặn theo nhịp mà không ai được báo, còn /news trỏ tới file chưa từng được tạo. Cả hai có src, nên cả hai qua cửa. onError là cách duy nhất biết được.
Số 13, script rà cũng sai được, và sai theo hai kiểu:
- Khối trang trí (
blur-[100px]) nằm trong cha cắt tràn bị báo là "vượt mép phải". Nó không đẩy trang rộng ra; báo động giả làm người ta bỏ đọc báo cáo. Nay bỏ qua phần tử có chaoverflowhidden/clip/auto/scroll. - Đo ở mốc 1.800ms là đo trước khi hydrate ở dev: HTML server trả về vẫn mang thẻ
<img>hỏng, phần thay bằng ô dự phòng chưa chạy. Nâng lên 3.000ms.
Đã sửa (đợt 4, 09.09.2026)
Song ngữ cho toàn bộ trang danh sách và trang giới thiệu còn lại: /start, /mentors, /events, /announcements, /jobs, /playbooks, /challenges, /members, /what-we-believe, /conan-vision.
Kèm một lỗi SEO tìm thấy trên đường: /start tự khai canonical viết cứng vào https://www.conan.school/start. Khung trang đã đặt canonical theo đúng URL đang mở, nên thẻ thứ hai này bảo máy tìm kiếm rằng /en/start chỉ là bản sao của /start, tức bản tiếng Anh không bao giờ được index. Đã gỡ; đây là route duy nhất còn khai canonical riêng.
Đã sửa (đợt 5, 09.09.2026)
Song ngữ phần giao diện của các trang chi tiết: /communities/:slug, /courses/:slug, /books/:slug (+ /deepbooks/:slug), /events/:slug, /news/:slug, /announcements/:slug.
Kèm một lỗi nữa cùng họ với lỗi canonical của /start: /communities/:slug sinh ItemList trong dữ liệu có cấu trúc với URL viết cứng vào đường tiếng Việt. Trang /en liệt kê toàn đường /courses/... là tự mâu thuẫn với hreflang của chính nó, máy đọc thấy một trang tiếng Anh khẳng định nội dung của nó nằm ở URL tiếng Việt.
Ranh giới thật của việc dịch trang chi tiết nằm ở D1, không ở mã: legacy_events, legacy_announcements và legacy_news_articles không có cột *_en nào, nên thân bài luôn là tiếng Việt dù khung đã dịch. Bảng nào có gì: xem i18n-www.
Đã sửa (đợt 6, 09.09.2026)
Mở chỗ chứa cho nội dung tiếng Anh (20260919b_english_content_columns.sql) và nối mã đọc chúng qua pick(): khoá học, sự kiện, thông báo, tin tức. Nội dung điền tới đâu, /en hiện tới đó, không phải sửa mã lần nữa. Migration không điền gì, bản dịch là quyết định của người soạn.
Cột được thêm cho cả hai họ bảng (legacy_* và không tiền tố), vì hai đường đọc khác nhau cùng tồn tại: getD1Client() tự thêm tiền tố, còn SQL thô của events thì không. Thêm cho một họ thôi là cột nằm ở nơi mã không đọc tới, hợp lệ, không lỗi, không bao giờ hiện ra trang.
Kèm: /mentors/:slug, /members/:slug, /learner-stories/:slug, /playbooks/:slug song ngữ.
Một bẫy React gặp hai lần trong đợt này: gọi useLang() ngay trong JSX. Ở MembershipCountdown và ở ba trang chi tiết, chỗ dùng nằm trong nhánh điều kiện, số lần gọi hook đổi theo dữ liệu, và React đọc lệch thứ tự hook giữa hai lượt render. Hook phải nằm ở thân component, luôn luôn.
Đã sửa (đợt 7, 09.09.2026)
Trang chủ tràn viền: hero trước đây là một thẻ có viền nằm giữa hai dải trắng, trên màn 1440px trở lên nó đọc ra là "chưa dựng xong". Cùng lúc khổ chung của cả site đi từ max-w-7xl (1280px) lên max-w-[1600px], kể cả navbar và chân trang: nâng riêng thân trang mà để navbar ở 1280 là logo lệch khỏi cột nội dung. Chữ bên trong hero vẫn giữ khổ đọc được, tràn viền là chuyện của khung, không phải của dòng chữ.
Song ngữ nốt: /books/:slug/chapters/*, /books/:slug/sections/*, /courses/:slug/units/*, /jobs/:.../:..., /workshop-setup-guide. Từ đây mọi trang công khai đã có bản tiếng Anh phần giao diện; bốn trang cố ý không dịch và lý do ghi ở i18n-www.
Còn lại
| Việc | Vì sao chưa làm |
|---|---|
/news ảnh 404 (dữ liệu) | File không tồn tại ở đâu cả. Giao diện nay không vỡ nữa, nhưng thẻ vẫn không có ảnh, việc của nội dung. |
| 144/181 Deep Book không bìa | Việc của nội dung, xem skill course-covers. |
/jtbd 502 | Nhánh khác, chưa deploy API. |
/jobs một liên kết cao 16px | Liên kết nằm GIỮA một câu, WCAG miễn trừ đúng trường hợp này, phóng to sẽ phá dòng chữ. |