Skip to content

SDD-029 · Từ khoá, Search Console, bounce rate và checklist chất lượng site ​

Một phần của SDD-029. Bản đồ từ khoá phụ huynh, các đợt sửa theo Search Console, giảm bounce rate, checklist HTTPS, header, llms.txt.

Lịch sử quyết định ​

Bản đồ từ khoá phụ huynh và họ trang Cambridge, Oxford (SRC-1296, 08.10.2026) ​

Chủ dự án 08.10.2026: "Làm SEO cho xong xuôi." Đã làm:

PhầnChốt
Nguồnapps/web/content/keywords.yaml: 200 nhóm ý định tìm kiếm x 5 cụm = 1000 cụm phụ huynh gõ (IELTS 50 nhóm, SAT 35, luyện nói 30, ngữ pháp 35, Cambridge 30, Oxford 20). Mỗi nhóm khai topic, intent, stage, priority (ƯỚC LƯỢNG, không phải lượt tìm kiếm: repo không có nguồn số đó) và đúng một target
Chống ăn thịt nhauKhông hai nhóm chung URL, không một cụm ở hai nhóm (so sau khi bỏ dấu); cổng chặn
Bảng cho người đọcops/seo-keywords.md, sinh bằng node apps/web/scripts/keyword-coverage.mjs --docs
Cổngapps/web/scripts/keyword-coverage.mjs --gate chạy cuối npm run build của web: URL có trong dist/ và sitemap (hoặc là bài đã viết trong plan.yaml), mọi từ của cụm chính trong title hoặc H1, mọi từ của bốn cụm còn lại trong chữ trang, mô tả hoặc FAQ. Lúc gộp: 1000/1000 cụm khớp, 57% xuất hiện nguyên văn (lượt hai: 100%)
Trang mớiHọ trang nội dung /cambridge (30 trang: Starters, Movers, Flyers, KET, PET, FCE, CAE/CPE, thang điểm, CEFR, đăng ký, kết quả, xét tuyển lớp 6, chương trình Cambridge, IGCSE, A Level, Checkpoint, sách) và /oxford (20 trang: Oxford Test of English, Oxford Placement Test, sách Oxford, từ điển, Oxford hay Cambridge). Thế giới parents, tĩnh như mọi họ trang (§6.1), không thư viện mới
Sự thậtNemo12 không tổ chức hai kỳ thi ấy và không bán sách. Không trang nào ghi lệ phí, lịch, tên trung tâm hay cách một trường Việt Nam cụ thể dùng chứng chỉ; không in band IELTS quy đổi. Nguồn và phần chưa đối chiếu được ghi ở chú thích đầu mỗi file trong content/seo/cambridge/ và content/seo/oxford/, đã đối chiếu ở lượt hai (dòng dưới)
Trang cũTiêu đề mang từ khoá chính: trang app qua KEYWORD_TITLES trong routes.ts (áp SAU withCanonicals), trang họ nội dung sửa title, bài D1 sửa frontmatter. Câu phụ huynh gõ mà trang chưa trả lời: khối "Phụ huynh hay hỏi" (SearchFaq, dữ liệu searchFaqData.ts) cho trang app, content/seo/keywordFaq.ts cho họ trang nội dung, mục ## Câu hỏi thường gặp cho bài D1 (34 bài, phải nạp lại 4 file seed sau deploy)
Link nội bộArticleHubLinks (chân mọi trang và cuối trang chủ) thêm Cambridge, Oxford; hub mỗi họ link mọi trang con; /site-map tự gồm mọi trang nội dung
Skill/seo-keywords mới; /seo-page và /article-plan tra bản đồ trước khi chọn từ khoá
Đối chiếu sự thật (lượt hai, 08.10.2026)Mọi khẳng định trong content/seo/cambridge/*.ts và content/seo/oxford/*.ts đối chiếu với trang chính thức; chú thích đầu file nay ghi URL nguồn và ngày kiểm thay cho ghi chú "viết theo trí nhớ". Sửa: KET Writing (email hoặc ghi chú 25 từ và truyện theo ba tranh 35 từ, cambridgeenglish.org/exams-and-tests/key/exam-format); KET Listening 30 phút đã gồm 6 phút chép đáp án; PET Speaking 10 tới 12 phút (trước ghi 12 tới 14, .../preliminary/exam-format); Oxford Placement Test bỏ "khoảng một giờ", nay 40 tới 45 câu, giờ do trung tâm đặt; Young Learners Placement Test bỏ thời lượng (không trang OUP nào công bố); Oxford Reading Tree bỏ "dùng rộng rãi". Khớp nguồn, giữ nguyên: thời lượng FCE, CAE, CPE, mọi khoảng điểm Cambridge English Scale và ranh grade A/B/C, chứng chỉ không hết hạn, khiên YLE, IGCSE/A Level/Checkpoint, các mô-đun, thang 0 tới 140, thời hạn kết quả của Oxford Test of English. Không ghi mức CEFR cho Oxford Discover vì OUP không công bố một khoảng chung
Cụm nguyên vănCổng keyword-coverage --gate nay đỏ khi dưới 90% cụm xuất hiện nguyên văn (EXACT_MIN), in tỉ lệ theo chủ đề, --exact liệt kê cụm còn thiếu. Từ 571/1000 lên 1000/1000: câu hỏi thêm trong searchFaqData.ts, keywordFaq.ts và mục FAQ của 82 bài D1 (sinh lại 4 file scripts/seed-articles-*.sql, nạp qua /seed-data sau deploy). Đổi một cụm: "ngữ pháp cho ielts sat" thành "ngữ pháp cho ielts và sat"
Tranh hero trong bản tĩnhĐo 08.10.2026 (Lighthouse mobile): /ielts/band 72, /cambridge/starters 73, LCP 9,5 s, 86% là chờ tải. Khung tĩnh của họ trang nội dung (staticShell trong prerender.mjs) không có ô hero mà ContentPage vẽ (PagePhoto), nên tranh chỉ bắt đầu tải sau JS và lượt gọi API ảnh. Nay prerender.mjs vẽ ô ấy bằng CHÍNH component (renderPagePhoto trong src/ssrApp.tsx), cùng khối n12-boot "kho ảnh rỗng" như trang app (SRC-1295), nên tranh nằm sẵn trong HTML với fetchpriority="high" và React không đổi gì khi thay vào. Cổng: seo-gate.mjs chặn trang nội dung thiếu ô hero, hoặc có ô mà thiếu <img> khi bản dựng biết kho ảnh rỗng. Đo local (vite preview, không CDN): /ielts/band 84, /cambridge/starters 85, ngang /grammar 86 trên cùng máy; CLS 0

Search Console 08.10.2026: dữ liệu có cấu trúc, ban biên tập, trang mỏng, IndexNow, độ sâu click (SRC-1295) ​

Đợt hai sau SRC-1292, cùng số liệu Search Console 08.10.2026 (782 URL "Discovered - currently not indexed"). SRC-1292 lo đường link; đợt này lo bốn thứ còn lại Google dùng để xếp hàng crawl: trang nói nó LÀ gì (JSON-LD), AI viết (E-E-A-T), trang nào không đáng index, và báo URL mới cho máy tìm kiếm khác.

  • JSON-LD. Organization ở mọi trang mang logo PNG (avatar-1024.png; Google bỏ qua logo SVG, thứ trang bài viết từng khai), cùng @id https://nemo12.com/#organization ở trang dựng sẵn lẫn trang bài viết của worker, nên Google đọc ra MỘT thực thể. Trang chủ thêm publishingPrinciples trỏ /about/editorial. Trang ngoài bảng route (/mentors, /community, /privacy, năm trang linh vật) trước đây không có JSON-LD nào, nay có Organization, WebSite, BreadcrumbList, WebPage (pageJsonLdString trong apps/web/src/site/jsonld.ts). Article (trang route kind: "article", trang nội dung) và BlogPosting (bài trong D1) mang image, author là Ban biên tập, và mọi tham chiếu @id có node tương ứng ngay trong trang. FAQPage giữ nguyên luật cũ: chỉ khai khi trang có phần hỏi đáp hiển thị. Không khai Course offers: giá đọc từ GET /v1/public/membership-pricing và đổi theo ngày; nhúng lúc build là có thể khai một giá đã hết hạn (cùng lý do SRC-1170 không nạp bảng giá vào HTML).
  • Ban biên tập. /about/editorial (HTML tĩnh dựng bằng khuôn trang bài viết, không React, xem editorialPage trong apps/web/worker/articles/render.ts): ai chịu trách nhiệm nội dung, bài được làm thế nào (bản thiết kế, danh sách sự kiện có nguồn, soạn với hỗ trợ của AI, kiểm tra tự động, trích dẫn đối chiếu Crossref), điều không hứa, ý nghĩa ngày cập nhật, chỗ báo sai. Không bịa người hay chức danh: tác giả là thực thể Organization "Ban biên tập Nemo12" (https://nemo12.com/about/editorial#team). Bài viết có dòng tác giả link tới trang ấy, kèm ngày đăng (khi khác ngày cập nhật) và ngày cập nhật, dạng DD.MM.YYYY. /about link tới trang này.
  • Trang mỏng. Luật: trang được index (trừ /) phải có ít nhất MIN_STATIC_WORDS = 60 chữ trong HTML tĩnh (thân trang trừ script, style, svg, nav, header, footer: thứ bot đọc khi chưa chạy JavaScript). Dưới mức ấy thì noindex,follow, bỏ canonical, rời sitemap. Danh sách đo ngày 08.10.2026 nằm ở THIN_PATHS / THIN_PREFIXES trong apps/web/src/site/routes.ts: /soon/*, sách của AI Teen, AP, Essay, Math (26 chữ), sáu hub theo cấp học, /testimonials; và /specialist-schools (LEGACY_META). apps/web/scripts/seo-gate.mjs đỏ build khi một trang index được rơi dưới ngưỡng. Bài D1 dưới MIN_ARTICLE_WORDS = 300 chữ cũng noindex và rời sitemap của cụm. Trang trùng nội dung đã có luật canonical riêng (SRC-1150, withCanonicals).
  • Kiểm hình dạng JSON-LD trong seo-gate: mọi trang index (trừ /) có BreadcrumbList; Organization có logo raster tuyệt đối; Article/BlogPosting có headline, image, author, publisher và tham chiếu giải được trong trang; FAQPage có câu hỏi và câu trả lời; Course có name, description, provider. Phần bài viết D1 kiểm bằng apps/web/worker/articles/render.test.ts.
  • Độ sâu click. /site-map (HTML tĩnh, directoryPage trong render.ts): mọi trang index xếp theo khu, cộng hub và category của bốn cụm bài viết; link từ mọi chân trang và từ cuối trang chủ (ArticleHubLinks). apps/web/scripts/click-depth.mjs (chạy cuối npm run build) dựng đồ thị link tĩnh của dist/ cộng các trang bài viết dựng bằng CHÍNH hàm render của worker từ content/articles/**, rồi đỏ build khi một trang index nằm sâu hơn 3 click từ /. Trước khi sửa: 158 trang sâu hơn 3 click (sâu nhất 6) và 116 trang không có đường link nào từ / (gần hết /math, vì bộ lọc khoá học chỉ chạy bằng JavaScript). Sau: 1.006 trang, sâu nhất 3. Hub bài viết không phân trang: category đã liệt kê đủ bài bằng link thường.
  • lastmod. Không đổi cách tính (ngày commit cuối của file nguồn, SRC-1292; bài D1 dùng updated_at). Hai trang tĩnh mới vào sitemap-pages.xml. Không làm lịch đăng bài dần: 400 bài đã công khai và một phần đã được index, rút xuống là mất chỗ đã có.
  • IndexNow (Bing, Yandex). Khoá công khai theo thiết kế của giao thức: file apps/web/public/<khoá>.txt. apps/web/scripts/indexnow.mjs đọc sitemap ĐANG CHẠY (cả sitemap bài viết của worker), lấy URL có lastmod trong 2 ngày qua và gửi tới https://api.indexnow.org/indexnow; bước "IndexNow ping (web)" chạy sau deploy web trong .github/workflows/deploy-app.yml. Khoá chưa lên production hay API lỗi thì chỉ là ::warning::, không bao giờ làm đỏ deploy. Bài nạp qua seed-data.yml (không qua deploy web) được báo ở lượt deploy web kế tiếp trong vòng 2 ngày, hoặc chạy tay npm run indexnow -w @nemo12/web.
  • Việc tay: xác minh site trong Bing Webmaster Tools (cách nhanh nhất: nhập từ Google Search Console, không cần file); nếu chọn xác minh bằng file thì đặt BingSiteAuth.xml do Bing cấp vào apps/web/public/ như file Google đang có. Gửi lại sitemap.xml trong Search Console sau deploy.

Truy vấn gần trang 1: tiêu đề trả lời đúng câu hỏi (SRC-1295, 08.10.2026) ​

Search Console 90 ngày tới 05.10.2026: 37 click, 152 lượt hiển thị, vị trí trung bình 17.4; ngoài truy vấn thương hiệu, mọi truy vấn đều 0 click. Truy vấn đứng vị trí 3 tới 10 mà 0 click là lỗi tiêu đề và đoạn trích, không phải lỗi xếp hạng, nên chúng được sửa trước.

Truy vấnTrangSửa
cần mang gì khi đi thi ielts (7.4)/ielts/articles/test-day/ielts-exam-day-checklisttiêu đề, mô tả, câu trả lời ở hai câu đầu, FAQ
sổ ghi lỗi sai (9.8)/grammar/articles/writing-accuracy/know-your-error-patternsnhư trên
bảng học phí ielts (3.0)/ielts/tuition (routes.ts)tiêu đề, mô tả; vẫn không ghi con số tiền (SRC-1047)
xét tuyển đại học bằng sat (6.0)/sat/articles/target-score/sat-score-vietnam-university-admissiontiêu đề, mô tả, câu trả lời đầu bài, FAQ
ielts academic vs general (67.7)/ielts/articles/target-band/ielts-academic-vs-general-trainingnhư trên
ngữ pháp tiếng anh gồm những gì (17)/grammar/articles/foundations/grammar-foundations-checklisttiêu đề, mô tả, câu trả lời đầu bài
  • Luật: <title> của bài là title · <brand> nên phần title giữ tối đa khoảng 47 ký tự để cả chuỗi không quá 60; truy vấn đứng đầu tiêu đề; mô tả tối đa 155 ký tự.
  • Mỗi trang được 2 tới 3 bài cùng cụm link tới, chữ neo chính là truy vấn.
  • Bài sống trong D1: sau khi gộp phải chạy seed-data với scripts/seed-articles-{ielts,sat,grammar}.sql.
  • Bản nháp backlink và việc chủ dự án làm tay (Google Business Profile, Bing Webmaster Tools): backlink-drafts-2026-10.

Search Console 08.10.2026: liên kết nội bộ tới cụm bài viết (SRC-1292) ​

Số liệu Search Console (property https://nemo12.com/): 322 trang đã index, 870 chưa; 782 trong số đó là "Discovered - currently not indexed" (Google biết URL nhưng chưa crawl), phần lớn là bốn cụm bài viết (/{ielts,sat,speak,grammar}/articles) và cụm /math. Crawl ~30 lượt/ngày cho ~1.200 URL; 11 liên kết ngoài từ 4 tên miền; 28 ngày qua 30 click, 115 lượt hiển thị. Ít uy tín thì ít lượt crawl, và trang không được trang mạnh nào link tới thì xếp cuối hàng.

  • Bốn hub bài viết được link từ MỌI chân trang và từ cuối trang chủ (ArticleHubLinks trong apps/web/src/site/SiteNav.tsx). Trước đây trang chủ, /students, /parents không link tới cụm nào.
  • apps/web/worker/index.ts: đường dẫn có gạch chéo cuối trả 301 về bản không gạch chéo (Cloudflare tự trả 307; Search Console ghi 5% lượt crawl là "Moved temporarily").
  • RSS (/{service}/articles/feed.xml) mang X-Robots-Tag: noindex: bản sao toàn văn của bài, bị xếp vào "Crawled - currently not indexed".
  • Kệ "Bài viết" theo ngữ cảnh trên mọi trang của một dịch vụ (/ielts…, /sat…, /speak…, /grammar…, kể cả bản /parents/...): năm category và một bài tiêu biểu của mỗi category (apps/web/src/site/ArticleShelf.tsx; dữ liệu apps/web/scripts/articles/shelf.mjs sinh từ bản soạn). Khung tĩnh trang nội dung dựng cùng markup (staticShelf trong prerender). Trang dịch vụ và trang nội dung IELTS/SAT nay có 11 link tới bài viết ngay trong HTML dựng sẵn.
  • Sitemap theo khu: khu nào từ 15 trang trở lên có sitemap-pages-<khu>.xml riêng (apps/web/scripts/gen-sitemap.mjs), để Search Console báo tỉ lệ index theo từng khu (riêng /math 99 trang).

Giảm bounce rate (SRC-1296, 08.10.2026) ​

Mỗi trang phải cho người đọc một bước thứ hai ngay trong HTML tĩnh (không popup, không xê dịch bố cục):

  • Bài viết D1 (worker/articles/render.ts): mục lục "Trong bài này" khi bài có từ 3 mục h2 (h2 tự mang id), số phút đọc, link chủ đề ngay dòng tác giả, một khối "Đọc thêm" ở giữa bài, khối "Bài tiếp theo" (hết chủ đề thì quay về trang chủ đề, không ngõ cụt), ba bài cùng chủ đề, rồi CTA của dịch vụ. Link ra ngoài nemo12.com mở tab mới với noopener.
  • Trang dịch vụ (ArticleShelf và staticShelf của prerender, cùng markup): khối "Bước tiếp theo" cuối trang lấy đúng CTA trong plan.yaml của dịch vụ; trang nào chính là đích của CTA thì không hiện.
  • 404: các lối đi lấy từ một danh sách src/site/notFoundLinks.ts (khoá học, cụm bài viết, /site-map), dùng chung cho NotFound.tsx và 404.html.

Đo lường. public/engage.js nạp trên mọi trang công khai (React, khung tĩnh, spa-shell, 404, bài viết, /about/editorial): tạo dataLayer, nạp Google Tag Manager GTM-K8HHG87P (container mang sẵn GA4, không cài GA riêng) SAU load cộng lúc rảnh, hoặc ở lần tương tác đầu; bỏ qua khi người đọc chọn "Chỉ cần thiết". ID chỉ nằm ở file này. Sự kiện đẩy vào dataLayer: page_view (đổi route SPA), engaged_session, scroll_50, cta_click, related_click (link mang data-n12="cta" hoặc "related"). Song song là tín hiệu first-party không cookie: tối đa hai sendBeacon mỗi lượt tải tới POST /e (worker/engage.ts), l khi vào trang và e khi đọc thật (10 giây hiển thị, cuộn 50%, hoặc bấm link/nút). Worker ghi một dòng log {"event":"engage","kind","path","reason"}, không IP, không user agent.

Cách đọc. Cloudflare, Workers, nemo12-web, Observability: lọc event = "engage", nhóm theo kind và path. Bounce rate của một trang = 1 - số e / số l; reason của dòng l là tên miền giới thiệu. Trong GA4, "Engagement rate" và sự kiện engaged_session cho cùng thước đo theo định nghĩa của Google.

Tốc độ. Lighthouse mobile trên bản build chạy bằng wrangler dev (08.10.2026): / 98, /grammar 95, /ielts/band 100, một bài IELTS 100; CLS 0 cả bốn trang.

Checklist chất lượng site: HTTPS, header bảo mật, icon, llms.txt (SRC-1296, 08.10.2026) ​

Đo bằng curl trên https://nemo12.com theo website growth checklist; các mục còn đỏ và cách sửa:

MụcSửa
TECH-07: http:// trả 200apps/web/worker/security.ts httpsRedirect(): url.protocol hoặc x-forwarded-proto là http thì 301 sang https:// (gộp luôn www), trước mọi bước khác
QUALITY-11..17: thiếu header bảo mậtwithSecurityHeaders() bọc MỌI response của worker (worker chạy trước kho tĩnh nên phủ cả file tĩnh, bài viết D1, 404, redirect): HSTS 1 năm + includeSubDomains, nosniff, strict-origin-when-cross-origin, Permissions-Policy tắt camera, micro, vị trí (nemo12.com không dùng micro; luyện nói ở learn), X-Frame-Options: SAMEORIGIN
CSPShip dạng Content-Security-Policy-Report-Only trước: GTM có thể nạp thẻ từ tên miền ngoài danh sách, chặn nhầm là mất đo lường im lặng. Danh sách nguồn ghi ở đầu worker/security.ts; script inline (boot loader, theme, JSON-LD) cần 'unsafe-inline'. Đổi CSP_ENFORCE = true khi một tuần không có vi phạm
TECH-20public/llms.txt: mô tả Việt + Anh, các mục chính, bốn hub bài viết, sitemap
SQ-10, QUALITY-20apps/web/scripts/icons.mjs (npm run icons) sinh từ favicon.svg: favicon.ico (16+32), apple-touch-icon.png 180 nền trắng, icon-192/512.png; site.webmanifest. Link nằm trong index.html (nên có ở mọi trang prerender, spa-shell, 404) và worker/articles/render.ts
ONPAGE-05/06, SQ-05Trang chủ: H1 hết sr-only, hiện thành dòng giới thiệu nhỏ trên bước chọn vai; "Bạn là ai?" và câu hỏi bước 2 thành H2, nên HTML tĩnh có H2. Các trang dịch vụ chính đã có H2 trong bản prerender

Test: apps/web/worker/security.test.ts.

Đợt hai: lastmod, một bước chuyển hướng, độ dài tiêu đề, cache, trang Liên hệ (SRC-1296, 08.10.2026) ​

MụcSửa
TECH-05: 645 URL cùng lastmod = ngày deployapps/web/scripts/gen-sitemap.mjs vốn đã lấy ngày commit cuối của file nguồn, nhưng deploy-app.yml checkout NÔNG nên mọi file mang ngày của commit duy nhất. Nay deploy checkout fetch-depth: 0; ngày dự phòng là một ngày CỐ ĐỊNH (2026-09-01), không bao giờ là "hôm nay". Bài D1 vẫn dùng updated_at
TECH-11: http://nemo12.com/about/ đi hai bước 301canonicalRedirect() trong worker/security.ts gộp https, bỏ www, bỏ gạch chéo cuối và giải bí danh (aliasFor) thành MỘT bước 301
ONPAGE-01, SQ-02: tiêu đề 30 tới 60 ký tựsrc/site/titleFit.ts brandTitle(): hậu tố thương hiệu (" · Nemo12", " · NEMO SAT") chỉ giữ khi còn vừa 60, quá thì gỡ dần; tiêu đề ngắn chưa có thương hiệu thì thêm. Gọi ở bảng ROUTES, pageTitle() của họ trang nội dung, ContentPage và head() của bài viết. seo-gate nay CHẶN build khi trang lập chỉ mục có tiêu đề ngoài 30 tới 60
ONPAGE-03, SQ-03: mô tả 70 tới 160clampDescription() cắt ở ranh giới câu gần nhất (áp cho ROUTES và mọi họ trang nội dung, không sửa file nội dung); fitDescription() không còn lùi về ứng viên dưới 70 khi có ứng viên dài cắt được. seo-gate chặn build ngoài khung
SQ-07: tỉ lệ chữ trên HTML thấp ở /, /ieltsCSS nhúng sẵn bỏ các biến --* không ai đọc (pruneUnusedVars trong prerender.mjs; CSS đầy đủ vẫn tải sau lần vẽ đầu); bỏ ghi chú HTML của template; trang chủ thêm một câu giới thiệu dưới H1. /: 44,3 KB, 0,9% thành 35,9 KB, 1,7%. /ielts: 91,0 KB, 2,6% thành 79,9 KB, 3,1%
QUALITY-02: /assets/* trả max-age=0withCacheHeaders(): file có hash trong /assets/ nhận public, max-age=31536000, immutable; HTML giữ nguyên max-age=0 để luôn kiểm lại
CONTENT-08, CRO-13: không có trang Liên hệ/contact tĩnh (contactPage() trong worker/articles/render.ts, prerender ghi ra dist/), có trong sitemap, sơ đồ trang và chân trang (cả React lẫn bài viết). Chỉ dùng kênh đã có: hello@nemo12.com và Page Facebook, LinkedIn, YouTube, Substack. Không có số điện thoại, địa chỉ, Zalo OA (đã bỏ 25.08.2026) hay link Messenger riêng trong repo nên không ghi; thêm là việc của chủ dự án
CONTENT-06: tác giảGiữ ký tên Ban biên tập; /about/editorial thêm mục "Về ban biên tập" nói rõ từng vai (thiết kế cụm bài, soạn, đối chiếu, sửa). Ký tên một tác giả cá nhân là quyết định của chủ dự án

Test: apps/web/worker/security.test.ts (một bước 301, cache), apps/web/src/site/titleFit.test.ts.